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 Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,8 +118,8 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design:
pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (955) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (259: 45 daemon_client + 19 conn-manager + 22 app-commands + 130 renderer smoke + 16 i18n + 7 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
Python: `uv run pytest tests/ -v` (958) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (260: 45 daemon_client + 19 conn-manager + 22 app-commands + 131 renderer smoke + 16 i18n + 7 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)

Expand Down
59 changes: 59 additions & 0 deletions emrg/gui/renderer/css/components.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1024,6 +1024,65 @@ dialog::backdrop {
border-radius: 8px;
border: 1px solid var(--warn-soft, var(--border));
}
/* rant 2026-08-18T21:32:32:任务卡点击展开的最近运行子表(时间/干了什么/降频) */
.task-run-detail {
flex-basis: 100%;
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 4px;
padding-top: 4px;
border-top: 1px dashed var(--border);
min-width: 0;
}
.task-run-detail.hidden {
display: none;
}
.task-run-head,
.task-run-row {
display: grid;
grid-template-columns: 72px 1fr 92px;
gap: 6px;
font-size: var(--fs-aux);
min-width: 0;
align-items: center;
}
.task-run-head {
color: var(--text-3);
font-weight: 600;
}
.task-run-time {
color: var(--text-3);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.task-run-done {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-2);
}
.task-run-flag {
display: flex;
gap: 4px;
align-items: center;
min-width: 0;
}
.task-run-badge-warn {
background: var(--warn-soft, var(--bg-soft));
color: var(--warn, var(--text-3));
border-color: var(--warn-soft, var(--border));
}
.task-run-badge-idle {
color: var(--text-3);
}
.task-run-empty {
flex-basis: 100%;
font-size: var(--fs-aux);
color: var(--text-3);
padding: 4px 0;
}

/* rant 09:17:45:提示词编辑器 Monaco 挂载容器(长提示词 300px 起步) */
.monaco-host {
Expand Down
44 changes: 44 additions & 0 deletions emrg/gui/renderer/js/dialogs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -699,6 +699,39 @@ const Dialogs = (() => {
} catch { taskProjects = []; }
}

// rant 2026-08-18T21:32:32:任务卡点击 → 手风琴展开最近运行子表
// (时间 / 干了什么 / 降频)。数据来自 daemon handler.status().recent_runs
// (最多 5 条);旧数据无 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 : [];
if (runs.length === 0) {
wrap.appendChild(el("div", { class: "task-run-empty" }, _t("app.taskRunsEmpty")));
return wrap;
}
const head = el("div", { class: "task-run-head" });
head.appendChild(el("span", {}, _t("app.taskRunsColTime")));
head.appendChild(el("span", {}, _t("app.taskRunsColDone")));
head.appendChild(el("span", {}, _t("app.taskRunsColThrottle")));
wrap.appendChild(head);
for (const r of runs) {
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) {
flagCell.appendChild(el("span", { class: "task-badge task-run-badge-warn" }, _t("app.taskRunThrottle")));
} else if (r.meaningful === false) {
flagCell.appendChild(el("span", { class: "task-badge task-run-badge-idle" }, _t("app.taskRunIdle")));
}
row.appendChild(flagCell);
wrap.appendChild(row);
}
return wrap;
}

