fix(#410): move compress/decompress permission ask outside the per-session guard - #413
Conversation
…ssion guard The per-session guard added in #408 wrapped the whole compress/decompress tool execute() body, including the interactive host permission prompt (await toolCtx.ask(...)). If the host abandons the tool execution without settling that promise (session deleted / turn aborted / dropped continuation), ask() never settles, run() never returns, the finally never runs, and the per-session chain entry is never released - every later same-session operation (transforms, tools, event-hook saves) then awaits the guard forever and the session wedges until process restart. Move the abandonable ask await OUTSIDE the guard in both tools; only the bounded state-mutating transaction (fetch -> init -> assignMessageRefs -> mutate -> save) stays serialized end-to-end. Extract requestCompressPermission(toolCtx, title) into lib/compress/pipeline.ts as the single owner of the ask+metadata side effect; prepareSession/prepareDecompressSession gain an optional { preApproved?: boolean } (default false) so existing direct/test callers are unchanged. Also removes the unnecessary lock hold during normal prompt review: a user reading a permission dialog no longer blocks legitimate same-session work, and two concurrent same-session prompts can now be live at once (harmless - prompts mutate no state). Tests: +2 regression tests in tests/session-guard.test.ts driving the real compress-range and decompress tools with a never-settling ask, asserting a concurrent same-session withSessionGuard still completes. Verified red->green by temporarily reverting range.ts. Full suite 1273/0; tsc clean. Fixes #410. Stacks on #408.
[bot] 🏷 收到。这是带 |
[bot] 🏷 审查完成(按 独立验证结果(Node 22.23.2)
Red→green 亲自复现:在本地临时回退 代码层核对
CI 状态说明(重要)本 PR 没有任何 CI 运行记录: Nit(仅报告,不阻塞):prettier 格式漂移
Residual exposure文档化的遗留风险(guard 内其他可被遗弃的 await,如 mid-fetch/mid-persist)同意按 PR 正文处理:magic timeout 会重新引入 #404 的 stale-write 风险,权衡下推迟合理。是否拆 follow-up issue 由你决定。 中文摘要:独立复核通过——typecheck、全量 1273 测试、构建全绿,red→green 已亲自复现确认回归测试有效,修复正确且 diff 干净;注意本 PR 因堆叠在 #408 分支上没有任何 CI 运行(本地验证已覆盖 typecheck+单测+构建,E2E 待合入 master 时补跑);唯一小问题是仓库既有的 prettier 格式漂移(CI 未拦截),不构成阻塞——可以合并(待 #408 先落地)。 |
Stacked on #408 (branch
2026-09-16_serialize-session-init-transforms). Base is set to that branch so this diff shows only the incremental change; it folds in cleanly once #408 lands.Problem (#410)
#408 wraps the entire compress/decompress tool
execute()inregistry.withSessionGuard. The guarded body's first statement is the interactive host permission promptawait toolCtx.ask(...), which can wait unbounded and can be abandoned without settling (session deleted / turn aborted / dropped continuation). Because the guard releases in afinally, an unsettledaskmeans the body never returns → the lock is never released → every later same-session operation awaits the guard forever and the session wedges until process restart. Cross-session work is unaffected (per-session chains).Fix
Move the abandonable
askawait outside the guard in both tools; only the bounded state-mutating transaction (fetchSessionMessages→ensureSessionInitialized→assignMessageRefs→ mutate → save) stays serialized end-to-end.lib/compress/pipeline.ts: new exportedrequestCompressPermission(toolCtx, title)— single owner of theask+metadata({title})side effect.prepareSession(...)gains an optional{ preApproved?: boolean }(defaultfalse) so existing direct/test callers are unchanged.lib/compress/range.ts: computestitlebefore the guard, callsrequestCompressPermissionoutside it, passes{ preApproved: true }toprepareSession.lib/compress/decompress.ts: same pattern viaprepareDecompressSession(..., { preApproved: true }).Bonus: removes the unnecessary lock hold during normal prompt review — a user reading a permission dialog no longer blocks legitimate same-session work; two concurrent same-session prompts can now be live at once (harmless, prompts mutate no state).
Verification
tests/session-guard.test.tsdrive the real compress-range and decompress tools with a never-settlingtoolCtx.askand assert a concurrent same-sessionwithSessionGuardstill completes. Verified red→green: temporarily revertingrange.tsmakes the test fail (timeoutvscompleted); re-applying the fix turns it green.tsc --noEmitclean; full suite 1273/0.Residual exposure (documented, deferred)
If v1 abandons a tool call at some other abandonable await inside the guarded region (e.g. mid-fetch / mid-persist), the same leak class would still apply. Fully closing that needs either live verification of v1's interrupt semantics (not possible from this sandbox) or a watchdog/timeout that force-releases a stuck chain entry — deliberately deferred here, since a magic timeout risks releasing a legitimately long-but-alive transaction and reintroducing #404's stale-write corruption. See
devlog/2026-09-16_session-guard-abandoned-ask/WORKLOG.md§5. Happy to split that into a follow-up issue if you want it tracked.Fixes #410.