Skip to content

fix(#410): move compress/decompress permission ask outside the per-session guard - #413

Open
ranxianglei wants to merge 1 commit into
2026-09-16_serialize-session-init-transformsfrom
2026-09-16_session-guard-abandoned-ask
Open

ranxianglei wants to merge 1 commit into
2026-09-16_serialize-session-init-transformsfrom
2026-09-16_session-guard-abandoned-ask

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

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() in registry.withSessionGuard. The guarded body's first statement is the interactive host permission prompt await toolCtx.ask(...), which can wait unbounded and can be abandoned without settling (session deleted / turn aborted / dropped continuation). Because the guard releases in a finally, an unsettled ask means 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 ask await outside the guard in both tools; only the bounded state-mutating transaction (fetchSessionMessagesensureSessionInitializedassignMessageRefs → mutate → save) stays serialized end-to-end.

  • lib/compress/pipeline.ts: new exported requestCompressPermission(toolCtx, title) — single owner of the ask + metadata({title}) side effect. prepareSession(...) gains an optional { preApproved?: boolean } (default false) so existing direct/test callers are unchanged.
  • lib/compress/range.ts: computes title before the guard, calls requestCompressPermission outside it, passes { preApproved: true } to prepareSession.
  • lib/compress/decompress.ts: same pattern via prepareDecompressSession(..., { 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

  • Regression tests (+2) in tests/session-guard.test.ts drive the real compress-range and decompress tools with a never-settling toolCtx.ask and assert a concurrent same-session withSessionGuard still completes. Verified red→green: temporarily reverting range.ts makes the test fail (timeout vs completed); re-applying the fix turns it green.
  • tsc --noEmit clean; full suite 1273/0.
  • Dual-agent review (AGENTS.md §5.3/§5.6): both APPROVE-WITH-NITS, zero blockers/majors; each ran tsc + full suite independently. Nits addressed in-branch: added the decompress mirror test and documented the concurrent-prompts behavior delta in the devlog.

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.

…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.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 收到。这是带 ework-agent-pr 标记的已完成 PR,我不重做工作,改为:拉取分支核对代码 → 独立跑 typecheck + 全量测试 → 核对 CI 状态 → 给出结论。正在拉取 2026-09-16_session-guard-abandoned-ask(head 3f7b68d),稍后回复审查结果。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 审查完成(按 ework-agent-pr 协议:核对代码 + 独立验证,不重做工作)。结论:APPROVE,#408 落地后即可合入。我未在分支上做任何改动(工作区保持干净)。

独立验证结果(Node 22.23.2)

结果
tsc --noEmit ✅ clean
全量测试 1273 pass / 0 fail(与 PR 声明一致)
npm run build ✅ OK
red→green 复现 ✅ 见下

Red→green 亲自复现:在本地临时回退 lib/compress/range.ts 的修复(去掉 { preApproved: true } + 去掉 guard 外的 requestCompressPermission 调用)后跑 tests/session-guard.test.ts:compress-range 的 #410 回归测试如预期失败(走 timeout 分支,断言 same-session op must complete despite the abandoned tool execution 失败),decompress 镜像测试仍通过(该文件未被回退);恢复修复后 10/10 全绿。测试确实能抓住这个 bug,不是恒真测试。

代码层核对

  • 增量 diff(vs fix: serialize same-session state initialization and transforms (#404) #408 tip 8340afa):单 commit,7 个文件(+473/−14)= devlog REQ/WORKLOG/DESIGN + lib/compress/{pipeline,range,decompress}.ts + tests/session-guard.test.ts。无 version bump、无无关文件、无纯空格噪音。diff 干净。
  • lib/ 下现在唯一的 .ask(lib/compress/pipeline.ts:69requestCompressPermission 本体);hooks.ts 无任何 .ask( —— 没有其他持锁路径还包含交互式 prompt。
  • preApproved: truetitle 参数在 prepareSession 内部不再被使用 → metadata({ title }) 副作用恰好执行一次(不会重复);不传 preApproved 时行为与旧代码逐字节一致,直接调用方不受影响(如 tests/remove-prune-regression.test.ts:156 的三参调用)。
  • index.ts:151-152 注册的两个工具(compress=range、decompress)都走了同一修复模式;devlog 目录名与分支名一致;PR 带 Fixes #410

CI 状态说明(重要)

本 PR 没有任何 CI 运行记录ci.ymlpr-checks.yml 都只触发于 pull_request: branches: [master],而 #413 的 base 是 #408 的分支,所以 GitHub 上零 check(mergeable_state: clean)。因此上面的本地验证就是本次质量门禁。docker E2E 在本沙箱无法运行(无 docker),会在最终合入 master 时由 push 触发补跑。

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 先落地)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant