Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ What EMRG does today — capability statements, not a changelog.
One line per command: what it answers. Run the daemon in the foreground with `python -m emrg`.

- Python: `uv run pytest tests/ -v` — the number is **measured, never stored**: `uv run --no-sync python3 scripts/check-doc-count.py --measure`. Import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (168: 100 daemon_client + 20 conn-manager + 7 integration + 7 nav-policy + 7 gui-state + 9 build-config + 11 boot-contract + 3 preload-api + 2 theme-guard + 2 image-hash) — syntax: `node --check main.js preload.js daemon_client.js`
GUI: `cd emrg/gui && npm test` (169: 100 daemon_client + 20 conn-manager + 7 integration + 7 nav-policy + 7 gui-state + 9 build-config + 12 boot-contract + 3 preload-api + 2 theme-guard + 2 image-hash) — syntax: `node --check main.js preload.js daemon_client.js`
Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (740: 5 snapshot-store + 13 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 13 markdown + 29 transcript + 28 TranscriptView + 22 history + 25 historyReplay + 40 composer + 75 Composer + 6 LinkDialog + 16 sidebar + 18 Sidebar + 9 fileTree + 9 FileTree + 17 resultPanel + 8 ResultPanel + 27 workspaceView + 13 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 12 dialogLists + 3 HelpDialog + 9 MemoryDialog + 8 SkillsDialog + 8 openSession + 6 WelcomeDialog + 9 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 59 daemonBridge + 9 DaemonBridgeProvider + 50 Shell + 25 DialogHost + 26 SettingsPanel + 18 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown + 6 ApprovalDialog + 16 SandboxRootsDialog)
- CI: pytest on ubuntu **and windows-2025**, the GUI suites, and an actionlint gate over `.github/workflows/`; re-trigger a dropped push with `scripts/re-trigger-ci.sh <branch>`, never an empty commit
- Merge gates, run before merging — each asks what no other gate asks: `uv run --no-sync python3 scripts/check-vote-count.py` (approvals still valid) · `scripts/check-pr-base.py` (base reaches master) · `scripts/check-merge-freshness.py` (that green CI is about the landing tree) · `scripts/check-merge-order.py` (dirt and what it lands) · `scripts/check-merge-landing-diff.py` (what the landing changes) · `scripts/check-merge-pairs.py` / `scripts/check-merge-sequence.py` (a pair, or a whole order) · `scripts/check-merge-tree-health.py` (the merged tree passes one guard) · `scripts/check-merge-plan-suite.py` (the plan's final tree passes the suite) — all under `uv run --no-sync python3`
Expand Down
8 changes: 3 additions & 5 deletions emrg/gui/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -584,11 +584,9 @@ vision = false
logger.warn(`[gui:switch] REJECT too many open sessions (${openSessions.size}/${DEFAULT_CAP}) sid=${sessionId}`);
throw new Error(`too many open sessions (${DEFAULT_CAP}) — close some first`);
}
// G65:自有流运行中禁止切会话(每连接独立锁,查当前激活连接)
if (connManager?.get(currentSessionId)?.ownStream) {
logger.warn(`[gui:switch] REJECT own stream in progress (sid=${currentSessionId})`);
throw new Error("stream in progress — cannot switch");
}
// issue #2637:切会话不看 ownStream。自有流跑在它自己会话的连接上,切换不动它
// (P4:切走不关闭旧连接),所以「流运行中禁止切」只拒绝、不保护。G65 锁仍归
// close() / cancel 用:conn-manager 关闭在忙连接时先 cancel 再断 ws。
const prevSid = currentSessionId;
logger.info(`[gui:switch] prevSid=${prevSid} → target=${sessionId}, openSessions.size=${openSessions.size}`);
// G110:切会话清空旧连接分组缓存(含 timer),防广播"幽灵"残留
Expand Down
14 changes: 13 additions & 1 deletion emrg/gui/renderer/src/components/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,19 @@ export function Shell() {

async function selectProjectSession(p: AgentRec, sid: string) {
const b = wsBridge();
if (b?.switchSession) await b.switchSession({ sessionId: sid });
// issue #2637:这里原来没有 catch —— 一次拒绝就是未处理的 promise 拒绝,界面毫无
// 反应(与任务面板的 catch 不对称)。拒绝要有落点。
if (b?.switchSession) {
try {
await b.switchSession({ sessionId: sid });
} catch (e) {
transcript.addSystemMessage(
t("openSession.loadFailed", { msg: (e as Error)?.message ?? String(e) }),
activeSid,
);
return;
}
}
setActiveSid(sid);
setActiveView("sessions");
void loadHistory(sid);
Expand Down
31 changes: 31 additions & 0 deletions emrg/gui/test/boot-contract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,34 @@ test("issue #2490: emrg:taskLimits reports the daemon's own minimum, never a lit
"no literal floor may live in this handler — that is the copy issue #2490 removes"
);
});

// issue #2637:切会话不再因「当前会话自有流运行中」被拒。
// 那条拒绝成立过一个年代:切换当时会关闭旧连接,而 close() 对 ownStream 连接先发
// cancel 再断 ws(conn-manager.test.js 的 P6 正钉着「先 cancel 再 close」),于是放行
// 切换就等于静默杀掉正在跑的那一轮。此后切换改为不动旧连接(关闭是 emrg:closeSession
// 的事),每会话各有 ws 与记录桶 ⇒ 那条拒绝只剩拒绝本身,而它的两个调用方都把拒绝说
// 在用户看不见的地方:侧边栏没有 catch(未处理的 promise 拒绝、界面毫无反应),任务
// 面板的 catch 写进「正在离开的」那个会话的记录。
test("issue #2637: emrg:switchSession does not consult ownStream to refuse a switch", () => {
// 「查了没有」问的是代码,不是注释:注释里点到这把锁的正当改版必须静默,代码里真去
// 读它必须变红 —— 两条都在下面各钉一次,否则这个谓词读不出任何东西。
const withoutLineComments = (s) => s.replace(/^\s*\/\/.*$/gm, "");
const region = handlerRegion(read("main.js"), "emrg:switchSession");
assert.ok(
!/ownStream/.test(withoutLineComments(region)),
"a streaming turn lives on its own session's connection and outlives the switch, so " +
"refusing the navigation protects nothing; the lock still matters to close(), which " +
"cancels a busy connection — it just must not gate switching",
);
assert.ok(
!/ownStream/.test(withoutLineComments("// 切会话不查 ownStream\nfoo();")),
"a comment naming the lock is not a consultation of it",
);
assert.ok(
/ownStream/.test(withoutLineComments('if (c?.ownStream) throw new Error("x");')),
"reading it in code is still caught — else this guard reads nothing at all",
);
// 反方向:同一段里别的拒绝不许被一起带走。
assert.match(region, /REJECT invalid session_id/, "session-id validation stays a refusal");
assert.match(region, /too many open sessions/, "the open-session cap stays a refusal");
});
Loading