Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions emrg/gui/renderer/js/dialogs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,7 +701,8 @@ const Dialogs = (() => {

// rant 2026-08-18T21:32:32:任务卡点击 → 手风琴展开最近运行子表
// (时间 / 干了什么 / 降频)。数据来自 daemon handler.status().recent_runs
// (最多 5 条);旧数据无 summary → fallback impact 拼接;无记录 → 占位文案。
// (最多 5 条)。Rant 2026-08-19T07:06:45(宿主定稿):无 summary 不再
// fallback impact 机器串——空则显示 "-";无记录 → 占位文案。
function buildTaskRunDetail(t) {
const wrap = el("div", { class: "task-run-detail hidden" });
const runs = Array.isArray(t.recent_runs) ? t.recent_runs : [];
Expand All@@ -718,7 +719,6 @@ const Dialogs = (() => {
const row = el("div", { class: "task-run-row" });
row.appendChild(el("span", { class: "task-run-time" }, formatRelativeTime(r.timestamp)));
let done = (typeof r.summary === "string" && r.summary) ? r.summary : "";
if (!done && Array.isArray(r.impact) && r.impact.length) done = r.impact.join(", ");
row.appendChild(el("span", { class: "task-run-done" }, done || "-"));
const flagCell = el("span", { class: "task-run-flag" });
if (r.recommend_slowdown) {
Expand Down
4 changes: 2 additions & 2 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2688,9 +2688,9 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什
assert.ok(warnCount >= 1, "recommend_slowdown=true 应显示建议降频徽章");
const idleCount = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-badge-idle").length`, ctx);
assert.ok(idleCount >= 1, "meaningful=false 应显示空转徽章");
// 旧数据无 summary → fallback impact 拼接(第二行 run
// summary → 显示 "-"(rant 2026-08-19T07:06:45 宿主定稿:不再 fallback impact 机器串
const doneTexts = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")).map((n) => n.textContent)`, ctx);
assert.ok(doneTexts[1].includes("cycle-ts-complete"), `无 summary 应 fallback impact,实际: ${doneTexts[1]}`);
assert.strictEqual(doneTexts[1], "-", `无 summary 应显示 "-",实际: ${doneTexts[1]}`);
// 再次点击 → 折叠
await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx);
await tick();
Expand Down
35 changes: 15 additions & 20 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1160,32 +1160,27 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary
what meaningful work was done this cycle, for humans to read in the
GUI task recent-runs table. Old models / old parsing omit it → "".

Rant 2026-08-19T07:10:40 (root cause): the done frame used to carry an
empty ``content``, so ``completion_summary`` here was empty and the
memoryless LLM could not judge what happened. The done frame now
carries the agent's full final reply (daemon.py done broadcast), so
this prompt is evidence-driven: judge from the real final reply, not
from an empty shell. System + user messages live in
``prompts/vibe_check.j2`` (same live-reload mechanism as system.j2).

Raises on any failure (caller sends ``ok: false``); the scheduler
conservatively leaves its empty-cycle counter unchanged then.
"""
system = (
"你是 EMRG 定时任务调度助手。刚完成一次定时任务「" + (task_name or "") + "」,"
"任务要求与最终回复摘要如下。\n"
"请用 JSON 严格回答(不要任何其他文字),格式:\n"
'{"meaningful": true|false, "recommend_slowdown": true|false, '
'"reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}\n'
"- meaningful:这轮是否对项目产生了有意义的价值(产出/提交/分析/决策/"
"维护动作都算;纯空转/无可做=NTE 算 false)\n"
"- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token"
"(true=建议降低检查频率)\n"
"- reason:简短中文原因(给降频判断用)\n"
"- done:这次干了哪些有意义有价值的事(自然语言列举,如:修了 XX bug / "
"加了 XX 功能 / 分析了 XX;没有则空字符串)"
)
user = (
"任务名称:" + (task_name or "") + "\n"
"任务要求:" + (prompt or "")[:2000] + "\n"
"任务最终回复摘要:" + (completion_summary or "")[:3000]
template = _get_jinja_env().get_template("vibe_check.j2")
system = template.render(
task_name=task_name or "",
prompt=(prompt or "")[:2000],
completion_summary=(completion_summary or "")[:3000],
)
msg = await self.llm.chat(
[
{"role": "system", "content": system},
{"role": "user", "content": user},
{"role": "user", "content": "请基于以上任务信息,严格按 system 中要求的 JSON 格式回答。"},
],
tools=[],
)
Expand DownExpand Up@@ -2411,7 +2406,7 @@ async def _run_tool_loop(

await self._broadcast(session.session_id, {
"request_id": req.id,
"content": "",
"content": full_content or "",
"done": True,
"delta": False,
"session_id": session.session_id,
Expand Down
27 changes: 27 additions & 0 deletions emrg/server/prompts/vibe_check.j2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
{# Vibe check prompt (rant 2026-08-19T07:10:40) — evidence-driven, one-shot.
Rendered by daemon._task_vibe_check via _get_jinja_env() (FileSystemLoader,
same live-reload mechanism as system.j2: editing this template takes effect
without a daemon restart). Context: task_name, prompt, completion_summary. #}
你是 EMRG 定时任务调度助手。刚完成一次定时任务「{{ task_name }}」,以下是该任务的
实际信息(任务要求 + Agent 最终回复摘要)。请基于这些**真实输入**做总结与判断:
只有信息显示确实没做任何事(无回复、无动作)才算空转;不要因为"没有 commit/PR"就
武断判为无产出——工具执行、分析、排查、写 memory、决策等具体动作都是实际工作。

请用 JSON 严格回答(不要任何其他文字),格式:
{"meaningful": true|false, "recommend_slowdown": true|false, "reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}

字段要求:
- meaningful:这轮是否对项目产生了有意义的价值。只要任务最终回复/工作内容显示执行了
具体动作(工具调用、分析、排查、写 memory、决策、产出等)即为 true;只有确实
空转(无回复、无任何动作)才为 false。
- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token
(true=建议降低检查频率)。
- reason:简短中文原因(给降频判断用),引用实际信息说明依据。
- done:从任务最终回复/工作内容中**提炼**这次具体做了什么(工具动作、产出、分析,
自然语言列举,如:执行了 N 次工具调用,git fetch 检查了 X、分析了 Y、写了
memory Z)。**只要回复/工作内容有实质内容就必须写具体动作**;回复确实为空、
纯空转才写空字符串 ""。

任务名称:{{ task_name }}
任务要求:{{ prompt }}
任务最终回复摘要:{{ completion_summary }}
18 changes: 6 additions & 12 deletions emrg/server/scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -779,13 +779,9 @@ def status(self) -> dict:
if self.evolutions:
last = self.evolutions[-1]
last_run_at = last.timestamp
# brief summary: agent's natural-language "done" summary preferred
# (rant 2026-08-18T21:32:32), fallback to machine impact tags.
if last.summary:
last_cycle_summary = last.summary
else:
parts = [str(i) for i in last.impact if i]
last_cycle_summary = ", ".join(parts[:3]) if parts else None
# rant 2026-08-19T07:06:45 (host-finalized): NO machine impact
# fallback — empty summary shows as None (GUI renders "-").
last_cycle_summary = last.summary if last.summary else None
# rant 2026-08-18T21:32:32: last 5 run records for the GUI accordion
# subtable — {timestamp, summary, impact, meaningful,
# recommend_slowdown, tool_count}; all in-memory, no extra I/O.
Expand DownExpand Up@@ -1120,18 +1116,16 @@ async def _run_evolution_cycle(self) -> None:
# rant 2026-08-18T21:32:32: persist the agent's own summary of what
# meaningful work was done (vibe check "done" field) + the vibe flags,
# so the GUI task recent-runs table shows real value, not a machine
# string. Fallbacks: vibe unavailable → None flags + first line of the
# completion summary as a rough summary (never crash).
# string. Rant 2026-08-19T07:06:45 (host-finalized): NO fallback to the
# completion first line — summary uses only the vibe check "done"
# field; empty stays empty (GUI shows "-"), never a machine fallback.
summary = ""
meaningful = None
recommend = False
if vibe_result is not None:
summary = str(vibe_result.get("done") or "")[:500]
meaningful = vibe_result.get("meaningful")
recommend = bool(vibe_result.get("recommend_slowdown"))
if not summary and completion_content:
first = completion_content.strip().splitlines()[0] if completion_content.strip() else ""
summary = first[:500]

log = EvolutionLog(
timestamp=cycle_ts,
Expand Down
20 changes: 10 additions & 10 deletions tests/test_scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -518,7 +518,8 @@ def test_evolution_handler_status_last_run_fields():
handler._empty_cycles = 3
st = handler.status()
assert st["last_run_at"] == "2026-08-18T10:00:00"
assert st["last_cycle_summary"] == "tools-executed=24, cycle-complete"
# rant 2026-08-19T07:06:45: empty summary → None (no machine impact fallback)
assert st["last_cycle_summary"] is None
assert st["saturation"]["empty_cycles"] == 3
assert len(st["recent_runs"]) == 1
r0 = st["recent_runs"][0]
Expand DownExpand Up@@ -1673,10 +1674,10 @@ def test_evolution_cycle_complete_agent_says_meaningful_resets_streak(tmp_path):
assert log.tool_count == 0


def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
"""Vibe check ok but missing 'done' → log.summary falls back to the first
line of the completion content; vibe unavailable → empty summary (never
crash). Rant 2026-08-18T21:32:32."""
def test_evolution_cycle_log_summary_no_completion_fallback(tmp_path):
"""Rant 2026-08-19T07:06:45 (host-finalized): the summary uses ONLY the
vibe check "done" field — NO fallback to the completion first line. Empty
stays empty (GUI renders "-"), never a machine/rough fallback."""
handler, captured = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Reviewed PR and posted LGTM",
"done": True, "delta": False, "session_id": "s"},
Expand All@@ -1686,19 +1687,18 @@ def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
])
asyncio.run(handler._run_evolution_cycle())
log = captured["log"]
assert log.summary == "Reviewed PR and posted LGTM", \
"missing done → completion first line fallback"
assert log.summary == "", \
"missing done → summary stays empty (no completion fallback)"
assert log.meaningful is True

# vibe check entirely unavailable → summary falls back to the first line
# of the completion content (per rant design), flags None/False
# vibe check entirely unavailable → summary stays empty, flags None/False
handler2, captured2 = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Done", "done": True,
"delta": False, "session_id": "s"},
])
asyncio.run(handler2._run_evolution_cycle())
log2 = captured2["log"]
assert log2.summary == "Done", "vibe unavailable → completion first line fallback"
assert log2.summary == "", "vibe unavailable → summary stays empty (no fallback)"
assert log2.meaningful is None
assert log2.recommend_slowdown is False
assert log2.tool_count == 0
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions emrg/gui/renderer/js/dialogs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,7 +701,8 @@ const Dialogs = (() => {

// rant 2026-08-18T21:32:32:任务卡点击 → 手风琴展开最近运行子表
// (时间 / 干了什么 / 降频)。数据来自 daemon handler.status().recent_runs
// (最多 5 条);旧数据无 summary → fallback impact 拼接;无记录 → 占位文案。
// (最多 5 条)。Rant 2026-08-19T07:06:45(宿主定稿):无 summary 不再
// fallback impact 机器串——空则显示 "-";无记录 → 占位文案。
function buildTaskRunDetail(t) {
const wrap = el("div", { class: "task-run-detail hidden" });
const runs = Array.isArray(t.recent_runs) ? t.recent_runs : [];
Expand All@@ -718,7 +719,6 @@ const Dialogs = (() => {
const row = el("div", { class: "task-run-row" });
row.appendChild(el("span", { class: "task-run-time" }, formatRelativeTime(r.timestamp)));
let done = (typeof r.summary === "string" && r.summary) ? r.summary : "";
if (!done && Array.isArray(r.impact) && r.impact.length) done = r.impact.join(", ");
row.appendChild(el("span", { class: "task-run-done" }, done || "-"));
const flagCell = el("span", { class: "task-run-flag" });
if (r.recommend_slowdown) {
Expand Down
4 changes: 2 additions & 2 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2688,9 +2688,9 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什
assert.ok(warnCount >= 1, "recommend_slowdown=true 应显示建议降频徽章");
const idleCount = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-badge-idle").length`, ctx);
assert.ok(idleCount >= 1, "meaningful=false 应显示空转徽章");
// 旧数据无 summary → fallback impact 拼接(第二行 run
// summary → 显示 "-"(rant 2026-08-19T07:06:45 宿主定稿:不再 fallback impact 机器串
const doneTexts = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")).map((n) => n.textContent)`, ctx);
assert.ok(doneTexts[1].includes("cycle-ts-complete"), `无 summary 应 fallback impact,实际: ${doneTexts[1]}`);
assert.strictEqual(doneTexts[1], "-", `无 summary 应显示 "-",实际: ${doneTexts[1]}`);
// 再次点击 → 折叠
await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx);
await tick();
Expand Down
35 changes: 15 additions & 20 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1160,32 +1160,27 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary
what meaningful work was done this cycle, for humans to read in the
GUI task recent-runs table. Old models / old parsing omit it → "".

Rant 2026-08-19T07:10:40 (root cause): the done frame used to carry an
empty ``content``, so ``completion_summary`` here was empty and the
memoryless LLM could not judge what happened. The done frame now
carries the agent's full final reply (daemon.py done broadcast), so
this prompt is evidence-driven: judge from the real final reply, not
from an empty shell. System + user messages live in
``prompts/vibe_check.j2`` (same live-reload mechanism as system.j2).

Raises on any failure (caller sends ``ok: false``); the scheduler
conservatively leaves its empty-cycle counter unchanged then.
"""
system = (
"你是 EMRG 定时任务调度助手。刚完成一次定时任务「" + (task_name or "") + "」,"
"任务要求与最终回复摘要如下。\n"
"请用 JSON 严格回答(不要任何其他文字),格式:\n"
'{"meaningful": true|false, "recommend_slowdown": true|false, '
'"reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}\n'
"- meaningful:这轮是否对项目产生了有意义的价值(产出/提交/分析/决策/"
"维护动作都算;纯空转/无可做=NTE 算 false)\n"
"- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token"
"(true=建议降低检查频率)\n"
"- reason:简短中文原因(给降频判断用)\n"
"- done:这次干了哪些有意义有价值的事(自然语言列举,如:修了 XX bug / "
"加了 XX 功能 / 分析了 XX;没有则空字符串)"
)
user = (
"任务名称:" + (task_name or "") + "\n"
"任务要求:" + (prompt or "")[:2000] + "\n"
"任务最终回复摘要:" + (completion_summary or "")[:3000]
template = _get_jinja_env().get_template("vibe_check.j2")
system = template.render(
task_name=task_name or "",
prompt=(prompt or "")[:2000],
completion_summary=(completion_summary or "")[:3000],
)
msg = await self.llm.chat(
[
{"role": "system", "content": system},
{"role": "user", "content": user},
{"role": "user", "content": "请基于以上任务信息,严格按 system 中要求的 JSON 格式回答。"},
],
tools=[],
)
Expand DownExpand Up@@ -2411,7 +2406,7 @@ async def _run_tool_loop(

await self._broadcast(session.session_id, {
"request_id": req.id,
"content": "",
"content": full_content or "",
"done": True,
"delta": False,
"session_id": session.session_id,
Expand Down
27 changes: 27 additions & 0 deletions emrg/server/prompts/vibe_check.j2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
{# Vibe check prompt (rant 2026-08-19T07:10:40) — evidence-driven, one-shot.
Rendered by daemon._task_vibe_check via _get_jinja_env() (FileSystemLoader,
same live-reload mechanism as system.j2: editing this template takes effect
without a daemon restart). Context: task_name, prompt, completion_summary. #}
你是 EMRG 定时任务调度助手。刚完成一次定时任务「{{ task_name }}」,以下是该任务的
实际信息(任务要求 + Agent 最终回复摘要)。请基于这些**真实输入**做总结与判断:
只有信息显示确实没做任何事(无回复、无动作)才算空转;不要因为"没有 commit/PR"就
武断判为无产出——工具执行、分析、排查、写 memory、决策等具体动作都是实际工作。

请用 JSON 严格回答(不要任何其他文字),格式:
{"meaningful": true|false, "recommend_slowdown": true|false, "reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}

字段要求:
- meaningful:这轮是否对项目产生了有意义的价值。只要任务最终回复/工作内容显示执行了
具体动作(工具调用、分析、排查、写 memory、决策、产出等)即为 true;只有确实
空转(无回复、无任何动作)才为 false。
- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token
(true=建议降低检查频率)。
- reason:简短中文原因(给降频判断用),引用实际信息说明依据。
- done:从任务最终回复/工作内容中**提炼**这次具体做了什么(工具动作、产出、分析,
自然语言列举,如:执行了 N 次工具调用,git fetch 检查了 X、分析了 Y、写了
memory Z)。**只要回复/工作内容有实质内容就必须写具体动作**;回复确实为空、
纯空转才写空字符串 ""。

任务名称:{{ task_name }}
任务要求:{{ prompt }}
任务最终回复摘要:{{ completion_summary }}
18 changes: 6 additions & 12 deletions emrg/server/scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -779,13 +779,9 @@ def status(self) -> dict:
if self.evolutions:
last = self.evolutions[-1]
last_run_at = last.timestamp
# brief summary: agent's natural-language "done" summary preferred
# (rant 2026-08-18T21:32:32), fallback to machine impact tags.
if last.summary:
last_cycle_summary = last.summary
else:
parts = [str(i) for i in last.impact if i]
last_cycle_summary = ", ".join(parts[:3]) if parts else None
# rant 2026-08-19T07:06:45 (host-finalized): NO machine impact
# fallback — empty summary shows as None (GUI renders "-").
last_cycle_summary = last.summary if last.summary else None
# rant 2026-08-18T21:32:32: last 5 run records for the GUI accordion
# subtable — {timestamp, summary, impact, meaningful,
# recommend_slowdown, tool_count}; all in-memory, no extra I/O.
Expand DownExpand Up@@ -1120,18 +1116,16 @@ async def _run_evolution_cycle(self) -> None:
# rant 2026-08-18T21:32:32: persist the agent's own summary of what
# meaningful work was done (vibe check "done" field) + the vibe flags,
# so the GUI task recent-runs table shows real value, not a machine
# string. Fallbacks: vibe unavailable → None flags + first line of the
# completion summary as a rough summary (never crash).
# string. Rant 2026-08-19T07:06:45 (host-finalized): NO fallback to the
# completion first line — summary uses only the vibe check "done"
# field; empty stays empty (GUI shows "-"), never a machine fallback.
summary = ""
meaningful = None
recommend = False
if vibe_result is not None:
summary = str(vibe_result.get("done") or "")[:500]
meaningful = vibe_result.get("meaningful")
recommend = bool(vibe_result.get("recommend_slowdown"))
if not summary and completion_content:
first = completion_content.strip().splitlines()[0] if completion_content.strip() else ""
summary = first[:500]

log = EvolutionLog(
timestamp=cycle_ts,
Expand Down
20 changes: 10 additions & 10 deletions tests/test_scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -518,7 +518,8 @@ def test_evolution_handler_status_last_run_fields():
handler._empty_cycles = 3
st = handler.status()
assert st["last_run_at"] == "2026-08-18T10:00:00"
assert st["last_cycle_summary"] == "tools-executed=24, cycle-complete"
# rant 2026-08-19T07:06:45: empty summary → None (no machine impact fallback)
assert st["last_cycle_summary"] is None
assert st["saturation"]["empty_cycles"] == 3
assert len(st["recent_runs"]) == 1
r0 = st["recent_runs"][0]
Expand DownExpand Up@@ -1673,10 +1674,10 @@ def test_evolution_cycle_complete_agent_says_meaningful_resets_streak(tmp_path):
assert log.tool_count == 0


def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
"""Vibe check ok but missing 'done' → log.summary falls back to the first
line of the completion content; vibe unavailable → empty summary (never
crash). Rant 2026-08-18T21:32:32."""
def test_evolution_cycle_log_summary_no_completion_fallback(tmp_path):
"""Rant 2026-08-19T07:06:45 (host-finalized): the summary uses ONLY the
vibe check "done" field — NO fallback to the completion first line. Empty
stays empty (GUI renders "-"), never a machine/rough fallback."""
handler, captured = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Reviewed PR and posted LGTM",
"done": True, "delta": False, "session_id": "s"},
Expand All@@ -1686,19 +1687,18 @@ def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
])
asyncio.run(handler._run_evolution_cycle())
log = captured["log"]
assert log.summary == "Reviewed PR and posted LGTM", \
"missing done → completion first line fallback"
assert log.summary == "", \
"missing done → summary stays empty (no completion fallback)"
assert log.meaningful is True

# vibe check entirely unavailable → summary falls back to the first line
# of the completion content (per rant design), flags None/False
# vibe check entirely unavailable → summary stays empty, flags None/False
handler2, captured2 = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Done", "done": True,
"delta": False, "session_id": "s"},
])
asyncio.run(handler2._run_evolution_cycle())
log2 = captured2["log"]
assert log2.summary == "Done", "vibe unavailable → completion first line fallback"
assert log2.summary == "", "vibe unavailable → summary stays empty (no fallback)"
assert log2.meaningful is None
assert log2.recommend_slowdown is False
assert log2.tool_count == 0
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions emrg/gui/renderer/js/dialogs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,7 +701,8 @@ const Dialogs = (() => {

// rant 2026-08-18T21:32:32:任务卡点击 → 手风琴展开最近运行子表
// (时间 / 干了什么 / 降频)。数据来自 daemon handler.status().recent_runs
// (最多 5 条);旧数据无 summary → fallback impact 拼接;无记录 → 占位文案。
// (最多 5 条)。Rant 2026-08-19T07:06:45(宿主定稿):无 summary 不再
// fallback impact 机器串——空则显示 "-";无记录 → 占位文案。
function buildTaskRunDetail(t) {
const wrap = el("div", { class: "task-run-detail hidden" });
const runs = Array.isArray(t.recent_runs) ? t.recent_runs : [];
Expand All@@ -718,7 +719,6 @@ const Dialogs = (() => {
const row = el("div", { class: "task-run-row" });
row.appendChild(el("span", { class: "task-run-time" }, formatRelativeTime(r.timestamp)));
let done = (typeof r.summary === "string" && r.summary) ? r.summary : "";
if (!done && Array.isArray(r.impact) && r.impact.length) done = r.impact.join(", ");
row.appendChild(el("span", { class: "task-run-done" }, done || "-"));
const flagCell = el("span", { class: "task-run-flag" });
if (r.recommend_slowdown) {
Expand Down
4 changes: 2 additions & 2 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2688,9 +2688,9 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什
assert.ok(warnCount >= 1, "recommend_slowdown=true 应显示建议降频徽章");
const idleCount = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-badge-idle").length`, ctx);
assert.ok(idleCount >= 1, "meaningful=false 应显示空转徽章");
// 旧数据无 summary → fallback impact 拼接(第二行 run
// summary → 显示 "-"(rant 2026-08-19T07:06:45 宿主定稿:不再 fallback impact 机器串
const doneTexts = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")).map((n) => n.textContent)`, ctx);
assert.ok(doneTexts[1].includes("cycle-ts-complete"), `无 summary 应 fallback impact,实际: ${doneTexts[1]}`);
assert.strictEqual(doneTexts[1], "-", `无 summary 应显示 "-",实际: ${doneTexts[1]}`);
// 再次点击 → 折叠
await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx);
await tick();
Expand Down
35 changes: 15 additions & 20 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1160,32 +1160,27 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary
what meaningful work was done this cycle, for humans to read in the
GUI task recent-runs table. Old models / old parsing omit it → "".

Rant 2026-08-19T07:10:40 (root cause): the done frame used to carry an
empty ``content``, so ``completion_summary`` here was empty and the
memoryless LLM could not judge what happened. The done frame now
carries the agent's full final reply (daemon.py done broadcast), so
this prompt is evidence-driven: judge from the real final reply, not
from an empty shell. System + user messages live in
``prompts/vibe_check.j2`` (same live-reload mechanism as system.j2).

Raises on any failure (caller sends ``ok: false``); the scheduler
conservatively leaves its empty-cycle counter unchanged then.
"""
system = (
"你是 EMRG 定时任务调度助手。刚完成一次定时任务「" + (task_name or "") + "」,"
"任务要求与最终回复摘要如下。\n"
"请用 JSON 严格回答(不要任何其他文字),格式:\n"
'{"meaningful": true|false, "recommend_slowdown": true|false, '
'"reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}\n'
"- meaningful:这轮是否对项目产生了有意义的价值(产出/提交/分析/决策/"
"维护动作都算;纯空转/无可做=NTE 算 false)\n"
"- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token"
"(true=建议降低检查频率)\n"
"- reason:简短中文原因(给降频判断用)\n"
"- done:这次干了哪些有意义有价值的事(自然语言列举,如:修了 XX bug / "
"加了 XX 功能 / 分析了 XX;没有则空字符串)"
)
user = (
"任务名称:" + (task_name or "") + "\n"
"任务要求:" + (prompt or "")[:2000] + "\n"
"任务最终回复摘要:" + (completion_summary or "")[:3000]
template = _get_jinja_env().get_template("vibe_check.j2")
system = template.render(
task_name=task_name or "",
prompt=(prompt or "")[:2000],
completion_summary=(completion_summary or "")[:3000],
)
msg = await self.llm.chat(
[
{"role": "system", "content": system},
{"role": "user", "content": user},
{"role": "user", "content": "请基于以上任务信息,严格按 system 中要求的 JSON 格式回答。"},
],
tools=[],
)
Expand DownExpand Up@@ -2411,7 +2406,7 @@ async def _run_tool_loop(

await self._broadcast(session.session_id, {
"request_id": req.id,
"content": "",
"content": full_content or "",
"done": True,
"delta": False,
"session_id": session.session_id,
Expand Down
27 changes: 27 additions & 0 deletions emrg/server/prompts/vibe_check.j2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
{# Vibe check prompt (rant 2026-08-19T07:10:40) — evidence-driven, one-shot.
Rendered by daemon._task_vibe_check via _get_jinja_env() (FileSystemLoader,
same live-reload mechanism as system.j2: editing this template takes effect
without a daemon restart). Context: task_name, prompt, completion_summary. #}
你是 EMRG 定时任务调度助手。刚完成一次定时任务「{{ task_name }}」,以下是该任务的
实际信息(任务要求 + Agent 最终回复摘要)。请基于这些**真实输入**做总结与判断:
只有信息显示确实没做任何事(无回复、无动作)才算空转;不要因为"没有 commit/PR"就
武断判为无产出——工具执行、分析、排查、写 memory、决策等具体动作都是实际工作。

请用 JSON 严格回答(不要任何其他文字),格式:
{"meaningful": true|false, "recommend_slowdown": true|false, "reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}

字段要求:
- meaningful:这轮是否对项目产生了有意义的价值。只要任务最终回复/工作内容显示执行了
具体动作(工具调用、分析、排查、写 memory、决策、产出等)即为 true;只有确实
空转(无回复、无任何动作)才为 false。
- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token
(true=建议降低检查频率)。
- reason:简短中文原因(给降频判断用),引用实际信息说明依据。
- done:从任务最终回复/工作内容中**提炼**这次具体做了什么(工具动作、产出、分析,
自然语言列举,如:执行了 N 次工具调用,git fetch 检查了 X、分析了 Y、写了
memory Z)。**只要回复/工作内容有实质内容就必须写具体动作**;回复确实为空、
纯空转才写空字符串 ""。

任务名称:{{ task_name }}
任务要求:{{ prompt }}
任务最终回复摘要:{{ completion_summary }}
18 changes: 6 additions & 12 deletions emrg/server/scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -779,13 +779,9 @@ def status(self) -> dict:
if self.evolutions:
last = self.evolutions[-1]
last_run_at = last.timestamp
# brief summary: agent's natural-language "done" summary preferred
# (rant 2026-08-18T21:32:32), fallback to machine impact tags.
if last.summary:
last_cycle_summary = last.summary
else:
parts = [str(i) for i in last.impact if i]
last_cycle_summary = ", ".join(parts[:3]) if parts else None
# rant 2026-08-19T07:06:45 (host-finalized): NO machine impact
# fallback — empty summary shows as None (GUI renders "-").
last_cycle_summary = last.summary if last.summary else None
# rant 2026-08-18T21:32:32: last 5 run records for the GUI accordion
# subtable — {timestamp, summary, impact, meaningful,
# recommend_slowdown, tool_count}; all in-memory, no extra I/O.
Expand DownExpand Up@@ -1120,18 +1116,16 @@ async def _run_evolution_cycle(self) -> None:
# rant 2026-08-18T21:32:32: persist the agent's own summary of what
# meaningful work was done (vibe check "done" field) + the vibe flags,
# so the GUI task recent-runs table shows real value, not a machine
# string. Fallbacks: vibe unavailable → None flags + first line of the
# completion summary as a rough summary (never crash).
# string. Rant 2026-08-19T07:06:45 (host-finalized): NO fallback to the
# completion first line — summary uses only the vibe check "done"
# field; empty stays empty (GUI shows "-"), never a machine fallback.
summary = ""
meaningful = None
recommend = False
if vibe_result is not None:
summary = str(vibe_result.get("done") or "")[:500]
meaningful = vibe_result.get("meaningful")
recommend = bool(vibe_result.get("recommend_slowdown"))
if not summary and completion_content:
first = completion_content.strip().splitlines()[0] if completion_content.strip() else ""
summary = first[:500]

log = EvolutionLog(
timestamp=cycle_ts,
Expand Down
20 changes: 10 additions & 10 deletions tests/test_scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -518,7 +518,8 @@ def test_evolution_handler_status_last_run_fields():
handler._empty_cycles = 3
st = handler.status()
assert st["last_run_at"] == "2026-08-18T10:00:00"
assert st["last_cycle_summary"] == "tools-executed=24, cycle-complete"
# rant 2026-08-19T07:06:45: empty summary → None (no machine impact fallback)
assert st["last_cycle_summary"] is None
assert st["saturation"]["empty_cycles"] == 3
assert len(st["recent_runs"]) == 1
r0 = st["recent_runs"][0]
Expand DownExpand Up@@ -1673,10 +1674,10 @@ def test_evolution_cycle_complete_agent_says_meaningful_resets_streak(tmp_path):
assert log.tool_count == 0


def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
"""Vibe check ok but missing 'done' → log.summary falls back to the first
line of the completion content; vibe unavailable → empty summary (never
crash). Rant 2026-08-18T21:32:32."""
def test_evolution_cycle_log_summary_no_completion_fallback(tmp_path):
"""Rant 2026-08-19T07:06:45 (host-finalized): the summary uses ONLY the
vibe check "done" field — NO fallback to the completion first line. Empty
stays empty (GUI renders "-"), never a machine/rough fallback."""
handler, captured = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Reviewed PR and posted LGTM",
"done": True, "delta": False, "session_id": "s"},
Expand All@@ -1686,19 +1687,18 @@ def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
])
asyncio.run(handler._run_evolution_cycle())
log = captured["log"]
assert log.summary == "Reviewed PR and posted LGTM", \
"missing done → completion first line fallback"
assert log.summary == "", \
"missing done → summary stays empty (no completion fallback)"
assert log.meaningful is True

# vibe check entirely unavailable → summary falls back to the first line
# of the completion content (per rant design), flags None/False
# vibe check entirely unavailable → summary stays empty, flags None/False
handler2, captured2 = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Done", "done": True,
"delta": False, "session_id": "s"},
])
asyncio.run(handler2._run_evolution_cycle())
log2 = captured2["log"]
assert log2.summary == "Done", "vibe unavailable → completion first line fallback"
assert log2.summary == "", "vibe unavailable → summary stays empty (no fallback)"
assert log2.meaningful is None
assert log2.recommend_slowdown is False
assert log2.tool_count == 0
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions emrg/gui/renderer/js/dialogs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,7 +701,8 @@ const Dialogs = (() => {

// rant 2026-08-18T21:32:32:任务卡点击 → 手风琴展开最近运行子表
// (时间 / 干了什么 / 降频)。数据来自 daemon handler.status().recent_runs
// (最多 5 条);旧数据无 summary → fallback impact 拼接;无记录 → 占位文案。
// (最多 5 条)。Rant 2026-08-19T07:06:45(宿主定稿):无 summary 不再
// fallback impact 机器串——空则显示 "-";无记录 → 占位文案。
function buildTaskRunDetail(t) {
const wrap = el("div", { class: "task-run-detail hidden" });
const runs = Array.isArray(t.recent_runs) ? t.recent_runs : [];
Expand All@@ -718,7 +719,6 @@ const Dialogs = (() => {
const row = el("div", { class: "task-run-row" });
row.appendChild(el("span", { class: "task-run-time" }, formatRelativeTime(r.timestamp)));
let done = (typeof r.summary === "string" && r.summary) ? r.summary : "";
if (!done && Array.isArray(r.impact) && r.impact.length) done = r.impact.join(", ");
row.appendChild(el("span", { class: "task-run-done" }, done || "-"));
const flagCell = el("span", { class: "task-run-flag" });
if (r.recommend_slowdown) {
Expand Down
4 changes: 2 additions & 2 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2688,9 +2688,9 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什
assert.ok(warnCount >= 1, "recommend_slowdown=true 应显示建议降频徽章");
const idleCount = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-badge-idle").length`, ctx);
assert.ok(idleCount >= 1, "meaningful=false 应显示空转徽章");
// 旧数据无 summary → fallback impact 拼接(第二行 run
// summary → 显示 "-"(rant 2026-08-19T07:06:45 宿主定稿:不再 fallback impact 机器串
const doneTexts = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")).map((n) => n.textContent)`, ctx);
assert.ok(doneTexts[1].includes("cycle-ts-complete"), `无 summary 应 fallback impact,实际: ${doneTexts[1]}`);
assert.strictEqual(doneTexts[1], "-", `无 summary 应显示 "-",实际: ${doneTexts[1]}`);
// 再次点击 → 折叠
await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx);
await tick();
Expand Down
35 changes: 15 additions & 20 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1160,32 +1160,27 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary
what meaningful work was done this cycle, for humans to read in the
GUI task recent-runs table. Old models / old parsing omit it → "".

Rant 2026-08-19T07:10:40 (root cause): the done frame used to carry an
empty ``content``, so ``completion_summary`` here was empty and the
memoryless LLM could not judge what happened. The done frame now
carries the agent's full final reply (daemon.py done broadcast), so
this prompt is evidence-driven: judge from the real final reply, not
from an empty shell. System + user messages live in
``prompts/vibe_check.j2`` (same live-reload mechanism as system.j2).

Raises on any failure (caller sends ``ok: false``); the scheduler
conservatively leaves its empty-cycle counter unchanged then.
"""
system = (
"你是 EMRG 定时任务调度助手。刚完成一次定时任务「" + (task_name or "") + "」,"
"任务要求与最终回复摘要如下。\n"
"请用 JSON 严格回答(不要任何其他文字),格式:\n"
'{"meaningful": true|false, "recommend_slowdown": true|false, '
'"reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}\n'
"- meaningful:这轮是否对项目产生了有意义的价值(产出/提交/分析/决策/"
"维护动作都算;纯空转/无可做=NTE 算 false)\n"
"- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token"
"(true=建议降低检查频率)\n"
"- reason:简短中文原因(给降频判断用)\n"
"- done:这次干了哪些有意义有价值的事(自然语言列举,如:修了 XX bug / "
"加了 XX 功能 / 分析了 XX;没有则空字符串)"
)
user = (
"任务名称:" + (task_name or "") + "\n"
"任务要求:" + (prompt or "")[:2000] + "\n"
"任务最终回复摘要:" + (completion_summary or "")[:3000]
template = _get_jinja_env().get_template("vibe_check.j2")
system = template.render(
task_name=task_name or "",
prompt=(prompt or "")[:2000],
completion_summary=(completion_summary or "")[:3000],
)
msg = await self.llm.chat(
[
{"role": "system", "content": system},
{"role": "user", "content": user},
{"role": "user", "content": "请基于以上任务信息,严格按 system 中要求的 JSON 格式回答。"},
],
tools=[],
)
Expand DownExpand Up@@ -2411,7 +2406,7 @@ async def _run_tool_loop(

await self._broadcast(session.session_id, {
"request_id": req.id,
"content": "",
"content": full_content or "",
"done": True,
"delta": False,
"session_id": session.session_id,
Expand Down
27 changes: 27 additions & 0 deletions emrg/server/prompts/vibe_check.j2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
{# Vibe check prompt (rant 2026-08-19T07:10:40) — evidence-driven, one-shot.
Rendered by daemon._task_vibe_check via _get_jinja_env() (FileSystemLoader,
same live-reload mechanism as system.j2: editing this template takes effect
without a daemon restart). Context: task_name, prompt, completion_summary. #}
你是 EMRG 定时任务调度助手。刚完成一次定时任务「{{ task_name }}」,以下是该任务的
实际信息(任务要求 + Agent 最终回复摘要)。请基于这些**真实输入**做总结与判断:
只有信息显示确实没做任何事(无回复、无动作)才算空转;不要因为"没有 commit/PR"就
武断判为无产出——工具执行、分析、排查、写 memory、决策等具体动作都是实际工作。

请用 JSON 严格回答(不要任何其他文字),格式:
{"meaningful": true|false, "recommend_slowdown": true|false, "reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}

字段要求:
- meaningful:这轮是否对项目产生了有意义的价值。只要任务最终回复/工作内容显示执行了
具体动作(工具调用、分析、排查、写 memory、决策、产出等)即为 true;只有确实
空转(无回复、无任何动作)才为 false。
- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token
(true=建议降低检查频率)。
- reason:简短中文原因(给降频判断用),引用实际信息说明依据。
- done:从任务最终回复/工作内容中**提炼**这次具体做了什么(工具动作、产出、分析,
自然语言列举,如:执行了 N 次工具调用,git fetch 检查了 X、分析了 Y、写了
memory Z)。**只要回复/工作内容有实质内容就必须写具体动作**;回复确实为空、
纯空转才写空字符串 ""。

任务名称:{{ task_name }}
任务要求:{{ prompt }}
任务最终回复摘要:{{ completion_summary }}
18 changes: 6 additions & 12 deletions emrg/server/scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -779,13 +779,9 @@ def status(self) -> dict:
if self.evolutions:
last = self.evolutions[-1]
last_run_at = last.timestamp
# brief summary: agent's natural-language "done" summary preferred
# (rant 2026-08-18T21:32:32), fallback to machine impact tags.
if last.summary:
last_cycle_summary = last.summary
else:
parts = [str(i) for i in last.impact if i]
last_cycle_summary = ", ".join(parts[:3]) if parts else None
# rant 2026-08-19T07:06:45 (host-finalized): NO machine impact
# fallback — empty summary shows as None (GUI renders "-").
last_cycle_summary = last.summary if last.summary else None
# rant 2026-08-18T21:32:32: last 5 run records for the GUI accordion
# subtable — {timestamp, summary, impact, meaningful,
# recommend_slowdown, tool_count}; all in-memory, no extra I/O.
Expand DownExpand Up@@ -1120,18 +1116,16 @@ async def _run_evolution_cycle(self) -> None:
# rant 2026-08-18T21:32:32: persist the agent's own summary of what
# meaningful work was done (vibe check "done" field) + the vibe flags,
# so the GUI task recent-runs table shows real value, not a machine
# string. Fallbacks: vibe unavailable → None flags + first line of the
# completion summary as a rough summary (never crash).
# string. Rant 2026-08-19T07:06:45 (host-finalized): NO fallback to the
# completion first line — summary uses only the vibe check "done"
# field; empty stays empty (GUI shows "-"), never a machine fallback.
summary = ""
meaningful = None
recommend = False
if vibe_result is not None:
summary = str(vibe_result.get("done") or "")[:500]
meaningful = vibe_result.get("meaningful")
recommend = bool(vibe_result.get("recommend_slowdown"))
if not summary and completion_content:
first = completion_content.strip().splitlines()[0] if completion_content.strip() else ""
summary = first[:500]

log = EvolutionLog(
timestamp=cycle_ts,
Expand Down
20 changes: 10 additions & 10 deletions tests/test_scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -518,7 +518,8 @@ def test_evolution_handler_status_last_run_fields():
handler._empty_cycles = 3
st = handler.status()
assert st["last_run_at"] == "2026-08-18T10:00:00"
assert st["last_cycle_summary"] == "tools-executed=24, cycle-complete"
# rant 2026-08-19T07:06:45: empty summary → None (no machine impact fallback)
assert st["last_cycle_summary"] is None
assert st["saturation"]["empty_cycles"] == 3
assert len(st["recent_runs"]) == 1
r0 = st["recent_runs"][0]
Expand DownExpand Up@@ -1673,10 +1674,10 @@ def test_evolution_cycle_complete_agent_says_meaningful_resets_streak(tmp_path):
assert log.tool_count == 0


def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
"""Vibe check ok but missing 'done' → log.summary falls back to the first
line of the completion content; vibe unavailable → empty summary (never
crash). Rant 2026-08-18T21:32:32."""
def test_evolution_cycle_log_summary_no_completion_fallback(tmp_path):
"""Rant 2026-08-19T07:06:45 (host-finalized): the summary uses ONLY the
vibe check "done" field — NO fallback to the completion first line. Empty
stays empty (GUI renders "-"), never a machine/rough fallback."""
handler, captured = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Reviewed PR and posted LGTM",
"done": True, "delta": False, "session_id": "s"},
Expand All@@ -1686,19 +1687,18 @@ def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
])
asyncio.run(handler._run_evolution_cycle())
log = captured["log"]
assert log.summary == "Reviewed PR and posted LGTM", \
"missing done → completion first line fallback"
assert log.summary == "", \
"missing done → summary stays empty (no completion fallback)"
assert log.meaningful is True

# vibe check entirely unavailable → summary falls back to the first line
# of the completion content (per rant design), flags None/False
# vibe check entirely unavailable → summary stays empty, flags None/False
handler2, captured2 = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Done", "done": True,
"delta": False, "session_id": "s"},
])
asyncio.run(handler2._run_evolution_cycle())
log2 = captured2["log"]
assert log2.summary == "Done", "vibe unavailable → completion first line fallback"
assert log2.summary == "", "vibe unavailable → summary stays empty (no fallback)"
assert log2.meaningful is None
assert log2.recommend_slowdown is False
assert log2.tool_count == 0
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions emrg/gui/renderer/js/dialogs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,7 +701,8 @@ const Dialogs = (() => {

// rant 2026-08-18T21:32:32:任务卡点击 → 手风琴展开最近运行子表
// (时间 / 干了什么 / 降频)。数据来自 daemon handler.status().recent_runs
// (最多 5 条);旧数据无 summary → fallback impact 拼接;无记录 → 占位文案。
// (最多 5 条)。Rant 2026-08-19T07:06:45(宿主定稿):无 summary 不再
// fallback impact 机器串——空则显示 "-";无记录 → 占位文案。
function buildTaskRunDetail(t) {
const wrap = el("div", { class: "task-run-detail hidden" });
const runs = Array.isArray(t.recent_runs) ? t.recent_runs : [];
Expand All@@ -718,7 +719,6 @@ const Dialogs = (() => {
const row = el("div", { class: "task-run-row" });
row.appendChild(el("span", { class: "task-run-time" }, formatRelativeTime(r.timestamp)));
let done = (typeof r.summary === "string" && r.summary) ? r.summary : "";
if (!done && Array.isArray(r.impact) && r.impact.length) done = r.impact.join(", ");
row.appendChild(el("span", { class: "task-run-done" }, done || "-"));
const flagCell = el("span", { class: "task-run-flag" });
if (r.recommend_slowdown) {
Expand Down
4 changes: 2 additions & 2 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2688,9 +2688,9 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什
assert.ok(warnCount >= 1, "recommend_slowdown=true 应显示建议降频徽章");
const idleCount = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-badge-idle").length`, ctx);
assert.ok(idleCount >= 1, "meaningful=false 应显示空转徽章");
// 旧数据无 summary → fallback impact 拼接(第二行 run
// summary → 显示 "-"(rant 2026-08-19T07:06:45 宿主定稿:不再 fallback impact 机器串
const doneTexts = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")).map((n) => n.textContent)`, ctx);
assert.ok(doneTexts[1].includes("cycle-ts-complete"), `无 summary 应 fallback impact,实际: ${doneTexts[1]}`);
assert.strictEqual(doneTexts[1], "-", `无 summary 应显示 "-",实际: ${doneTexts[1]}`);
// 再次点击 → 折叠
await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx);
await tick();
Expand Down
35 changes: 15 additions & 20 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1160,32 +1160,27 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary
what meaningful work was done this cycle, for humans to read in the
GUI task recent-runs table. Old models / old parsing omit it → "".

Rant 2026-08-19T07:10:40 (root cause): the done frame used to carry an
empty ``content``, so ``completion_summary`` here was empty and the
memoryless LLM could not judge what happened. The done frame now
carries the agent's full final reply (daemon.py done broadcast), so
this prompt is evidence-driven: judge from the real final reply, not
from an empty shell. System + user messages live in
``prompts/vibe_check.j2`` (same live-reload mechanism as system.j2).

Raises on any failure (caller sends ``ok: false``); the scheduler
conservatively leaves its empty-cycle counter unchanged then.
"""
system = (
"你是 EMRG 定时任务调度助手。刚完成一次定时任务「" + (task_name or "") + "」,"
"任务要求与最终回复摘要如下。\n"
"请用 JSON 严格回答(不要任何其他文字),格式:\n"
'{"meaningful": true|false, "recommend_slowdown": true|false, '
'"reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}\n'
"- meaningful:这轮是否对项目产生了有意义的价值(产出/提交/分析/决策/"
"维护动作都算;纯空转/无可做=NTE 算 false)\n"
"- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token"
"(true=建议降低检查频率)\n"
"- reason:简短中文原因(给降频判断用)\n"
"- done:这次干了哪些有意义有价值的事(自然语言列举,如:修了 XX bug / "
"加了 XX 功能 / 分析了 XX;没有则空字符串)"
)
user = (
"任务名称:" + (task_name or "") + "\n"
"任务要求:" + (prompt or "")[:2000] + "\n"
"任务最终回复摘要:" + (completion_summary or "")[:3000]
template = _get_jinja_env().get_template("vibe_check.j2")
system = template.render(
task_name=task_name or "",
prompt=(prompt or "")[:2000],
completion_summary=(completion_summary or "")[:3000],
)
msg = await self.llm.chat(
[
{"role": "system", "content": system},
{"role": "user", "content": user},
{"role": "user", "content": "请基于以上任务信息,严格按 system 中要求的 JSON 格式回答。"},
],
tools=[],
)
Expand DownExpand Up@@ -2411,7 +2406,7 @@ async def _run_tool_loop(

await self._broadcast(session.session_id, {
"request_id": req.id,
"content": "",
"content": full_content or "",
"done": True,
"delta": False,
"session_id": session.session_id,
Expand Down
27 changes: 27 additions & 0 deletions emrg/server/prompts/vibe_check.j2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
{# Vibe check prompt (rant 2026-08-19T07:10:40) — evidence-driven, one-shot.
Rendered by daemon._task_vibe_check via _get_jinja_env() (FileSystemLoader,
same live-reload mechanism as system.j2: editing this template takes effect
without a daemon restart). Context: task_name, prompt, completion_summary. #}
你是 EMRG 定时任务调度助手。刚完成一次定时任务「{{ task_name }}」,以下是该任务的
实际信息(任务要求 + Agent 最终回复摘要)。请基于这些**真实输入**做总结与判断:
只有信息显示确实没做任何事(无回复、无动作)才算空转;不要因为"没有 commit/PR"就
武断判为无产出——工具执行、分析、排查、写 memory、决策等具体动作都是实际工作。

请用 JSON 严格回答(不要任何其他文字),格式:
{"meaningful": true|false, "recommend_slowdown": true|false, "reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}

字段要求:
- meaningful:这轮是否对项目产生了有意义的价值。只要任务最终回复/工作内容显示执行了
具体动作(工具调用、分析、排查、写 memory、决策、产出等)即为 true;只有确实
空转(无回复、无任何动作)才为 false。
- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token
(true=建议降低检查频率)。
- reason:简短中文原因(给降频判断用),引用实际信息说明依据。
- done:从任务最终回复/工作内容中**提炼**这次具体做了什么(工具动作、产出、分析,
自然语言列举,如:执行了 N 次工具调用,git fetch 检查了 X、分析了 Y、写了
memory Z)。**只要回复/工作内容有实质内容就必须写具体动作**;回复确实为空、
纯空转才写空字符串 ""。

任务名称:{{ task_name }}
任务要求:{{ prompt }}
任务最终回复摘要:{{ completion_summary }}
18 changes: 6 additions & 12 deletions emrg/server/scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -779,13 +779,9 @@ def status(self) -> dict:
if self.evolutions:
last = self.evolutions[-1]
last_run_at = last.timestamp
# brief summary: agent's natural-language "done" summary preferred
# (rant 2026-08-18T21:32:32), fallback to machine impact tags.
if last.summary:
last_cycle_summary = last.summary
else:
parts = [str(i) for i in last.impact if i]
last_cycle_summary = ", ".join(parts[:3]) if parts else None
# rant 2026-08-19T07:06:45 (host-finalized): NO machine impact
# fallback — empty summary shows as None (GUI renders "-").
last_cycle_summary = last.summary if last.summary else None
# rant 2026-08-18T21:32:32: last 5 run records for the GUI accordion
# subtable — {timestamp, summary, impact, meaningful,
# recommend_slowdown, tool_count}; all in-memory, no extra I/O.
Expand DownExpand Up@@ -1120,18 +1116,16 @@ async def _run_evolution_cycle(self) -> None:
# rant 2026-08-18T21:32:32: persist the agent's own summary of what
# meaningful work was done (vibe check "done" field) + the vibe flags,
# so the GUI task recent-runs table shows real value, not a machine
# string. Fallbacks: vibe unavailable → None flags + first line of the
# completion summary as a rough summary (never crash).
# string. Rant 2026-08-19T07:06:45 (host-finalized): NO fallback to the
# completion first line — summary uses only the vibe check "done"
# field; empty stays empty (GUI shows "-"), never a machine fallback.
summary = ""
meaningful = None
recommend = False
if vibe_result is not None:
summary = str(vibe_result.get("done") or "")[:500]
meaningful = vibe_result.get("meaningful")
recommend = bool(vibe_result.get("recommend_slowdown"))
if not summary and completion_content:
first = completion_content.strip().splitlines()[0] if completion_content.strip() else ""
summary = first[:500]

log = EvolutionLog(
timestamp=cycle_ts,
Expand Down
20 changes: 10 additions & 10 deletions tests/test_scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -518,7 +518,8 @@ def test_evolution_handler_status_last_run_fields():
handler._empty_cycles = 3
st = handler.status()
assert st["last_run_at"] == "2026-08-18T10:00:00"
assert st["last_cycle_summary"] == "tools-executed=24, cycle-complete"
# rant 2026-08-19T07:06:45: empty summary → None (no machine impact fallback)
assert st["last_cycle_summary"] is None
assert st["saturation"]["empty_cycles"] == 3
assert len(st["recent_runs"]) == 1
r0 = st["recent_runs"][0]
Expand DownExpand Up@@ -1673,10 +1674,10 @@ def test_evolution_cycle_complete_agent_says_meaningful_resets_streak(tmp_path):
assert log.tool_count == 0


def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
"""Vibe check ok but missing 'done' → log.summary falls back to the first
line of the completion content; vibe unavailable → empty summary (never
crash). Rant 2026-08-18T21:32:32."""
def test_evolution_cycle_log_summary_no_completion_fallback(tmp_path):
"""Rant 2026-08-19T07:06:45 (host-finalized): the summary uses ONLY the
vibe check "done" field — NO fallback to the completion first line. Empty
stays empty (GUI renders "-"), never a machine/rough fallback."""
handler, captured = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Reviewed PR and posted LGTM",
"done": True, "delta": False, "session_id": "s"},
Expand All@@ -1686,19 +1687,18 @@ def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
])
asyncio.run(handler._run_evolution_cycle())
log = captured["log"]
assert log.summary == "Reviewed PR and posted LGTM", \
"missing done → completion first line fallback"
assert log.summary == "", \
"missing done → summary stays empty (no completion fallback)"
assert log.meaningful is True

# vibe check entirely unavailable → summary falls back to the first line
# of the completion content (per rant design), flags None/False
# vibe check entirely unavailable → summary stays empty, flags None/False
handler2, captured2 = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Done", "done": True,
"delta": False, "session_id": "s"},
])
asyncio.run(handler2._run_evolution_cycle())
log2 = captured2["log"]
assert log2.summary == "Done", "vibe unavailable → completion first line fallback"
assert log2.summary == "", "vibe unavailable → summary stays empty (no fallback)"
assert log2.meaningful is None
assert log2.recommend_slowdown is False
assert log2.tool_count == 0
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions emrg/gui/renderer/js/dialogs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,7 +701,8 @@ const Dialogs = (() => {

// rant 2026-08-18T21:32:32:任务卡点击 → 手风琴展开最近运行子表
// (时间 / 干了什么 / 降频)。数据来自 daemon handler.status().recent_runs
// (最多 5 条);旧数据无 summary → fallback impact 拼接;无记录 → 占位文案。
// (最多 5 条)。Rant 2026-08-19T07:06:45(宿主定稿):无 summary 不再
// fallback impact 机器串——空则显示 "-";无记录 → 占位文案。
function buildTaskRunDetail(t) {
const wrap = el("div", { class: "task-run-detail hidden" });
const runs = Array.isArray(t.recent_runs) ? t.recent_runs : [];
Expand All@@ -718,7 +719,6 @@ const Dialogs = (() => {
const row = el("div", { class: "task-run-row" });
row.appendChild(el("span", { class: "task-run-time" }, formatRelativeTime(r.timestamp)));
let done = (typeof r.summary === "string" && r.summary) ? r.summary : "";
if (!done && Array.isArray(r.impact) && r.impact.length) done = r.impact.join(", ");
row.appendChild(el("span", { class: "task-run-done" }, done || "-"));
const flagCell = el("span", { class: "task-run-flag" });
if (r.recommend_slowdown) {
Expand Down
4 changes: 2 additions & 2 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2688,9 +2688,9 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什
assert.ok(warnCount >= 1, "recommend_slowdown=true 应显示建议降频徽章");
const idleCount = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-badge-idle").length`, ctx);
assert.ok(idleCount >= 1, "meaningful=false 应显示空转徽章");
// 旧数据无 summary → fallback impact 拼接(第二行 run
// summary → 显示 "-"(rant 2026-08-19T07:06:45 宿主定稿:不再 fallback impact 机器串
const doneTexts = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")).map((n) => n.textContent)`, ctx);
assert.ok(doneTexts[1].includes("cycle-ts-complete"), `无 summary 应 fallback impact,实际: ${doneTexts[1]}`);
assert.strictEqual(doneTexts[1], "-", `无 summary 应显示 "-",实际: ${doneTexts[1]}`);
// 再次点击 → 折叠
await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx);
await tick();
Expand Down
35 changes: 15 additions & 20 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1160,32 +1160,27 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary
what meaningful work was done this cycle, for humans to read in the
GUI task recent-runs table. Old models / old parsing omit it → "".

Rant 2026-08-19T07:10:40 (root cause): the done frame used to carry an
empty ``content``, so ``completion_summary`` here was empty and the
memoryless LLM could not judge what happened. The done frame now
carries the agent's full final reply (daemon.py done broadcast), so
this prompt is evidence-driven: judge from the real final reply, not
from an empty shell. System + user messages live in
``prompts/vibe_check.j2`` (same live-reload mechanism as system.j2).

Raises on any failure (caller sends ``ok: false``); the scheduler
conservatively leaves its empty-cycle counter unchanged then.
"""
system = (
"你是 EMRG 定时任务调度助手。刚完成一次定时任务「" + (task_name or "") + "」,"
"任务要求与最终回复摘要如下。\n"
"请用 JSON 严格回答(不要任何其他文字),格式:\n"
'{"meaningful": true|false, "recommend_slowdown": true|false, '
'"reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}\n'
"- meaningful:这轮是否对项目产生了有意义的价值(产出/提交/分析/决策/"
"维护动作都算;纯空转/无可做=NTE 算 false)\n"
"- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token"
"(true=建议降低检查频率)\n"
"- reason:简短中文原因(给降频判断用)\n"
"- done:这次干了哪些有意义有价值的事(自然语言列举,如:修了 XX bug / "
"加了 XX 功能 / 分析了 XX;没有则空字符串)"
)
user = (
"任务名称:" + (task_name or "") + "\n"
"任务要求:" + (prompt or "")[:2000] + "\n"
"任务最终回复摘要:" + (completion_summary or "")[:3000]
template = _get_jinja_env().get_template("vibe_check.j2")
system = template.render(
task_name=task_name or "",
prompt=(prompt or "")[:2000],
completion_summary=(completion_summary or "")[:3000],
)
msg = await self.llm.chat(
[
{"role": "system", "content": system},
{"role": "user", "content": user},
{"role": "user", "content": "请基于以上任务信息,严格按 system 中要求的 JSON 格式回答。"},
],
tools=[],
)
Expand DownExpand Up@@ -2411,7 +2406,7 @@ async def _run_tool_loop(

await self._broadcast(session.session_id, {
"request_id": req.id,
"content": "",
"content": full_content or "",
"done": True,
"delta": False,
"session_id": session.session_id,
Expand Down
27 changes: 27 additions & 0 deletions emrg/server/prompts/vibe_check.j2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
{# Vibe check prompt (rant 2026-08-19T07:10:40) — evidence-driven, one-shot.
Rendered by daemon._task_vibe_check via _get_jinja_env() (FileSystemLoader,
same live-reload mechanism as system.j2: editing this template takes effect
without a daemon restart). Context: task_name, prompt, completion_summary. #}
你是 EMRG 定时任务调度助手。刚完成一次定时任务「{{ task_name }}」,以下是该任务的
实际信息(任务要求 + Agent 最终回复摘要)。请基于这些**真实输入**做总结与判断:
只有信息显示确实没做任何事(无回复、无动作)才算空转;不要因为"没有 commit/PR"就
武断判为无产出——工具执行、分析、排查、写 memory、决策等具体动作都是实际工作。

请用 JSON 严格回答(不要任何其他文字),格式:
{"meaningful": true|false, "recommend_slowdown": true|false, "reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}

字段要求:
- meaningful:这轮是否对项目产生了有意义的价值。只要任务最终回复/工作内容显示执行了
具体动作(工具调用、分析、排查、写 memory、决策、产出等)即为 true;只有确实
空转(无回复、无任何动作)才为 false。
- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token
(true=建议降低检查频率)。
- reason:简短中文原因(给降频判断用),引用实际信息说明依据。
- done:从任务最终回复/工作内容中**提炼**这次具体做了什么(工具动作、产出、分析,
自然语言列举,如:执行了 N 次工具调用,git fetch 检查了 X、分析了 Y、写了
memory Z)。**只要回复/工作内容有实质内容就必须写具体动作**;回复确实为空、
纯空转才写空字符串 ""。

任务名称:{{ task_name }}
任务要求:{{ prompt }}
任务最终回复摘要:{{ completion_summary }}
18 changes: 6 additions & 12 deletions emrg/server/scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -779,13 +779,9 @@ def status(self) -> dict:
if self.evolutions:
last = self.evolutions[-1]
last_run_at = last.timestamp
# brief summary: agent's natural-language "done" summary preferred
# (rant 2026-08-18T21:32:32), fallback to machine impact tags.
if last.summary:
last_cycle_summary = last.summary
else:
parts = [str(i) for i in last.impact if i]
last_cycle_summary = ", ".join(parts[:3]) if parts else None
# rant 2026-08-19T07:06:45 (host-finalized): NO machine impact
# fallback — empty summary shows as None (GUI renders "-").
last_cycle_summary = last.summary if last.summary else None
# rant 2026-08-18T21:32:32: last 5 run records for the GUI accordion
# subtable — {timestamp, summary, impact, meaningful,
# recommend_slowdown, tool_count}; all in-memory, no extra I/O.
Expand DownExpand Up@@ -1120,18 +1116,16 @@ async def _run_evolution_cycle(self) -> None:
# rant 2026-08-18T21:32:32: persist the agent's own summary of what
# meaningful work was done (vibe check "done" field) + the vibe flags,
# so the GUI task recent-runs table shows real value, not a machine
# string. Fallbacks: vibe unavailable → None flags + first line of the
# completion summary as a rough summary (never crash).
# string. Rant 2026-08-19T07:06:45 (host-finalized): NO fallback to the
# completion first line — summary uses only the vibe check "done"
# field; empty stays empty (GUI shows "-"), never a machine fallback.
summary = ""
meaningful = None
recommend = False
if vibe_result is not None:
summary = str(vibe_result.get("done") or "")[:500]
meaningful = vibe_result.get("meaningful")
recommend = bool(vibe_result.get("recommend_slowdown"))
if not summary and completion_content:
first = completion_content.strip().splitlines()[0] if completion_content.strip() else ""
summary = first[:500]

log = EvolutionLog(
timestamp=cycle_ts,
Expand Down
20 changes: 10 additions & 10 deletions tests/test_scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -518,7 +518,8 @@ def test_evolution_handler_status_last_run_fields():
handler._empty_cycles = 3
st = handler.status()
assert st["last_run_at"] == "2026-08-18T10:00:00"
assert st["last_cycle_summary"] == "tools-executed=24, cycle-complete"
# rant 2026-08-19T07:06:45: empty summary → None (no machine impact fallback)
assert st["last_cycle_summary"] is None
assert st["saturation"]["empty_cycles"] == 3
assert len(st["recent_runs"]) == 1
r0 = st["recent_runs"][0]
Expand DownExpand Up@@ -1673,10 +1674,10 @@ def test_evolution_cycle_complete_agent_says_meaningful_resets_streak(tmp_path):
assert log.tool_count == 0


def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
"""Vibe check ok but missing 'done' → log.summary falls back to the first
line of the completion content; vibe unavailable → empty summary (never
crash). Rant 2026-08-18T21:32:32."""
def test_evolution_cycle_log_summary_no_completion_fallback(tmp_path):
"""Rant 2026-08-19T07:06:45 (host-finalized): the summary uses ONLY the
vibe check "done" field — NO fallback to the completion first line. Empty
stays empty (GUI renders "-"), never a machine/rough fallback."""
handler, captured = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Reviewed PR and posted LGTM",
"done": True, "delta": False, "session_id": "s"},
Expand All@@ -1686,19 +1687,18 @@ def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
])
asyncio.run(handler._run_evolution_cycle())
log = captured["log"]
assert log.summary == "Reviewed PR and posted LGTM", \
"missing done → completion first line fallback"
assert log.summary == "", \
"missing done → summary stays empty (no completion fallback)"
assert log.meaningful is True

# vibe check entirely unavailable → summary falls back to the first line
# of the completion content (per rant design), flags None/False
# vibe check entirely unavailable → summary stays empty, flags None/False
handler2, captured2 = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Done", "done": True,
"delta": False, "session_id": "s"},
])
asyncio.run(handler2._run_evolution_cycle())
log2 = captured2["log"]
assert log2.summary == "Done", "vibe unavailable → completion first line fallback"
assert log2.summary == "", "vibe unavailable → summary stays empty (no fallback)"
assert log2.meaningful is None
assert log2.recommend_slowdown is False
assert log2.tool_count == 0
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions emrg/gui/renderer/js/dialogs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,7 +701,8 @@ const Dialogs = (() => {

// rant 2026-08-18T21:32:32:任务卡点击 → 手风琴展开最近运行子表
// (时间 / 干了什么 / 降频)。数据来自 daemon handler.status().recent_runs
// (最多 5 条);旧数据无 summary → fallback impact 拼接;无记录 → 占位文案。
// (最多 5 条)。Rant 2026-08-19T07:06:45(宿主定稿):无 summary 不再
// fallback impact 机器串——空则显示 "-";无记录 → 占位文案。
function buildTaskRunDetail(t) {
const wrap = el("div", { class: "task-run-detail hidden" });
const runs = Array.isArray(t.recent_runs) ? t.recent_runs : [];
Expand All@@ -718,7 +719,6 @@ const Dialogs = (() => {
const row = el("div", { class: "task-run-row" });
row.appendChild(el("span", { class: "task-run-time" }, formatRelativeTime(r.timestamp)));
let done = (typeof r.summary === "string" && r.summary) ? r.summary : "";
if (!done && Array.isArray(r.impact) && r.impact.length) done = r.impact.join(", ");
row.appendChild(el("span", { class: "task-run-done" }, done || "-"));
const flagCell = el("span", { class: "task-run-flag" });
if (r.recommend_slowdown) {
Expand Down
4 changes: 2 additions & 2 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2688,9 +2688,9 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什
assert.ok(warnCount >= 1, "recommend_slowdown=true 应显示建议降频徽章");
const idleCount = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-badge-idle").length`, ctx);
assert.ok(idleCount >= 1, "meaningful=false 应显示空转徽章");
// 旧数据无 summary → fallback impact 拼接(第二行 run
// summary → 显示 "-"(rant 2026-08-19T07:06:45 宿主定稿:不再 fallback impact 机器串
const doneTexts = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")).map((n) => n.textContent)`, ctx);
assert.ok(doneTexts[1].includes("cycle-ts-complete"), `无 summary 应 fallback impact,实际: ${doneTexts[1]}`);
assert.strictEqual(doneTexts[1], "-", `无 summary 应显示 "-",实际: ${doneTexts[1]}`);
// 再次点击 → 折叠
await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx);
await tick();
Expand Down
35 changes: 15 additions & 20 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1160,32 +1160,27 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary
what meaningful work was done this cycle, for humans to read in the
GUI task recent-runs table. Old models / old parsing omit it → "".

Rant 2026-08-19T07:10:40 (root cause): the done frame used to carry an
empty ``content``, so ``completion_summary`` here was empty and the
memoryless LLM could not judge what happened. The done frame now
carries the agent's full final reply (daemon.py done broadcast), so
this prompt is evidence-driven: judge from the real final reply, not
from an empty shell. System + user messages live in
``prompts/vibe_check.j2`` (same live-reload mechanism as system.j2).

Raises on any failure (caller sends ``ok: false``); the scheduler
conservatively leaves its empty-cycle counter unchanged then.
"""
system = (
"你是 EMRG 定时任务调度助手。刚完成一次定时任务「" + (task_name or "") + "」,"
"任务要求与最终回复摘要如下。\n"
"请用 JSON 严格回答(不要任何其他文字),格式:\n"
'{"meaningful": true|false, "recommend_slowdown": true|false, '
'"reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}\n'
"- meaningful:这轮是否对项目产生了有意义的价值(产出/提交/分析/决策/"
"维护动作都算;纯空转/无可做=NTE 算 false)\n"
"- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token"
"(true=建议降低检查频率)\n"
"- reason:简短中文原因(给降频判断用)\n"
"- done:这次干了哪些有意义有价值的事(自然语言列举,如:修了 XX bug / "
"加了 XX 功能 / 分析了 XX;没有则空字符串)"
)
user = (
"任务名称:" + (task_name or "") + "\n"
"任务要求:" + (prompt or "")[:2000] + "\n"
"任务最终回复摘要:" + (completion_summary or "")[:3000]
template = _get_jinja_env().get_template("vibe_check.j2")
system = template.render(
task_name=task_name or "",
prompt=(prompt or "")[:2000],
completion_summary=(completion_summary or "")[:3000],
)
msg = await self.llm.chat(
[
{"role": "system", "content": system},
{"role": "user", "content": user},
{"role": "user", "content": "请基于以上任务信息,严格按 system 中要求的 JSON 格式回答。"},
],
tools=[],
)
Expand DownExpand Up@@ -2411,7 +2406,7 @@ async def _run_tool_loop(

await self._broadcast(session.session_id, {
"request_id": req.id,
"content": "",
"content": full_content or "",
"done": True,
"delta": False,
"session_id": session.session_id,
Expand Down
27 changes: 27 additions & 0 deletions emrg/server/prompts/vibe_check.j2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
{# Vibe check prompt (rant 2026-08-19T07:10:40) — evidence-driven, one-shot.
Rendered by daemon._task_vibe_check via _get_jinja_env() (FileSystemLoader,
same live-reload mechanism as system.j2: editing this template takes effect
without a daemon restart). Context: task_name, prompt, completion_summary. #}
你是 EMRG 定时任务调度助手。刚完成一次定时任务「{{ task_name }}」,以下是该任务的
实际信息(任务要求 + Agent 最终回复摘要)。请基于这些**真实输入**做总结与判断:
只有信息显示确实没做任何事(无回复、无动作)才算空转;不要因为"没有 commit/PR"就
武断判为无产出——工具执行、分析、排查、写 memory、决策等具体动作都是实际工作。

请用 JSON 严格回答(不要任何其他文字),格式:
{"meaningful": true|false, "recommend_slowdown": true|false, "reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}

字段要求:
- meaningful:这轮是否对项目产生了有意义的价值。只要任务最终回复/工作内容显示执行了
具体动作(工具调用、分析、排查、写 memory、决策、产出等)即为 true;只有确实
空转(无回复、无任何动作)才为 false。
- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token
(true=建议降低检查频率)。
- reason:简短中文原因(给降频判断用),引用实际信息说明依据。
- done:从任务最终回复/工作内容中**提炼**这次具体做了什么(工具动作、产出、分析,
自然语言列举,如:执行了 N 次工具调用,git fetch 检查了 X、分析了 Y、写了
memory Z)。**只要回复/工作内容有实质内容就必须写具体动作**;回复确实为空、
纯空转才写空字符串 ""。

任务名称:{{ task_name }}
任务要求:{{ prompt }}
任务最终回复摘要:{{ completion_summary }}
18 changes: 6 additions & 12 deletions emrg/server/scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -779,13 +779,9 @@ def status(self) -> dict:
if self.evolutions:
last = self.evolutions[-1]
last_run_at = last.timestamp
# brief summary: agent's natural-language "done" summary preferred
# (rant 2026-08-18T21:32:32), fallback to machine impact tags.
if last.summary:
last_cycle_summary = last.summary
else:
parts = [str(i) for i in last.impact if i]
last_cycle_summary = ", ".join(parts[:3]) if parts else None
# rant 2026-08-19T07:06:45 (host-finalized): NO machine impact
# fallback — empty summary shows as None (GUI renders "-").
last_cycle_summary = last.summary if last.summary else None
# rant 2026-08-18T21:32:32: last 5 run records for the GUI accordion
# subtable — {timestamp, summary, impact, meaningful,
# recommend_slowdown, tool_count}; all in-memory, no extra I/O.
Expand DownExpand Up@@ -1120,18 +1116,16 @@ async def _run_evolution_cycle(self) -> None:
# rant 2026-08-18T21:32:32: persist the agent's own summary of what
# meaningful work was done (vibe check "done" field) + the vibe flags,
# so the GUI task recent-runs table shows real value, not a machine
# string. Fallbacks: vibe unavailable → None flags + first line of the
# completion summary as a rough summary (never crash).
# string. Rant 2026-08-19T07:06:45 (host-finalized): NO fallback to the
# completion first line — summary uses only the vibe check "done"
# field; empty stays empty (GUI shows "-"), never a machine fallback.
summary = ""
meaningful = None
recommend = False
if vibe_result is not None:
summary = str(vibe_result.get("done") or "")[:500]
meaningful = vibe_result.get("meaningful")
recommend = bool(vibe_result.get("recommend_slowdown"))
if not summary and completion_content:
first = completion_content.strip().splitlines()[0] if completion_content.strip() else ""
summary = first[:500]

log = EvolutionLog(
timestamp=cycle_ts,
Expand Down
20 changes: 10 additions & 10 deletions tests/test_scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -518,7 +518,8 @@ def test_evolution_handler_status_last_run_fields():
handler._empty_cycles = 3
st = handler.status()
assert st["last_run_at"] == "2026-08-18T10:00:00"
assert st["last_cycle_summary"] == "tools-executed=24, cycle-complete"
# rant 2026-08-19T07:06:45: empty summary → None (no machine impact fallback)
assert st["last_cycle_summary"] is None
assert st["saturation"]["empty_cycles"] == 3
assert len(st["recent_runs"]) == 1
r0 = st["recent_runs"][0]
Expand DownExpand Up@@ -1673,10 +1674,10 @@ def test_evolution_cycle_complete_agent_says_meaningful_resets_streak(tmp_path):
assert log.tool_count == 0


def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
"""Vibe check ok but missing 'done' → log.summary falls back to the first
line of the completion content; vibe unavailable → empty summary (never
crash). Rant 2026-08-18T21:32:32."""
def test_evolution_cycle_log_summary_no_completion_fallback(tmp_path):
"""Rant 2026-08-19T07:06:45 (host-finalized): the summary uses ONLY the
vibe check "done" field — NO fallback to the completion first line. Empty
stays empty (GUI renders "-"), never a machine/rough fallback."""
handler, captured = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Reviewed PR and posted LGTM",
"done": True, "delta": False, "session_id": "s"},
Expand All@@ -1686,19 +1687,18 @@ def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
])
asyncio.run(handler._run_evolution_cycle())
log = captured["log"]
assert log.summary == "Reviewed PR and posted LGTM", \
"missing done → completion first line fallback"
assert log.summary == "", \
"missing done → summary stays empty (no completion fallback)"
assert log.meaningful is True

# vibe check entirely unavailable → summary falls back to the first line
# of the completion content (per rant design), flags None/False
# vibe check entirely unavailable → summary stays empty, flags None/False
handler2, captured2 = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Done", "done": True,
"delta": False, "session_id": "s"},
])
asyncio.run(handler2._run_evolution_cycle())
log2 = captured2["log"]
assert log2.summary == "Done", "vibe unavailable → completion first line fallback"
assert log2.summary == "", "vibe unavailable → summary stays empty (no fallback)"
assert log2.meaningful is None
assert log2.recommend_slowdown is False
assert log2.tool_count == 0
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions emrg/gui/renderer/js/dialogs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,7 +701,8 @@ const Dialogs = (() => {

// rant 2026-08-18T21:32:32:任务卡点击 → 手风琴展开最近运行子表
// (时间 / 干了什么 / 降频)。数据来自 daemon handler.status().recent_runs
// (最多 5 条);旧数据无 summary → fallback impact 拼接;无记录 → 占位文案。
// (最多 5 条)。Rant 2026-08-19T07:06:45(宿主定稿):无 summary 不再
// fallback impact 机器串——空则显示 "-";无记录 → 占位文案。
function buildTaskRunDetail(t) {
const wrap = el("div", { class: "task-run-detail hidden" });
const runs = Array.isArray(t.recent_runs) ? t.recent_runs : [];
Expand All@@ -718,7 +719,6 @@ const Dialogs = (() => {
const row = el("div", { class: "task-run-row" });
row.appendChild(el("span", { class: "task-run-time" }, formatRelativeTime(r.timestamp)));
let done = (typeof r.summary === "string" && r.summary) ? r.summary : "";
if (!done && Array.isArray(r.impact) && r.impact.length) done = r.impact.join(", ");
row.appendChild(el("span", { class: "task-run-done" }, done || "-"));
const flagCell = el("span", { class: "task-run-flag" });
if (r.recommend_slowdown) {
Expand Down
4 changes: 2 additions & 2 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2688,9 +2688,9 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什
assert.ok(warnCount >= 1, "recommend_slowdown=true 应显示建议降频徽章");
const idleCount = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-badge-idle").length`, ctx);
assert.ok(idleCount >= 1, "meaningful=false 应显示空转徽章");
// 旧数据无 summary → fallback impact 拼接(第二行 run
// summary → 显示 "-"(rant 2026-08-19T07:06:45 宿主定稿:不再 fallback impact 机器串
const doneTexts = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")).map((n) => n.textContent)`, ctx);
assert.ok(doneTexts[1].includes("cycle-ts-complete"), `无 summary 应 fallback impact,实际: ${doneTexts[1]}`);
assert.strictEqual(doneTexts[1], "-", `无 summary 应显示 "-",实际: ${doneTexts[1]}`);
// 再次点击 → 折叠
await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx);
await tick();
Expand Down
35 changes: 15 additions & 20 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1160,32 +1160,27 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary
what meaningful work was done this cycle, for humans to read in the
GUI task recent-runs table. Old models / old parsing omit it → "".

Rant 2026-08-19T07:10:40 (root cause): the done frame used to carry an
empty ``content``, so ``completion_summary`` here was empty and the
memoryless LLM could not judge what happened. The done frame now
carries the agent's full final reply (daemon.py done broadcast), so
this prompt is evidence-driven: judge from the real final reply, not
from an empty shell. System + user messages live in
``prompts/vibe_check.j2`` (same live-reload mechanism as system.j2).

Raises on any failure (caller sends ``ok: false``); the scheduler
conservatively leaves its empty-cycle counter unchanged then.
"""
system = (
"你是 EMRG 定时任务调度助手。刚完成一次定时任务「" + (task_name or "") + "」,"
"任务要求与最终回复摘要如下。\n"
"请用 JSON 严格回答(不要任何其他文字),格式:\n"
'{"meaningful": true|false, "recommend_slowdown": true|false, '
'"reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}\n'
"- meaningful:这轮是否对项目产生了有意义的价值(产出/提交/分析/决策/"
"维护动作都算;纯空转/无可做=NTE 算 false)\n"
"- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token"
"(true=建议降低检查频率)\n"
"- reason:简短中文原因(给降频判断用)\n"
"- done:这次干了哪些有意义有价值的事(自然语言列举,如:修了 XX bug / "
"加了 XX 功能 / 分析了 XX;没有则空字符串)"
)
user = (
"任务名称:" + (task_name or "") + "\n"
"任务要求:" + (prompt or "")[:2000] + "\n"
"任务最终回复摘要:" + (completion_summary or "")[:3000]
template = _get_jinja_env().get_template("vibe_check.j2")
system = template.render(
task_name=task_name or "",
prompt=(prompt or "")[:2000],
completion_summary=(completion_summary or "")[:3000],
)
msg = await self.llm.chat(
[
{"role": "system", "content": system},
{"role": "user", "content": user},
{"role": "user", "content": "请基于以上任务信息,严格按 system 中要求的 JSON 格式回答。"},
],
tools=[],
)
Expand DownExpand Up@@ -2411,7 +2406,7 @@ async def _run_tool_loop(

await self._broadcast(session.session_id, {
"request_id": req.id,
"content": "",
"content": full_content or "",
"done": True,
"delta": False,
"session_id": session.session_id,
Expand Down
27 changes: 27 additions & 0 deletions emrg/server/prompts/vibe_check.j2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
{# Vibe check prompt (rant 2026-08-19T07:10:40) — evidence-driven, one-shot.
Rendered by daemon._task_vibe_check via _get_jinja_env() (FileSystemLoader,
same live-reload mechanism as system.j2: editing this template takes effect
without a daemon restart). Context: task_name, prompt, completion_summary. #}
你是 EMRG 定时任务调度助手。刚完成一次定时任务「{{ task_name }}」,以下是该任务的
实际信息(任务要求 + Agent 最终回复摘要)。请基于这些**真实输入**做总结与判断:
只有信息显示确实没做任何事(无回复、无动作)才算空转;不要因为"没有 commit/PR"就
武断判为无产出——工具执行、分析、排查、写 memory、决策等具体动作都是实际工作。

请用 JSON 严格回答(不要任何其他文字),格式:
{"meaningful": true|false, "recommend_slowdown": true|false, "reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}

字段要求:
- meaningful:这轮是否对项目产生了有意义的价值。只要任务最终回复/工作内容显示执行了
具体动作(工具调用、分析、排查、写 memory、决策、产出等)即为 true;只有确实
空转(无回复、无任何动作)才为 false。
- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token
(true=建议降低检查频率)。
- reason:简短中文原因(给降频判断用),引用实际信息说明依据。
- done:从任务最终回复/工作内容中**提炼**这次具体做了什么(工具动作、产出、分析,
自然语言列举,如:执行了 N 次工具调用,git fetch 检查了 X、分析了 Y、写了
memory Z)。**只要回复/工作内容有实质内容就必须写具体动作**;回复确实为空、
纯空转才写空字符串 ""。

任务名称:{{ task_name }}
任务要求:{{ prompt }}
任务最终回复摘要:{{ completion_summary }}
18 changes: 6 additions & 12 deletions emrg/server/scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -779,13 +779,9 @@ def status(self) -> dict:
if self.evolutions:
last = self.evolutions[-1]
last_run_at = last.timestamp
# brief summary: agent's natural-language "done" summary preferred
# (rant 2026-08-18T21:32:32), fallback to machine impact tags.
if last.summary:
last_cycle_summary = last.summary
else:
parts = [str(i) for i in last.impact if i]
last_cycle_summary = ", ".join(parts[:3]) if parts else None
# rant 2026-08-19T07:06:45 (host-finalized): NO machine impact
# fallback — empty summary shows as None (GUI renders "-").
last_cycle_summary = last.summary if last.summary else None
# rant 2026-08-18T21:32:32: last 5 run records for the GUI accordion
# subtable — {timestamp, summary, impact, meaningful,
# recommend_slowdown, tool_count}; all in-memory, no extra I/O.
Expand DownExpand Up@@ -1120,18 +1116,16 @@ async def _run_evolution_cycle(self) -> None:
# rant 2026-08-18T21:32:32: persist the agent's own summary of what
# meaningful work was done (vibe check "done" field) + the vibe flags,
# so the GUI task recent-runs table shows real value, not a machine
# string. Fallbacks: vibe unavailable → None flags + first line of the
# completion summary as a rough summary (never crash).
# string. Rant 2026-08-19T07:06:45 (host-finalized): NO fallback to the
# completion first line — summary uses only the vibe check "done"
# field; empty stays empty (GUI shows "-"), never a machine fallback.
summary = ""
meaningful = None
recommend = False
if vibe_result is not None:
summary = str(vibe_result.get("done") or "")[:500]
meaningful = vibe_result.get("meaningful")
recommend = bool(vibe_result.get("recommend_slowdown"))
if not summary and completion_content:
first = completion_content.strip().splitlines()[0] if completion_content.strip() else ""
summary = first[:500]

log = EvolutionLog(
timestamp=cycle_ts,
Expand Down
20 changes: 10 additions & 10 deletions tests/test_scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -518,7 +518,8 @@ def test_evolution_handler_status_last_run_fields():
handler._empty_cycles = 3
st = handler.status()
assert st["last_run_at"] == "2026-08-18T10:00:00"
assert st["last_cycle_summary"] == "tools-executed=24, cycle-complete"
# rant 2026-08-19T07:06:45: empty summary → None (no machine impact fallback)
assert st["last_cycle_summary"] is None
assert st["saturation"]["empty_cycles"] == 3
assert len(st["recent_runs"]) == 1
r0 = st["recent_runs"][0]
Expand DownExpand Up@@ -1673,10 +1674,10 @@ def test_evolution_cycle_complete_agent_says_meaningful_resets_streak(tmp_path):
assert log.tool_count == 0


def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
"""Vibe check ok but missing 'done' → log.summary falls back to the first
line of the completion content; vibe unavailable → empty summary (never
crash). Rant 2026-08-18T21:32:32."""
def test_evolution_cycle_log_summary_no_completion_fallback(tmp_path):
"""Rant 2026-08-19T07:06:45 (host-finalized): the summary uses ONLY the
vibe check "done" field — NO fallback to the completion first line. Empty
stays empty (GUI renders "-"), never a machine/rough fallback."""
handler, captured = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Reviewed PR and posted LGTM",
"done": True, "delta": False, "session_id": "s"},
Expand All@@ -1686,19 +1687,18 @@ def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path):
])
asyncio.run(handler._run_evolution_cycle())
log = captured["log"]
assert log.summary == "Reviewed PR and posted LGTM", \
"missing done → completion first line fallback"
assert log.summary == "", \
"missing done → summary stays empty (no completion fallback)"
assert log.meaningful is True

# vibe check entirely unavailable → summary falls back to the first line
# of the completion content (per rant design), flags None/False
# vibe check entirely unavailable → summary stays empty, flags None/False
handler2, captured2 = _make_cycle_handler(tmp_path, frames=[
{"request_id": "r1", "content": "Done", "done": True,
"delta": False, "session_id": "s"},
])
asyncio.run(handler2._run_evolution_cycle())
log2 = captured2["log"]
assert log2.summary == "Done", "vibe unavailable → completion first line fallback"
assert log2.summary == "", "vibe unavailable → summary stays empty (no fallback)"
assert log2.meaningful is None
assert log2.recommend_slowdown is False
assert log2.tool_count == 0
Expand Down
Loading