You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Session guard can leak permanently if tool execution is abandoned during a permission ask #410
PR #404 wraps the entire compress/decompress tool execution in registry.withSessionGuard(toolCtx.sessionID, ...). That guarded region includes prepareSession() → toolCtx.ask(...), i.e. the host permission prompt, which can wait an unbounded amount of time (user away from keyboard).
The guard releases in a finally block. If the host ever abandons tool execution without rejecting the execute promise (session deleted, user aborts the turn, process-level cancel that drops the continuation instead of settling it), the finally never runs and that session's guard chain never releases.
Impact: every subsequent same-session operation (transforms, tools, event-hook saves, command handlers) awaits the guard forever → the session wedges until the opencode process restarts. Cross-session work is unaffected (per-session chains).
Whether opencode v1 actually abandons (vs rejects) interrupted tool executions could not be verified from this repo — needs live verification against the v1 runtime.
Suggested directions
Verify v1 interrupt semantics live (abort a session while a compress permission dialog is open; observe whether execute settles).
If abandonment is possible: narrow the guard scope to exclude the ask await (acquire around state mutation/persistence only), or add a watchdog/timeout that force-releases the chain entry.
Add a regression test simulating an unsettled tool execution asserting the next same-session transform still completes.
run() starts with prepareSession / prepareDecompressSession, whose first statement is await toolCtx.ask(...) — the interactive permission prompt (lib/compress/pipeline.ts:59, lib/compress/decompress.ts:47). Every state mutation (ensureSessionInitialized, assignMessageRefs) happens after the ask.
createSessionGuard (lib/state/state.ts) releases the chain entry only in a finally after await fn(). Traced it: if fn() never settles, release() never runs, the stored chain promise stays pending forever, and every later same-session withSessionGuard call hangs at await previous. Cross-session keys are unaffected. Matches your described impact exactly.
Root cause (not symptom)
The guard couples lock-release to the guarded body settling, but the guarded body contains an unbounded, externally-abandonable await (ask). Holding a serialization lock across a user-facing permission prompt is wrong twice over: (1) it wedges the whole session if the prompt is abandoned, and (2) even when it settles normally, it blocks all legitimate same-session work for as long as the user is away from the keyboard.
Fix (your primary suggested direction — narrow the scope)
Move ask (and the sync metadata title call) outside the guard in both tools; the guard then covers only the bounded state-mutating transaction (fetch → init → mutate → persist). This kills the reported wedge with no magic timeout/timer. I'm also adding a regression test that drives the real compress tool with an abandoned (never-settling) ask and asserts the next same-session guard operation still completes — it fails on current #408 code and passes with the fix.
Not done / needs you
Live v1 verification (abort a session with the permission dialog open, observe whether execute settles) — I don't have a live v1 runtime here. Note the fix is robust regardless of v1's exact semantics, since it removes the dependence on the prompt settling. Residual: any other abandonable await inside the guarded region (e.g. if v1 abandons mid-fetch/mid-persist instead of at the prompt) would still wedge — closing that fully needs the live answer, or a deliberate watchdog decision (left out to avoid a magic-number timeout that could itself break serialization).
I'm stacking the fix on PR #408's head (the guard lives there, and it hasn't merged). PR link coming shortly.
Confirmed real, root-caused, fixed, and shipped as a stacked PR.
Verification (per triage rules)
Repro: reproduced against the real createSessionGuard — a same-session task whose body never settles leaves the next same-session task hanging forever (WEDGE CONFIRMED). Cross-session is unaffected, exactly as reported.
Layering: the whole-session wedge you described is the consequence; the root cause is that fix: serialize same-session state initialization and transforms (#404) #408's guard spans an abandonable await. prepareSession()'s first statement is await toolCtx.ask(...) — the interactive permission prompt — which can wait unbounded and can be abandoned without settling. Because the guard releases in a finally, an unsettled ask means the body never returns → the lock is never released → every later same-session op awaits the guard forever.
Fix direction: adopted your primary suggestion (narrow the scope) rather than a watchdog — rationale below.
Fix (PR #413)
Move the abandonable askoutside the guard in both tools; only the bounded state-mutating transaction stays serialized:
lib/compress/pipeline.ts: new exported requestCompressPermission(toolCtx, title) owns 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 / lib/compress/decompress.ts: compute the title, call requestCompressPermissionoutsidewithSessionGuard, and pass { preApproved: true } into the guarded body.
Bonus: removes the unnecessary lock hold during normal prompt review — a user reading a permission dialog no longer blocks legitimate same-session work.
Why not a watchdog/timeout: a magic timeout risks force-releasing a legitimately long-but-alive transaction (a large session fetch/persist), which would reintroduce #404's stale-write corruption. Narrowing the scope has no such failure mode.
Testing
+2 regression tests 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 (reverting the fix makes them fail).
tsc --noEmit clean; full suite 1273/0. Dual-agent code + test review (AGENTS.md §5.3/§5.6): both approve with nits (addressed — added the decompress mirror test and documented the concurrent-prompts behavior delta).
Residual (documented in devlog/2026-09-16_session-guard-abandoned-ask/WORKLOG.md §5): if v1 abandons a 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 decision — I left it out of this PR deliberately. Happy to split it into a follow-up issue if you'd like it tracked.
PR #413 is stacked on #408 (base = its branch) so the diff shows only the incremental change; it folds in cleanly once #408 lands. Merge is yours to do.
Activity
ranxianglei commentedon Sep 16, 2026
[bot] 🏷 Triage update — verified the exposure at the code level, implementing a fix now
What I verified
execute()body inregistry.withSessionGuard(sessionId, () => run())—lib/compress/range.ts:375,lib/compress/decompress.ts:415.run()starts withprepareSession/prepareDecompressSession, whose first statement isawait toolCtx.ask(...)— the interactive permission prompt (lib/compress/pipeline.ts:59,lib/compress/decompress.ts:47). Every state mutation (ensureSessionInitialized,assignMessageRefs) happens after the ask.createSessionGuard(lib/state/state.ts) releases the chain entry only in afinallyafterawait fn(). Traced it: iffn()never settles,release()never runs, the stored chain promise stays pending forever, and every later same-sessionwithSessionGuardcall hangs atawait previous. Cross-session keys are unaffected. Matches your described impact exactly.Root cause (not symptom)
The guard couples lock-release to the guarded body settling, but the guarded body contains an unbounded, externally-abandonable await (
ask). Holding a serialization lock across a user-facing permission prompt is wrong twice over: (1) it wedges the whole session if the prompt is abandoned, and (2) even when it settles normally, it blocks all legitimate same-session work for as long as the user is away from the keyboard.Fix (your primary suggested direction — narrow the scope)
Move
ask(and the syncmetadatatitle call) outside the guard in both tools; the guard then covers only the bounded state-mutating transaction (fetch → init → mutate → persist). This kills the reported wedge with no magic timeout/timer. I'm also adding a regression test that drives the real compress tool with an abandoned (never-settling)askand asserts the next same-session guard operation still completes — it fails on current #408 code and passes with the fix.Not done / needs you
Live v1 verification (abort a session with the permission dialog open, observe whether
executesettles) — I don't have a live v1 runtime here. Note the fix is robust regardless of v1's exact semantics, since it removes the dependence on the prompt settling. Residual: any other abandonable await inside the guarded region (e.g. if v1 abandons mid-fetch/mid-persist instead of at the prompt) would still wedge — closing that fully needs the live answer, or a deliberate watchdog decision (left out to avoid a magic-number timeout that could itself break serialization).I'm stacking the fix on PR #408's head (the guard lives there, and it hasn't merged). PR link coming shortly.
中文一句话:已代码级确认 PR #408 把权限询问
ask包进会话锁、ask被放弃会导致整会话永久卡死是真实缺陷;修复是把ask移出锁范围(只锁有界的状态变更事务)+ 回归测试,正堆叠在 #408 上。ranxianglei commentedon Sep 16, 2026
Confirmed real, root-caused, fixed, and shipped as a stacked PR.
Verification (per triage rules)
createSessionGuard— a same-session task whose body never settles leaves the next same-session task hanging forever (WEDGE CONFIRMED). Cross-session is unaffected, exactly as reported.prepareSession()'s first statement isawait toolCtx.ask(...)— the interactive permission prompt — which can wait unbounded and can be abandoned without settling. Because the guard releases in afinally, an unsettledaskmeans the body never returns → the lock is never released → every later same-session op awaits the guard forever.Fix (PR #413)
Move the abandonable
askoutside the guard in both tools; only the bounded state-mutating transaction stays serialized:lib/compress/pipeline.ts: new exportedrequestCompressPermission(toolCtx, title)owns theask+metadata({title})side effect;prepareSession(...)gains an optional{ preApproved?: boolean }(defaultfalse, so existing direct/test callers are unchanged).lib/compress/range.ts/lib/compress/decompress.ts: compute the title, callrequestCompressPermissionoutsidewithSessionGuard, and pass{ preApproved: true }into the guarded body.Bonus: removes the unnecessary lock hold during normal prompt review — a user reading a permission dialog no longer blocks legitimate same-session work.
Why not a watchdog/timeout: a magic timeout risks force-releasing a legitimately long-but-alive transaction (a large session fetch/persist), which would reintroduce #404's stale-write corruption. Narrowing the scope has no such failure mode.
Testing
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 (reverting the fix makes them fail).tsc --noEmitclean; full suite 1273/0. Dual-agent code + test review (AGENTS.md §5.3/§5.6): both approve with nits (addressed — added the decompress mirror test and documented the concurrent-prompts behavior delta).Residual (documented in
devlog/2026-09-16_session-guard-abandoned-ask/WORKLOG.md§5): if v1 abandons a 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 decision — I left it out of this PR deliberately. Happy to split it into a follow-up issue if you'd like it tracked.PR #413 is stacked on #408 (base = its branch) so the diff shows only the incremental change; it folds in cleanly once #408 lands. Merge is yours to do.
中文摘要:确认该泄漏真实存在(用真实
createSessionGuard复现了"同会话后续操作永久卡死"),根因是 #408 的会话守卫把可被宿主放弃的权限询问ask()包进了临界区,导致finally永不触发、锁永不释放;修复按你的首选方案把ask移到守卫之外、只让有界的状态事务保持串行(不引入魔法超时,避免复活 #404 的陈旧写风险);已补红绿回归测试、tsc与全量 1273/0 通过、双 agent review 通过,以堆叠 PR #413 提交,待你合并。ranxianglei commentedon Sep 17, 2026
继续
ranxianglei commentedon Oct 23, 2026
查重:不重复(上一轮已确认,无同症状+同机制的其他条目)。
本轮「继续」做的事:把两个 PR 追平到最新 master(v1.18.5)并验证。它们停在 5 周前的 v1.18.0 基点上,期间 master 合入了大量改动(#478 图片归档、#485 紧急截断改 opt-in+sticky 回放、smart-plan 可见消息记录等),直接合并会冲突。
完成项:
lib/hooks.ts——fix: serialize same-session state initialization and transforms (#404) #408 把整个 transform 流水线移进了runPipeline(),而 master 在旧的内联位置新增了三个阶段。解法:把 master 的三个阶段逐字移植进runPipeline,移植后与 master 原文逐行比对(除缩进外字节一致);自动合并的三个文件(range.ts/decompress.ts/state.ts)逐一审计,与 master 的差集恰好只有 fix: serialize same-session state initialization and transforms (#404) #408 的 guard 包装本身,无丢失无污染。preApproved避免二次询问。inactive-block-decompress.test.ts一个用例硬编码/tmp路径,而 decompress 工具按os.tmpdir()校验路径——任何 TMPDIR 被重定向的环境都会挂(纯 master 上同样复现,CI 因os.tmpdir()恰为/tmp抓不到)。已改用os.tmpdir()构造路径,7/7 绿。当前卡点:推送失败——本地代码镜像的 git 服务持续返回 HTTP 502(同期 REST API 正常,判断为 git 服务侧瞬态故障),约 15 分钟内多轮重试未恢复。两条分支的提交已全部就绪在本地,服务恢复后立即推送并回报。这是基础设施问题,不是代码问题。
八问:
compress.permission=ask且询问中途被中断的会话(默认 allow 模式不触发);一旦触发该会话彻底不可用,属会话无法恢复类 → p1。标签(本环境 API 未实现标签端点,列出拟打值):bug / opencode / scope/kernel / p1(会话无法恢复类,按 2026-10-19 规则一律 p1)。
判定:代码侧闭环(两分支就绪+定向验证全绿),待镜像 git 服务恢复即推送;推送后 CI 绿,合并顺序 #408 先行 → #413 跟进(合并由人来点)。
ranxianglei commentedon Oct 23, 2026
[bot] 🏷 状态更新(接上一条):
推送仍未完成。后台重试窗口(60 次、间隔 2 分钟、共约 2 小时)已全部耗尽,最后一次直推仍为同一错误:镜像 git 服务返回 HTTP 502(REST API 同期正常)。故障已持续 2.5 小时以上,不再是瞬态抖动——需要运维侧查看镜像的 git 服务。
代码侧状态不变,全部就绪在本地两条分支上:
2026-09-16_serialize-session-init-transforms(fix: serialize same-session state initialization and transforms (#404) #408)@ b285315 —— 已追平 master v1.18.5,冲突已解并逐字核对2026-09-16_session-guard-abandoned-ask(fix(#410): move compress/decompress permission ask outside the per-session guard #413)@ 28a7dbd —— 已并入更新后的 fix: serialize same-session state initialization and transforms (#404) #408 + 一个独立标注的测试小修git 服务恢复后(下次唤醒或收到「继续」)立即推送两条分支并回报,随后进入 CI 观察。在那之前没有更多可推进的代码工作——不重复空转重试。
ranxianglei commentedon Oct 23, 2026
[bot] 🏷 推送完成(接状态更新):
镜像 git 服务持续不可用超过 11 小时,已改走直连 GitHub 推送,两条分支均已落地并核对远端 ref:
2026-09-16_serialize-session-init-transforms:8340afab → b285315(追平 master v1.18.5 + 冲突解决)2026-09-16_session-guard-abandoned-ask:3f7b68da → 28a7dbd(并入更新后的 fix: serialize same-session state initialization and transforms (#404) #408 + 独立标注的测试小修)CI 现状:#408 的四个检查(pr-validation / test×2 / build)已触发但全部排队中——Actions runner 队列积压,约 12 分钟未开始执行;#413 尚未被 CI 拾取(同一队列)。结果出来我再核一遍并回报。
待办只剩人工动作:CI 绿后按顺序合并 #408 先行 → #413 跟进(合并按钮由人来点)。镜像 git 服务恢复与否不影响这两条 PR,但建议运维仍要排查。
ranxianglei commentedon Oct 23, 2026
[bot] 🏷 CI 状态更新(含一个新发现):
#413 之前没有 CI 不是队列问题,是 base 设错了。 它此前 stack 在 #408 分支上(base = #408 的分支),而本仓库三个工作流(ci / pr-checks / pr-artifact)都只监听
pull_request → branches: [master]——stacked PR 永远不会触发任何检查。已处理:#408 @ b285315:Build Artifact 已绿;CI + PR Checks 仍在 runner 队列等待。其前三个 tip(571ccf0b / 2d817b1 / 8340afa)均为全绿记录。
两条 PR 现在都在 CI 可见、排队执行的状态。结果出来我再核并回报;合并顺序不变——#408 先行 → #413 跟进,合并按钮由人来点。
ranxianglei commentedon Oct 23, 2026
[bot] 🏷 排队原因查明(不是卡死):GitHub 组织级并发槽位被同账号其他仓库的 CI 占满(当前有 4 个兄弟仓库 CI 在跑),本仓库的测试任务在队列里等槽位。证据:#408 的 Build Artifact 任务从入队到跑完用了约 18 分钟(已绿),CI / PR Checks 还在等下一个空槽;兄弟仓库的在途 CI 最早已跑了 2 小时,陆续结束后这两条 PR 的检查会依次落地。无需干预,结果出来我继续核并回报。