async function renderTaskList() {
const list = $("task-list");
if (!list) return; // 元素缺失(测试桩)时忽略
Expand DownExpand Up@@ -804,6 +837,17 @@ const Dialogs = (() => {
});
actions.appendChild(delBtn);
row.appendChild(actions);
// rant 2026-08-18T21:32:32:点击任务卡(非按钮)→ 手风琴展开最近运行子表。
// 真实 DOM 中按钮点击通过 e.target.closest("button") 拦截(不触发展开);
// 测试沙箱的 click() 无 target → 直接切换(沙箱中按钮点击不冒泡,互不影响)。
const runDetail = buildTaskRunDetail(t);
row.appendChild(runDetail);
let runDetailOpen = false;
row.addEventListener("click", (e) => {
if (e && e.target && typeof e.target.closest === "function" && e.target.closest("button")) return;
runDetailOpen = !runDetailOpen;
runDetail.classList.toggle("hidden", !runDetailOpen);
});
list.appendChild(row);
}
taskCountdowns = countdowns;
Expand Down
12 changes: 12 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -412,6 +412,12 @@ const I18N = (() => {
"app.taskLastRunSummary": "干了:{n}",
"app.taskNoRunYet": "尚未运行",
"app.taskSaturation": "空转 {n} 轮,降频至 {m}s heartbeat",
"app.taskRunsEmpty": "暂无运行记录",
"app.taskRunsColTime": "时间",
"app.taskRunsColDone": "干了什么",
"app.taskRunsColThrottle": "降频",
"app.taskRunThrottle": "建议降频",
"app.taskRunIdle": "空转",
"app.deletedSwitch": "这个对话已被删除,已帮你切到最近的对话。",
"app.switched": "已切换对话。",
"app.sessionDisconnected": "该会话连接已断开,正在自动重连…",
Expand DownExpand Up@@ -836,6 +842,12 @@ const I18N = (() => {
"app.taskLastRunSummary": "did: {n}",
"app.taskNoRunYet": "never ran",
"app.taskSaturation": "{n} empty cycles, throttled to {m}s heartbeat",
"app.taskRunsEmpty": "No run records yet",
"app.taskRunsColTime": "Time",
"app.taskRunsColDone": "What was done",
"app.taskRunsColThrottle": "Throttle",
"app.taskRunThrottle": "slowdown advised",
"app.taskRunIdle": "idle",
"app.deletedSwitch": "This conversation was deleted — switched to the most recent one.",
"app.switched": "Conversation switched.",
"app.sessionDisconnected": "This session's connection is lost — reconnecting automatically…",
Expand Down
55 changes: 55 additions & 0 deletions emrg/gui/test/renderer.smoke.test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2645,6 +2645,61 @@ test("P3:设置面板打开 → 任务列表渲染(名称/类型/项目/间
assert.strictEqual(updated.enabled, true);
});

test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什么/降频徽章)", async () => {
const { ctx, els } = makeSandbox({
listTasks: async () => [
{
name: "emrg-task", type: "evolution", config: { project: "emrg" },
interval: 60, enabled: true, last_run_at: "2026-08-18T10:00:00",
last_cycle_summary: "修了双实例根因",
recent_runs: [
{ timestamp: "2026-08-18T10:00:00", summary: "修了双实例根因,提交 PR #854",
impact: ["cycle-ts-complete", "tools-executed=26"], meaningful: true,
recommend_slowdown: false, tool_count: 26 },
{ timestamp: "2026-08-18T09:00:00", summary: "",
impact: ["cycle-ts-complete", "tools-executed=3"], meaningful: false,
recommend_slowdown: false, tool_count: 3 },
{ timestamp: "2026-08-18T08:00:00", summary: "NTE",
impact: ["cycle-ts-complete"], meaningful: false,
recommend_slowdown: true, tool_count: 0 },
],
},
{ name: "fresh-task", type: "sync", config: { project: "docs" }, interval: 3600, enabled: true },
],
taskTemplateList: async () => [],
listProjects: async () => [{ name: "emrg", path: "/p/emrg" }],
});
await tick();
await vm.runInContext("App.openTasksPanel()", ctx);
await tick();
// 初始:子表隐藏(行内含 .task-run-detail.hidden)
const hiddenBefore = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-detail")).every((d) => d.classList.contains("hidden"))`, ctx);
assert.strictEqual(hiddenBefore, true, "子表初始应隐藏");
// 点击任务卡 → 展开
await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx);
await tick();
const detail = vm.runInContext(`document.getElementById("task-list").children[0].querySelector(".task-run-detail")`, ctx);
assert.strictEqual(detail.classList.contains("hidden"), false, "点击后子表应展开");
// 第一行 run:Agent 总结展示(自然语言)
const doneTxt = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")[0].textContent`, ctx);
assert.strictEqual(doneTxt, "修了双实例根因,提交 PR #854", `子表应显示 Agent 总结,实际: ${doneTxt}`);
// 降频徽章:recommend_slowdown=true → 建议降频;meaningful=false → 空转
const warnCount = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-badge-warn").length`, ctx);
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)
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]}`);
// 再次点击 → 折叠
await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx);
await tick();
assert.strictEqual(vm.runInContext(`document.getElementById("task-list").children[0].querySelector(".task-run-detail").classList.contains("hidden")`, ctx), true, "再次点击应折叠");
// 无 recent_runs → 占位文案
const emptyTxt = vm.runInContext(`document.getElementById("task-list").children[1].querySelector(".task-run-empty").textContent`, ctx);
assert.ok(emptyTxt.includes("暂无运行记录"), `无 recent_runs 应显示占位,实际: ${emptyTxt}`);
});

test("P3:新增任务表单 —— 间隔 <60 客户端拒绝;≥60 提交 taskCreate", async () => {
let created = null;
const { ctx, els } = makeSandbox({
Expand Down
7 changes: 7 additions & 0 deletions emrg/protocol.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ class EvolutionLog:
impact: list[str] = field(default_factory=list)
operations: list[str] = field(default_factory=list)
upstream_contribution: Optional[dict] = None
# rant 2026-08-18T21:32:32: the agent's own natural-language summary of
# what meaningful work was done this cycle (from the vibe check "done"
# field) + the vibe result flags, surfaced in GUI task recent-runs.
summary: str = ""
meaningful: Optional[bool] = None
recommend_slowdown: bool = False
tool_count: int = 0


@dataclass
Expand Down
13 changes: 10 additions & 3 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1154,7 +1154,11 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary

Asks whether a just-finished scheduled task produced meaningful value.
The agent must answer in strict JSON:
``{"meaningful": bool, "recommend_slowdown": bool, "reason": str}``.
``{"meaningful": bool, "recommend_slowdown": bool, "reason": str, "done": str}``.

``done`` (rant 2026-08-18T21:32:32) is a natural-language summary of
what meaningful work was done this cycle, for humans to read in the
GUI task recent-runs table. Old models / old parsing omit it → "".

Raises on any failure (caller sends ``ok: false``); the scheduler
conservatively leaves its empty-cycle counter unchanged then.
Expand All@@ -1164,12 +1168,14 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary
"任务要求与最终回复摘要如下。\n"
"请用 JSON 严格回答(不要任何其他文字),格式:\n"
'{"meaningful": true|false, "recommend_slowdown": true|false, '
'"reason": "一句话原因"}\n'
'"reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}\n'
"- meaningful:这轮是否对项目产生了有意义的价值(产出/提交/分析/决策/"
"维护动作都算;纯空转/无可做=NTE 算 false)\n"
"- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token"
"(true=建议降低检查频率)\n"
"- reason:简短中文原因"
"- reason:简短中文原因(给降频判断用)\n"
"- done:这次干了哪些有意义有价值的事(自然语言列举,如:修了 XX bug / "
"加了 XX 功能 / 分析了 XX;没有则空字符串)"
)
user = (
"任务名称:" + (task_name or "") + "\n"
Expand All@@ -1193,6 +1199,7 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary
"meaningful": bool(data.get("meaningful")),
"recommend_slowdown": bool(data.get("recommend_slowdown")),
"reason": str(data.get("reason", ""))[:200],
"done": str(data.get("done", ""))[:500],
}

def _build_system_prompt(self, session: Session | None = None) -> str:
Expand Down
49 changes: 46 additions & 3 deletions emrg/server/scheduler.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -750,9 +750,26 @@ def status(self) -> dict:
if self.evolutions:
last = self.evolutions[-1]
last_run_at = last.timestamp
# brief summary: tools-executed / cycle-complete etc. from impact
parts = [str(i) for i in last.impact if i]
last_cycle_summary = ", ".join(parts[:3]) if parts else None
# 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-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.
recent_runs = []
for log in self.evolutions[-5:]:
recent_runs.append({
"timestamp": log.timestamp,
"summary": log.summary,
"impact": list(log.impact),
"meaningful": log.meaningful,
"recommend_slowdown": log.recommend_slowdown,
"tool_count": log.tool_count,
})
saturation = {
"empty_cycles": self._empty_cycles,
"threshold": self._saturation_threshold(),
Expand All@@ -766,6 +783,7 @@ def status(self) -> dict:
"interval": self.interval,
"last_run_at": last_run_at,
"last_cycle_summary": last_cycle_summary,
"recent_runs": recent_runs,
"saturation": saturation,
}

Expand DownExpand Up@@ -851,6 +869,7 @@ async def _request_vibe_check(self, ws, prompt: str, completion_summary: str) ->
"meaningful": result.get("meaningful"),
"recommend_slowdown": result.get("recommend_slowdown"),
"reason": result.get("reason", ""),
"done": result.get("done", ""),
}
except Exception:
logger.debug("TaskHandler[%s]: vibe check failed", self.name, exc_info=True)
Expand DownExpand Up@@ -1065,11 +1084,31 @@ async def _run_evolution_cycle(self) -> None:
if truncated:
impact.append("truncated=max-tool-rounds")

# 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).
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,
trigger=f"evolution-{self.name}-{cycle_ts}",
impact=impact,
operations=["llm-reflection", "tool-execution", "self-improvement"],
summary=summary,
meaningful=meaningful,
recommend_slowdown=recommend,
tool_count=tool_count,
)
await self._write_evolution_log(log)
self.evolutions.append(log)
Expand DownExpand Up@@ -1126,6 +1165,10 @@ async def _write_evolution_log(self, entry: EvolutionLog) -> None:
"trigger": entry.trigger,
"impact": entry.impact,
"operations": entry.operations,
"summary": entry.summary,
"meaningful": entry.meaningful,
"recommend_slowdown": entry.recommend_slowdown,
"tool_count": entry.tool_count,
}
path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")

Expand Down
Loading
Loading