Conversation
Rewinding to a turn that carried quotes used to fail closed with rewind_unsupported_quotes, because the TUI could only refill the human-facing text and the replacement submit would silently drop the turn's structured context (apache#5109). The runtime-host driver now returns the rewound turn's QuoteRefs verbatim and forwards quotes given to submitMessage through turn.message.submit, whose admission already accepts them (only session-context attachments are Host-owned). Attachments and directory references still fail closed, since the TUI cannot re-attach files. The TUI stages the restored quotes keyed to the branched session: the status line carries a quotes:<n> segment while staging is live, bare /quotes lists the staged excerpts, /quotes clear discards them, and the first admitted submit consumes the staging while a refusal or failure restages it for the retry. Part of apache#5109 Generated-by: GLM-5.3-Flash (ZCode)
assert.ok the recorded submit before reading its content, per the noUnsafeOptionalChaining lint rule. Part of apache#5109 Generated-by: GLM-5.3-Flash (ZCode)
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for addressing the quoted-turn gap in #5109. I reviewed eb5c88fc3fcc56849fddb954349ff9a7074caae6. Returning the existing QuoteRefs and forwarding them through the existing Host admission is the right direction; attachments can continue to fail closed until their target-owned refs can be restored.
I found two reachable issues, detailed inline: P1, a delayed failure can attach one Session's staged quotes to another Session; P2, a quote-only replacement still cannot pass the text-only submit guards. These are draft ownership/content-admission issues within the existing runner, not a reason to introduce a new revision or import framework. Please bind retry restoration to the original draft/session identity and treat staged quotes as meaningful content at the submit entry points.
Validation: the two new runner tests pass in an exact-source bundle with local dependencies. Two additional probes through the real runner/editor submission path reproduce both findings. I also checked the production driver's switch/admission code: session switching does not wait on the submit admission queue, so the delayed-failure interleaving is reachable. I did not run an installed TUI with a live Host or the full repository suite.
Codex-assisted review with local reproduction.
| if (staged.length > 0) { | ||
| stagedRewindQuotes = staged; | ||
| stagedQuotesSessionId = input.driver.getSessionId(); | ||
| } |
There was a problem hiding this comment.
[P1] Restore failed-submit quotes only to their original draft
Reachability ②: an admission request can be pending while /session other-session succeeds. Both failure branches tag the old staged payload with driver.getSessionId() at callback time, which is now the other Session. In a real-runner probe, I rewound a quoted turn, held its submit promise, switched Sessions, then rejected the original request; the next unrelated message in the new Session carried the old quote. This silently sends context to the wrong conversation (and potentially a different model connection).
Could you capture the originating Session/draft generation before dispatch and restore only if that same draft still owns the result? Apply that check to both blocked and rejected responses, and prevent stale callbacks from overwriting a newer staged draft or explicit clear. A regression through submit → switch → delayed refusal → submit should carry no old quotes into the new Session.
| appendUserPrompt(state, text, messageId, true); | ||
| requestRender(); | ||
| // Quotes staged by a rewind (#5109) ride this message and only this one: | ||
| // the staging clears as the message dispatches, and a refusal or failure |
There was a problem hiding this comment.
[P2] Let a quote-only replacement reach this submission path
Reachability ①: a Desktop message can contain only QuoteRefs, so rewind now returns an empty prompt with nonempty quotes. The unchanged submitPrompt, steerRunningTurn and Alt+Enter guards still reject empty text before reaching this new quote-forwarding path. A runner probe restoring prompt: '' with one quote shows quotes:1, but Enter produces zero driver submissions; the user must invent extra text to resend the retained content.
Please make the relevant editor/submit guards consider the current draft's quotes as meaningful content, while still rejecting truly empty drafts. Cover a quote-only rewind sent unchanged, and the corresponding empty draft after /quotes clear.
…nly rewinds Two review findings on apache#5265: A failed or blocked admission restaged the rewound quotes tagged with the Session read at callback time, so a Session switch while the admission was in flight attached the old quotes to the next message of the wrong conversation. The originating Session and staging generation are now captured at dispatch, and the restore happens only when neither moved. A quote-only rewind refills an empty prompt, and the empty-text guards in submitPrompt, steerRunningTurn and Alt+Enter rejected it before the quote forwarding path could run. They now treat staged quotes as meaningful content; a cleared plate stays truly empty and keeps refusing. Part of apache#5109 Generated-by: GLM-5.3-Flash (ZCode)
|
Both findings fixed at 6ca7ff9: P1 (wrong-Session restore): the originating Session and a staging generation counter are captured at dispatch. The restore now runs only when the Session is unchanged and no newer staging or explicit clear bumped the generation — a switched Session, a newer rewind, or P2 (quote-only replacement): Regressions: |
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround — the quote-only submit path reads correctly. Two findings on the restage guard, reviewed at 6ca7ff99b.
P1 — restageForRetry can never restore. originGeneration is captured before clearStagedQuotes() runs (pi-tui-runner.ts: the capture sits above the stagedGeneration += 1 inside clearStagedQuotes). By the time a blocked result or a failure lands, stagedGeneration !== originGeneration is always true, so the restore never executes. Reachable on the ordinary failure path: /rewind → submit → admission refused or fails → the staged quotes are silently dropped instead of returning for the retry the feature promises. The new tests can't see it: "restores a failed quote submit only to its originating session" asserts the other session stays clean, which also holds when restage never runs; nothing covers the same-session restore. Smallest fix: read stagedGeneration after clearStagedQuotes(), and add the positive case — same-session failed submit restages the quotes.
P2 — a newer rewind does not invalidate an in-flight restage. The rewind path assigns stagedRewindQuotes/stagedQuotesSessionId directly (~pi-tui-runner.ts:2193) without bumping stagedGeneration, so after the capture order is fixed, a re-rewind landing while an admission is in flight would still be overwritten by the older failure's restage — the exact case the comment lists. Bump the generation on that assignment too (or route both writes through one setter).
Everything else in the delta looks right — session-id capture and the empty-text-with-quotes gates in submitPrompt/steerRunningTurn/handleSubmit cover the paths we discussed.
@me2seeks you know the TUI submit path best — could you sanity-check the restage semantics above?
中文版
在 6ca7ff99b 复核。P1:originGeneration 在 clearStagedQuotes()(内部 stagedGeneration += 1)之前捕获,失败或 blocked 落地时守卫恒真,restageForRetry 永不执行——带 staged quotes 的提交失败后 quotes 静默丢失,不回到草稿供重试。新测试只断言另一会话不继承,restage 不执行时也成立,所以没兜住;最小修法是把 generation 读取挪到 dispatch-clear 之后,并补同会话失败恢复的正例。P2:rewind 重新 stage 的赋值(~pi-tui-runner.ts:2193)不 bump generation,修好顺序后「in-flight 期间新 rewind」仍会被旧失败回调覆写,需在赋值处一并 bump。其余 delta(session 捕获、空文本+quotes 放行)没问题。
AI assistance: I used Devin to re-check the delta against the earlier findings; the assessment is mine.
The restage guard captured stagedGeneration before the dispatch's own clearStagedQuotes(), which bumps the generation, so the guard compared against a pre-clear value and a blocked or failed admission never restored the staged quotes to the draft. Read the generation after the clear instead, and route every staged-quote write through one setter that bumps the generation, so a re-rewind landing while an admission is in flight is never overwritten by the older failure's restage (apache#5109 review). Generated-by: GLM-5.3-Flash (ZCode)
|
Both findings fixed at 399f15e: P1 (restage unreachable): P2 (stale restage overwrites newer staging): all staged-quote writes now route through one Verification: full |
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for pushing this through three review rounds — I re-reviewed the current head (399f15e0) with fresh eyes, focusing on the staging semantics rather than the earlier findings.
What I verified
- Both earlier findings are genuinely fixed.
originGenerationis now read after the dispatch's ownclearStagedQuotes()(pi-tui-runner.ts:1310-1324), and every write routes throughsetStagedQuotes, which bumps the generation (:623-628, used by the rewind path at:2199-2205). So an ordinary failure restages, while a Session switch, a newer rewind, or/quotes clearlanding in flight vetoes the stale callback. All four new runner tests (__tests__/pi-tui-runner.test.ts:6780-7082) exercise exactly those vetoes plus the positive restage case. - Quote-only submits are admissible end to end.
hasMeaningfulMessageContent(packages/core/src/events.ts:186) is the single predicate shared by protocol admission (packages/runtime-host/src/protocol/turn.ts:472-489), storage and compaction, so emptytext+ quotes is legal at every boundary. I also confirmed pi-tui's editor does callonSubmit('')for Enter on an empty buffer (editor.js:submitValue), so the new gates at:1227,:1367,:1385are actually reachable — and thataddToHistorydrops empty input, so no empty history entry is created. - No duplication when the admission outcome is lost.
RuntimeHostMakaSessionDriverImpl#submitMessageswallowsoutcome_unknown/ interrupted-after-dispatch and resolvesundefined, i.e. success, so the staging stays consumed rather than being re-sent on a retry. That's the right call for this feature. - Quotes are the right refs, verbatim, in order.
rewindToTurnreturnspromptMessage.quotesfrom the rewound turn (runtime-host-session-driver.ts:901), the revision copy retains the copied turn ids, and the driver forwards a copy unchanged (:546). A newer rewind replaces (not appends to) the staging, so no cross-turn accumulation. No dedup is needed — the refs come from the Host, which already appliedTURN_MESSAGE_QUOTE_MAX_COUNT/size limits when they were admitted. - Attachments and directory refs still fail closed before any branch exists — the driver test asserts no
session.revision.createfor those carriers (__tests__/runtime-host-session-driver.test.ts:2125-2213), so a quoted and attached turn can't half-rewind. - Repo-wide grep finds no leftover references to the removed
rewind_unsupported_quotescode or copy outside the two changed files; the new/quotesentry is wired through both catalogs with all three locales filled in.
Findings
1. A quoted replacement submit renders as nothing in the TUI transcript. This is the one thing I'd like a decision on. The TUI never renders QuoteRefs: the durable user projection emits message.displayText ?? message.text only (pi-transcript.ts:1086-1096), renderUserBlock returns [] for blank text (:2229-2230), and the pending bar prints Steering: with an empty preview for a queued one (:1972). So for the quote-only flow this PR explicitly enables (:6987), the user sends a message and sees no row for it — just an assistant reply to an invisible prompt, with the quotes:<n> segment gone and /quotes now reporting "No restored quotes are staged", i.e. no remaining evidence of what went out. The desktop already solves both halves: quote chips plus a structured-only branch that avoids an empty bubble (packages/ui/src/chat-turn.tsx:250-275). Rendering the quotes (or at least a · N restored quote(s) hint) in the durable user entry would close the loop; happy for it to be a follow-up, but it's user-visible as-is.
2. nit — the staging is hidden on a Session switch, not invalidated. effectiveStagedQuotes() compares stagedQuotesSessionId to getSessionId() (:615-619), so leaving the branched session drops quotes:<n>, but coming back later (/session, the side-conversation toggle, /resume) silently re-arms the old quotes and the next submit carries them with no notice — potentially many turns later. The comment at :609-611 says switch paths "invalidate" the staging, which isn't quite what the code does. If resurrection isn't intended, bump stagedGeneration (or clear) when the view leaves that session; if it is, the comment and the copy could say so.
3. nit — /quotes clear always reports success. The handler clears and pushes quotesCleared unconditionally (:4296-4302), so it claims "Restored quotes discarded" when nothing was staged, and also when the quotes are currently riding an in-flight submit (which will still carry them). Bare /quotes already distinguishes the empty case with quotesNone; clear could reuse that check.
4. nit — a few cheap coverage gaps.
restageForRetry()is called on thedisposition === 'blocked'branch (:1337) but no test covers it — only the rejected-promise path does. The existingHostSkillDriver(__tests__/pi-tui-runner.test.ts:11545) can refuse, so a quoted rewind plus a refused skill would cover it directly.- Every staging test uses a single quote, so ordering, the
quotes:<n>count, and the/quoteslisting order for >1 excerpt are unverified. A two-quote rewind result would pin all three. /quotesis declaredmidTurn: 'local'but no test runs it mid-turn, which is the disposition most likely to regress silently.
5. nit — a second rewind can dangle the quotes' provenance. Nothing validates sourceTurnId against the branched session, and a revision copy slices before the target turn (packages/runtime-host/src/server/session-revision-coordinator.ts:371-380). Rewinding twice in a row can therefore stage refs whose source turn no longer exists in the new branch. The inline text is intact so nothing is dropped from the model input; only a chip click-through in the desktop may fail to resolve. Not worth changing here — just noting the staging deliberately carries refs it doesn't re-validate.
Net: I found no correctness bug in the staging/restage logic itself — the generation + session-id guards hold up under the interleavings I traced. Item 1 is the one I'd want an explicit answer on; the rest are nits.
Astro-Han
left a comment
There was a problem hiding this comment.
Thanks for pushing this through three review rounds — I re-reviewed the current head (399f15e0) with fresh eyes, focusing on the staging semantics rather than the earlier findings.
What I verified
- Both earlier findings are genuinely fixed.
originGenerationis now read after the dispatch's ownclearStagedQuotes()(pi-tui-runner.ts:1310-1324), and every write routes throughsetStagedQuotes, which bumps the generation (:623-628, used by the rewind path at:2199-2205). So an ordinary failure restages, while a Session switch, a newer rewind, or/quotes clearlanding in flight vetoes the stale callback. All four new runner tests (__tests__/pi-tui-runner.test.ts:6780-7082) exercise exactly those vetoes plus the positive restage case. - Quote-only submits are admissible end to end.
hasMeaningfulMessageContent(packages/core/src/events.ts:186) is the single predicate shared by protocol admission (packages/runtime-host/src/protocol/turn.ts:472-489), storage and compaction, so emptytext+ quotes is legal at every boundary. I also confirmed pi-tui's editor does callonSubmit('')for Enter on an empty buffer (editor.js:submitValue), so the new gates at:1227,:1367,:1385are actually reachable — and thataddToHistorydrops empty input, so no empty history entry is created. - No duplication when the admission outcome is lost.
RuntimeHostMakaSessionDriverImpl#submitMessageswallowsoutcome_unknown/ interrupted-after-dispatch and resolvesundefined, i.e. success, so the staging stays consumed rather than being re-sent on a retry. That's the right call for this feature. - Quotes are the right refs, verbatim, in order.
rewindToTurnreturnspromptMessage.quotesfrom the rewound turn (runtime-host-session-driver.ts:901), the revision copy retains the copied turn ids, and the driver forwards a copy unchanged (:546). A newer rewind replaces (not appends to) the staging, so no cross-turn accumulation. No dedup is needed — the refs come from the Host, which already appliedTURN_MESSAGE_QUOTE_MAX_COUNT/size limits when they were admitted. - Attachments and directory refs still fail closed before any branch exists — the driver test asserts no
session.revision.createfor those carriers (__tests__/runtime-host-session-driver.test.ts:2125-2213), so a quoted and attached turn can't half-rewind. - Repo-wide grep finds no leftover references to the removed
rewind_unsupported_quotescode or copy outside the two changed files; the new/quotesentry is wired through both catalogs with all three locales filled in.
Findings
1. A quoted replacement submit renders as nothing in the TUI transcript. This is the one thing I'd like a decision on. The TUI never renders QuoteRefs: the durable user projection emits message.displayText ?? message.text only (pi-transcript.ts:1086-1096), renderUserBlock returns [] for blank text (:2229-2230), and the pending bar prints Steering: with an empty preview for a queued one (:1972). So for the quote-only flow this PR explicitly enables (:6987), the user sends a message and sees no row for it — just an assistant reply to an invisible prompt, with the quotes:<n> segment gone and /quotes now reporting "No restored quotes are staged", i.e. no remaining evidence of what went out. The desktop already solves both halves: quote chips plus a structured-only branch that avoids an empty bubble (packages/ui/src/chat-turn.tsx:250-275). Rendering the quotes (or at least a · N restored quote(s) hint) in the durable user entry would close the loop; happy for it to be a follow-up, but it's user-visible as-is.
2. nit — the staging is hidden on a Session switch, not invalidated. effectiveStagedQuotes() compares stagedQuotesSessionId to getSessionId() (:615-619), so leaving the branched session drops quotes:<n>, but coming back later (/session, the side-conversation toggle, /resume) silently re-arms the old quotes and the next submit carries them with no notice — potentially many turns later. The comment at :609-611 says switch paths "invalidate" the staging, which isn't quite what the code does. If resurrection isn't intended, bump stagedGeneration (or clear) when the view leaves that session; if it is, the comment and the copy could say so.
3. nit — /quotes clear always reports success. The handler clears and pushes quotesCleared unconditionally (:4296-4302), so it claims "Restored quotes discarded" when nothing was staged, and also when the quotes are currently riding an in-flight submit (which will still carry them). Bare /quotes already distinguishes the empty case with quotesNone; clear could reuse that check.
4. nit — a few cheap coverage gaps.
restageForRetry()is called on thedisposition === 'blocked'branch (:1337) but no test covers it — only the rejected-promise path does. The existingHostSkillDriver(__tests__/pi-tui-runner.test.ts:11545) can refuse, so a quoted rewind plus a refused skill would cover it directly.- Every staging test uses a single quote, so ordering, the
quotes:<n>count, and the/quoteslisting order for >1 excerpt are unverified. A two-quote rewind result would pin all three. /quotesis declaredmidTurn: 'local'but no test runs it mid-turn, which is the disposition most likely to regress silently.
5. nit — a second rewind can dangle the quotes' provenance. Nothing validates sourceTurnId against the branched session, and a revision copy slices before the target turn (packages/runtime-host/src/server/session-revision-coordinator.ts:371-380). Rewinding twice in a row can therefore stage refs whose source turn no longer exists in the new branch. The inline text is intact so nothing is dropped from the model input; only a chip click-through in the desktop may fail to resolve. Not worth changing here — just noting the staging deliberately carries refs it doesn't re-validate.
Net: I found no correctness bug in the staging/restage logic itself — the generation + session-id guards hold up under the interleavings I traced. Item 1 is the one I'd want an explicit answer on; the rest are nits.
… transcript Third-round review items on the rewind quote staging: - A session change now clears the staged quotes outright instead of only hiding them while the user is elsewhere: keying alone let a silent resurrection re-arm the quotes on return, potentially many turns later. The rewind re-stages its own quotes after the switch settles. - /quotes clear distinguishes the nothing-staged case (including quotes that already left on an in-flight submit) instead of always claiming a discard. - A quote-only submit stored no text, so the replacement message left no trace in the transcript — an answer to an invisible prompt. The durable user entry now carries the restored-quote count and renders a trace line for it. - Coverage: the blocked-disposition restage, two-quote ordering across the status line, /quotes listing, and the submit, and /quotes routing mid-turn. Generated-by: GLM-5.3-Flash (ZCode)
|
Addressed everything actionable at Item 1 (quote-only submit renders as nothing) — implemented now, not deferred. The durable user entry carries the restored-quote count and renders a trace line: blank text renders Item 2 (hidden vs invalidated) — the staging is now invalidated. Item 3 (clear always reports success) — fixed. Item 4 (coverage) — all three gaps closed:
Item 5 (dangling provenance after a second rewind) — agreed, no change here: the refs are carried deliberately un-revalidated, and the desktop chip click-through is the place that resolves them. Verification: full |
# Conflicts: # packages/cli/src/session-driver.ts
The runtime-host PTY close-wait timeout fired on a merge head whose runtime-host tree is identical to green upstream; the PR's delta is confined to packages/cli. Local pi-tui + transcript suites pass on the merge head.
|
Gentle ping — everything actionable from your 09-17 review landed at |
me2seeks
left a comment
There was a problem hiding this comment.
Review(对抗式复核,head a127b13ed)
方向认可:把 rewind 掉的 turn 的 QuoteRef 原样返回并转发进替换 submit,而不是 fail-closed(rewind_unsupported_quotes),同时删掉死代码和对应文案,这是干净的做法。前几轮 review 的 P1(generation 读取顺序)和 switch 失效化确实已修好,我复核确认。
不过还有 1 个 P1 和 1 个 P2 需要处理。
P1 — outcome_unknown 路径下 staged quotes 会被静默吞掉,且永不 restage
restageForRetry() 只在两个分支被调用(packages/cli/src/pi-tui-runner.ts):
.then里result?.disposition === 'blocked'.catch里
但真实 driver 在「投递结果未知」时不抛异常,而是 resolve undefined(packages/cli/src/runtime-host-session-driver.ts:551-557):
} catch (error) {
if (
(error instanceof RuntimeHostOperationError && error.code === 'outcome_unknown') ||
(error instanceof RuntimeHostRequestInterruptedError && error.dispatch === 'dispatched')
) {
return undefined; // 注意:resolve,不是 reject
}
throw error;
}#admit(:1271)把这个 undefined 原样透传。于是对于 outcome_unknown / interrupted-after-dispatch 的 submit:
.then里result?.disposition是undefined、if (result)为 false → 不 restage;.catch不触发 → 不 restage。
而 staging 在 dispatch 时已经被 clearStagedQuotes() 清掉,所以 quotes 被静默消费、永久丢失。
这里需要纠正上一轮 review 的一个判断。上一轮把这条路径读成了「resolve undefined,i.e. success,所以 staging 保持消费是正确的」。但按 RuntimeHostRequestInterruptedError 自己的文案(packages/runtime-host/src/client/connection.ts:328-330),dispatch === 'dispatched' 的含义是:
'the operation outcome is unknown; do not retry it automatically'
message-coordinator.ts:1236 的 outcome_unknown 同样是「Message disposition cannot be proven in this Host Epoch」——结果无法证明,而不是「已确认接纳」。所以这不是一个「有意保持消费」的设计,而是一个未被识别的缺口:恰恰在「消息可能没被接纳」时,把上下文静默丢掉了。
需要说明的是,这里存在一个真实的两难:如果消息其实已经被接纳,restage 会导致重试时重复发送 quotes。但当前代码既没有注释说明这是有意为之,也没有测试覆盖这条路径。建议明确决策并写清理由(例如「outcome_unknown 视为已接纳,故不 restage,避免重复」),或者按语义选择 restage 并说明重复风险。
测试盲区:所有新测试用的 HeldSubmitQuotedDriver.submitMessage 都是 reject(packages/cli/src/__tests__/pi-tui-runner.test.ts 的 new Promise((_, reject) => ...)),没有任何用例走 resolve-undefined 这条真实路径。
P2 — 任意带 quotes 的用户消息都会被标成「restored quote」,不只是 rewind 恢复的
packages/cli/src/pi-transcript.ts(本 PR 新增):
const restoredQuotes = message.quotes?.length;
entries.push({
kind: 'user',
messageId: message.id,
text: message.displayText ?? message.text,
...(restoredQuotes ? { quotes: restoredQuotes } : {}),
});渲染时无条件加标签:
const hint = `· ${entry.quotes} restored quote${entry.quotes === 1 ? '' : 's'}`;问题是 StoredMessage.quotes 并不是「rewind 恢复」专属字段——桌面端普通引用消息也写入同一个字段,并且会进入 TUI 渲染的同一份 durable transcript:
apps/desktop/src/renderer/features/workbar/tools/side-chat/use-quote-companion.ts:1238(普通引用提交)apps/desktop/src/main/session-local-service.ts:198(把content.quotes投影回本地消息)- 桌面端 edit & resend 的 restage 路径(#5274)同样会写入该字段
而 TUI 在打开/切换会话时会走 applySwitchResult → replaceTranscript(messages) → storedMessagesToTranscriptEntries(pi-tui-runner.ts:1878、pi-transcript.ts:422),所以任何带引用的用户消息都会显示「· N restored quotes」,事实错误。
本 PR 自己的测试也把这个错误固化了:pi-transcript.test.ts 里 message-2 是 text: 'with words' + 一个 quote,断言输出 · 1 restored quote。而 packages/ui 对同一字段的中性投影是「Inline quoted excerpts」(packages/ui/src/materialize.ts:62),两边语义已经分叉。
建议:把标签改成中性(例如「· N quote(s)」),或者给 entry 增加一个真正的「restored」标记(由 rewind 路径显式设置),而不是用「有没有 quotes」来推断。
已确认没问题的点
- 删除的代码没有残留引用:全仓 grep 无
rewind_unsupported_quotes/unsupportedQuotes残留。 - quote-only submit 链路通:
hasMeaningfulMessageContent是 protocol admission / 存储 / compaction 共用的单一谓词,空 text + quotes 在各边界都合法;/quotes三个 locale(en / zh-CN / zh-TW)文案齐全。 /quotes解析:parts = trimmed.split(/\s+/),/quotes、/quotes clear、其它形式走 usage 提示,与文件内其它命令(如/host)一致;midTurn: 'local'是合法取值。/new路径:newSession不调用clearStagedQuotes(),但startNewSession会把#sessionId置 null,effectiveStagedQuotes()因 session 不匹配返回[],且之后任何切回都会经applySwitchResult清空——不会 resurrect,属于注释措辞不够精确,不是 active bug。
小结:P1 建议在合并前明确决策并补测试;P2 建议改中性标签。其余为已澄清项。
…the quote trace Two findings from the adversarial re-review at a127b13: - An outcome_unknown submit (or an interruption after the dispatch went out) resolves without a receipt, and the dispatch had already consumed the quote staging, so the quotes vanished silently with no restage and no test coverage. Restage them when the receipt is absent and surface a notice naming the uncertainty: admission cannot be proven either way, and losing the user's explicit context to an unproven outcome is worse than a visible duplicate ride (status line shows the restore; /quotes clear discards it). - The durable user-entry trace said "restored quote(s)" for every message carrying quotes, but StoredMessage.quotes also carries plain desktop quotes (including edit-restaged ones), so any quoted message resurfaced in the TUI mislabeled itself as rewind-restored. Word the trace neutrally ("· N quote(s)"). New UnknownOutcomeSubmitDriver pins the resolve-undefined path the previous tests only exercised through rejection. Generated-by: GLM-5.3-Flash (ZCode)
|
Thanks for the adversarial re-read — both findings confirmed and fixed on P1 — outcome_unknown restage. Confirmed: the real driver resolves P2 — neutral trace. Also confirmed: Tests: new |
…talog The notice introduced for the unknown submit outcome was a visible literal in pi-tui-runner, which check:tui-copy correctly rejects — TUI copy must go through the localized catalog. Add quotesRestoredUnknown to the rewind catalog (en / zh-CN / zh-TW) and reference it. Generated-by: GLM-5.3-Flash (ZCode)
|
Follow-up on |
Generated-by: GLM-5.3-Flash (ZCode)
|
Gentle ping — the head is |
hqhq1025
left a comment
There was a problem hiding this comment.
This change now carries rewound QuoteRefs through the CLI driver and into the replacement submit, accepts quote-only submits, and uses neutral wording in the durable transcript. I found one remaining failure-ordering issue below. The current-head test check passed, but I did not run local TUI tests or a cross-surface smoke test. The earlier CHANGES_REQUESTED review is attached to an older commit, not this head.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
| const restageForRetry = (): boolean => { | ||
| if (!staged.length) return false; | ||
| if (input.driver.getSessionId() !== originSessionId) return false; | ||
| if (stagedGeneration !== originGeneration) return false; |
There was a problem hiding this comment.
P2: The generation only changes when staging is written. After a quoted submit clears staging, a second ordinary submit in the same session does not advance it; /quotes clear also returns early while staging is empty (lines 4695-4705). If the first admission then fails or resolves without a receipt, this callback restages its old QuoteRefs, so a later unrelated message silently carries the earlier quote despite the intervening user intent. Invalidate the pending restoration when an ordinary submit or explicit clear supersedes it, and test both orderings with a held admission.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
There was a problem hiding this comment.
Thank you — the ordering you describe is real in the failure callback's guard, and both supersessions are now advanced explicitly: an ordinary quote-less submit and an empty-staging /quotes clear each call a new supersedePendingRestage() (a generation bump without a staging write), so the callback refuses. Pushed at b261e8dd2.
One finding from trying to write the two regression tests you asked for: through the public TUI harness the interleaving is currently unreachable, because the runner parks user input for the whole in-flight window — submitPrompt returns through restoreDraft when busy, and runControl refuses nested actions while one is running — so no ordinary submit or command can land between the dispatch and the settlement that arms the callback. Both orderings deadlock waiting for a driver call that the busy gate prevents. The invalidation is still worth having (the guard now holds by construction rather than by the busy gate staying where it is, and this file's routing is actively being reworked upstream), but a black-box regression for these two orderings would first need a driver that starts a Turn without ever resolving its admission receipt — the existing HeldSubmitQuotedDriver blocks the runner before a second submit can be attempted. If you'd like, I can follow up with that harness work as its own change.
|
Follow-up on the intermediate red: the first push ( |
hqhq1025
left a comment
There was a problem hiding this comment.
The new handoff guard and interrupt-toggle gate cover the two entry-order races, and the focused tests pass. However, the successful-retraction/late-switch path still loses the user’s retracted content (inline finding). I cannot treat a loss notification as recovery. The current hosted test is green and the fresh-main merge tree is clean, but this P2 blocks readiness. I did not run a real Host/TUI side-conversation race or the full local suite.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
| state.entries.push({ | ||
| kind: 'notice', | ||
| level: 'error', | ||
| text: `The session switched while the retraction was in flight${counts ? `: ${counts} came back` : ''}, but the retracted text could not be restored to the editor.`, |
There was a problem hiding this comment.
P2: Keep the retracted content recoverable when the switch wins. retractQueued() has already removed the queued message from the Host, but this branch reports only counts and discards retracted.text and retracted.quotes. Switching back cannot restore the queued draft. The new test checks that a loss notice appears, not that the data survives. Serialize navigation behind an in-flight retraction, or retain the payload in a draft owned by the original session; a notice alone does not fix the data loss.
Astro-Han
left a comment
There was a problem hiding this comment.
A second, independent review of head a72756ce (Claude lineage). It adds to the hqhq1025 review 5334255733 and does not repeat its findings.
What's fixed. Mutation checks confirm three fixes; for each, removing the change makes its new test fail:
- the
detachingentry guard (pi-tui-runner.ts:1543); - the late-landing notice (
:1557-1577); - the interrupt-toggle gate (
:2199), which also covers the earlier Esc Esc sub-issue.
P2: a retraction in flight, followed by a side switch, loses the queued text without any notice. We reproduced this with a temporary test. It is a different path from the one in the review above:
- Alt+↑ starts a retraction.
- The side toggle at
:2199doesn't check whether a retraction is in flight, so the switch proceeds. - The driver changes its session ID only at the end of
switchSession(runtime-host-session-driver.ts:831). Both fences at:1548-1579therefore pass, and the retracted text goes into the editor. switchView(:2202-2213) then overwrites the editor with the draft it saved before the retraction landed, andapplySwitchResultclears the quotes.
The messages are gone from the Host queue, they're in neither draft, and no notice appears. A fix: add a retracting flag and have the toggle and the mid-turn /session branch refuse or wait while it is set, as they do for interruptRequested.
P3:
- An empty retraction that lands after a switch still shows the error notice (
:1557-1577). It should return quietly when there are no messages and no quotes. - As the review above says, the notice gives only counts, so the text can't be recovered. Carrying the text in the notice, or keeping a per-session draft, would fix this.
Tests: pi-tui-runner 240/240 and runtime-host-session-driver 78/78 pass, and the TUI copy check passes. Not run: the full suite, and a real Host/TUI race.
Automated review (Claude lineage) by the Qronos review line on behalf of @Astro-Han; please verify before acting.
| // session until it settles: toggling away mid-handoff would drop the | ||
| // retracted queue on the fence and leave the stop request aimed at a | ||
| // session the TUI no longer watches (#5109 review). | ||
| if (!pair || detaching || interruptRequested || (busy && !turnRunning)) return; |
There was a problem hiding this comment.
P2: this gate covers detaching and interruptRequested but not a retraction that is still in flight. If Alt+↑ is followed by this toggle, the retracted text lands in the editor and is then overwritten by the draft that switchView saved earlier, with no notice. Consider a retracting flag checked here and in the mid-turn /session branch.
|
Both findings are addressed on head
Tests: the two new cases fail on If the fresh |
|
Merged latest |
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed commit 0313ab5. The new retracting guard prevents a single in-flight Alt+Up retraction from racing the side toggle and the mid-turn /session switch. The new test holds the retraction response, attempts Ctrl+/, then checks that salvaged text reaches the editor (pi-tui-runner.test.ts:8486-8542); the two focused new tests pass locally after a CLI build.
The prior content-loss issue is not fully resolved; one remaining switch path is identified inline. Current-head hosted test passed, and merge-tree/diff-check against fresh main de4fc5f are clean. I did not run the full suite or a real Host/TUI concurrency trace.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
| // flight is refused for the same reason: the driver re-keys at the end of | ||
| // a switch, so the fences would pass and the retracted text would be | ||
| // overwritten by the stale draft (#5109 review). | ||
| if (detaching || retracting) return; |
There was a problem hiding this comment.
[P2] Guard the idle /session path while a retraction is pending too. The new retracting check only runs after if (!turnRunning): that branch immediately calls switchSession(sessionId) at lines 2251-2255. A user can start Alt+Up while a turn is running, let the turn finish before the Host's retraction response returns, then run /session <other>; the idle branch re-keys the driver despite retracting === true. When the Host response arrives, the fence at lines 1563-1583 reports counts but discards the already-retracted text and quotes, leaving nothing in the old session queue. This is the previous loss mode through an unguarded navigation path. The new test exercises only Ctrl+/ while the turn remains active; add a deterministic delayed-response test that transitions to idle and invokes /session outside the side pair. Also make the flag reentrancy-safe: Alt+Up currently has no retracting admission check at line 1547, so one completed request can clear the boolean while another remains in flight.
|
Both findings addressed on head
Both new tests fail on |
me2seeks
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent operated by me2seeks make. It is not an independent human review and does not replace one.
Summary
Fixes the CLI losing a rewound turn's quotes: rewind-restored quotes are now staged for the next submit, keyed to the session they were restored in (stagedQuotesSessionId), cleared outright on every session switch, and kept across a refused/failed submit for retry. The race fencing is the strong part: a monotonically increasing stagedGeneration is captured at dispatch, and an in-flight submit's failure callback only re-arms quotes if no write landed since — so a stale callback can't resurrect quotes after /quotes clear or a quote-free submit (both advance the generation without touching the staging pair, as the comment documents). Copy catalog gains quotesRestored/quotesRestoredUnknown/quotesCleared/quotesNone/quotesUsage/quotesListHeading in place of the single unsupportedQuotes. CI test green.
Findings
- [P3]
effectiveStagedQuotes()readsinput.driver.getSessionId()at render time; if a session switch is in-flight (driver already re-pointed butapplySwitchResultnot yet run), the stale staging could render once for the new session before clearing. The generation fence prevents the wrong quotes from submitting, so the blast radius is one transient render — acceptable, worth a comment.
Verdict
merge-ready — careful session-keyed staging with generation-fenced callbacks; the two rounds of #5109 review feedback are visibly addressed in the design comments.
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed commit 78903d5. The new admission check prevents overlapping Alt+Up retractions (pi-tui-runner.ts:1542-1551), and the idle /session test now distinguishes the previous direct-switch gap (pi-tui-runner.test.ts:8538-8623). A new asynchronous ordering gap remains, described inline.
Node 24 CLI build and three focused retraction tests passed locally. The current-head hosted test passed, and merge-tree/diff-check against fresh main de4fc5f are clean. I did not run the full suite or a real Host/TUI trace; the inline scenario is a control-flow finding involving a delayed activity lease.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
| // still out would land after that re-key: the fence would report counts | ||
| // and discard what the Host already took back. Refuse until it settles | ||
| // (#5109 review). | ||
| if (retracting) return; |
There was a problem hiding this comment.
[P2] Recheck retracting after the activity lease wait, before re-keying. This check happens before runControl, but runControl awaits activities.acquire(sessionId) at lines 1152-1155 before invoking switchSession; an existing session activity can keep that wait pending. During the wait, Alt+Up is still handled by the root key handler (:5058-5065) and retractQueuedMessages has no busy guard (:1542-1551). Sequence: idle /session other passes this check and waits for a held activity lease; Alt+Up starts a Host retraction; the lease is released and the queued switch re-keys the driver; the retraction response then hits the switched-session fence and discards the text/quotes already removed from the Host queue. The new idle test starts Alt+Up before /session, so it does not cover this reversed order. Put the guard at the actual switch boundary (or serialize retraction with the control lease), and test a deliberately held activity lease plus Alt+Up before releasing it.
Fifth-round review (apache#5109 review): the retraction fence discarded the retracted payload whenever the driver re-keyed mid-flight, and every switch entry point had its own gap — the idle `/session` path, the activity-lease wait inside `runControl`, the detach toggle, and Alt+Up's own `settlePendingEnqueues` wait all allowed a switch to land between the Host retraction and its response. Serialize instead: both retraction paths register on a retraction task set, and `switchSession`/`switchAwayMidTurn` await `settleRetractions()` at their entry. The retracted text and quotes therefore always land in the session they were asked for (via the existing acceptRetraction restore) before any switch re-keys the driver; the parked-enqueue regression now fires Alt+Up while the enqueue is held, so it exercises the real race, and a new regression pins the switch-behind-retraction ordering. Generated-by: GLM-5.3-Flash (ZCode)
78903d5 to
f0b31c9
Compare
|
All six addressed with one mechanism — head
Full pi-tui-runner suite green locally (236/238 with only the pre-existing Windows SIGTERM pair); CI running on this head. |
Sixth-round review (apache#5265 review) flagged the idle `/session` path: the retraction guard ran before `runControl`, but `runControl` parks on `activities.acquire` before reaching the switch, and Alt+Up stays live during that wait - a retraction started there could land after the released lease let the switch re-key, and the switched-session fence would discard the text and quotes the Host had already taken back. The serialization at the switch boundary (`switchSession` awaits `settleRetractions()` before re-keying) already closes that race on this head and supersedes the reviewed branch's flag-based guard. Pin the reversed order with a regression: hold the session's activity lease, start the idle `/session` so `runControl` parks on the lease, fire Alt+Up while it waits, then release - the retracted text and quotes must reach the editor before the switch re-keys. With the boundary await removed the test fails (the switch re-keys before the retraction lands, `switch:...` then `retract-done:...`); with it, the four focused retraction tests pass individually. Generated-by: GLM-5.3-Flash (ZCode)
|
Confirmed the race on the reviewed commit (78903d5): the idle This head supersedes the flag-based guard reviewed at 78903d5 with the reviewer's second option: serialization at the actual switch boundary. Both retraction paths register on a pending-retraction task set, and Added the regression in the reversed order the review asked for (a16a934): hold the session's activity lease, start the idle Red/green evidence: with the New head: a16a934 |
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed exact head a16a934d7e8479cbac82f91d410161a1f28da647. The new test covers Alt+Up while the idle /session command is still waiting for an activity lease, and the existing boundary wait orders that specific case correctly. One P2 race remains, attached inline: Alt+Up can start after that wait returns but while the driver's asynchronous switch is in progress, so a Host retraction can be discarded after changing sessions. This remains a blocker for the claimed no-loss behavior.
Node 24 clean npm ci, CLI/dependency build, and 4 focused retraction tests pass. Hosted test is green; static merge-tree against current main 2f322055 and git diff --check are clean. I traced the TUI key handler, both retraction fences, the switch boundary, and the Runtime Host driver's request/re-key order. I did not run a real Host/TUI race or full suite; the current fake switches immediately and does not model the in-progress switch window. No schema/migration change is in the PR. This is not a merge approval.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
| // A session switch waits for an in-flight retraction: the retracted text | ||
| // and quotes must land in the session they were asked for before the | ||
| // driver re-keys (#5109 review). | ||
| await settleRetractions(); |
There was a problem hiding this comment.
[P2] Keep retraction and switch mutually exclusive through the driver's re-key, not only before it. This await drains retractions already pending, but input.driver.switchSession() is asynchronous and Alt+Up remains active while runControl is busy (:5028-5035). After this await returns, a user can press Alt+Up while the driver is still fetching/opening the target session; retractQueued() sends queue.retract for the old session (runtime-host-session-driver.ts:615-620). If the Host removes the queued message and the driver then sets its new session ID (:822-831) before the retract response arrives, the :1567 session fence discards the returned text and quotes. The new lease-wait test starts Alt+Up before this await and its fake switch is immediate, so it cannot catch this order. Block new retractions during the entire switch, or serialize the switch and retraction under one lock.
There was a problem hiding this comment.
Confirmed - the settleRetractions() drain only orders retractions that started before the switch, and the driver switchSession() does real asynchronous work (stopping user commands, opening the target Session channel) before it re-keys, so an Alt+Up pressed in that window still addresses the old Session and the switched-session fence then discards what the Host removed.
Fixed on the first suggested axis: new retractions are blocked for the entire switch. Every runner-level switch path - idle /session, the mid-turn detach, and rewind (its driver call re-keys the same way) - now runs inside a switch window (a counter, so the paths compose), and retractQueuedMessages drops the keypress while a window is open. Dropping it loses nothing: the entries stay queued on the Host, and Alt+Up after the switch lands retracts from the session that is then current. Blocking over a lock, because pairing a retraction-side wait with the drain re-check loop could otherwise cycle (switch waits retraction, retraction waits switch); a blocking check is order-free.
The new regression test models the in-progress window the previous test could not: the fake driver now parks inside switchSession before its re-key. On the previous head the test fails with a queue.retract crossing the window (retractCalls 1 !== 0); with the gate it passes (queue untouched, no retract-done), and removing the gate alone makes it fail again (ablation). The four previous focused retraction tests and the rewind in-flight tests pass individually; the full runner suite is 241 tests with only the two known pre-existing SIGTERM failures.
Head: b48832d.
…window Seventh-round review (apache#5265 review) flagged that the idle switch's `settleRetractions()` drain only covers retractions asked for before the switch starts. `driver.switchSession()` is asynchronous - it stops user commands and opens the target Session channel before it re-keys - and Alt+Up stays live through that whole window. A retraction pressed there captures the old session id, and when the driver re-keys before the retract response arrives, the switched-session fence discards the returned text and quotes after the Host already removed the queued entries: a real loss. Count every runner-level switch (idle `/session`, mid-turn detach, and rewind's branch-and-switch) in a switch window and make `retractQueuedMessages` drop keypresses while one is open. Swallowing the keypress loses nothing: the entries stay queued on the Host and Alt+Up works once the switch lands, retracting from the session that is then current. The regression test parks the fake driver inside `switchSession`, before its re-key, and fails on the previous head with a `queue.retract` crossing the window (`retractCalls` 1 !== 0); it passes with the gate and fails again with the gate removed (ablation). The four previous focused retraction tests, the rewind in-flight tests, and the full runner suite (241 tests, only the two known pre-existing SIGTERM failures) pass individually. Generated-by: GLM-5.3-Flash (ZCode)
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed exact head b48832dbf714bb4d8454677e1238e4203dcf0d7a. The new switch-window guard closes the previously reported /session race, and its focused regression passes.
[P2] The same Runtime Host re-key remains unguarded through the side-conversation APIs. /side can run while a Turn has queued follow-ups, but packages/cli/src/pi-tui-runner.ts:2302-2305 calls driver.openSideConversation() directly; the Runtime Host implementation eventually calls switchSession(sideSessionId) at packages/cli/src/runtime-host-session-driver.ts:944-975 without incrementing sessionSwitchesInFlight. Alt+Up therefore remains live at pi-tui-runner.ts:5067-5074. If queue.retract removes the old Session's entries while that internal switch re-keys, the response-time session fence at :1591 drops the returned text and quotes, reproducing the same silent input loss this commit fixes for /session. closeSideConversation() has the same direct re-key at :2343-2347. Please wrap these switch-owning operations in the same window and add a delayed open/close plus Alt+Up regression.
Node 24 clean npm ci, build:test, and three focused retraction/switch tests passed. Hosted test is green, git diff --check passes, and the merge tree against current main 03237142 is clean. I did not run a real Host/TUI timing reproduction, the complete test suite, or native Windows/macOS.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
The side-conversation open and close re-key the driver through the same internal switchSession path as `/session` (Runtime Host openSideConversation switches onto the forked side Session, closeSideConversation switches back onto the parent), but neither entered the sessionSwitchesInFlight window, so Alt+Up stayed live across the re-key. A retraction asked inside that window removes the parent's queued entries via queue.retract while the driver is re-keying, and the response-time session fence then discards the returned text and quotes — the same silent input loss already fixed for `/session`, mid-turn detach, and rewind at the current head. Hold the same window around both switch-owning operations: the open wraps its adopt (both the idle runControl path and the mid-turn detach path) and the close wraps its whole body behind the existing closeSideConversation entry points. The window swallows the keypress without loss: the entries stay on the Host queue and Alt+Up works once the re-key lands. Regression tests park a gated fake driver inside the open's and close's re-key, fire Alt+Up there, and assert no queue.retract crosses (retractCalls stays 0) while the queued entry survives untouched. Both fail on the unfixed head (retractCalls 1 !== 0) and pass with the guard; removing only the two new holdSwitchWindow wrappers brings both back to red. Generated-by: GLM-5.3-Flash (ZCode)
|
Pushed What's guarded (on top of the three paths the previous commit already covered):
Class sweep, not just the reported instance — every runner-level operation that ends in a driver re-key: idle Regression: two tests with a gate-holding driver ( Honest scope: focused suites ran locally; the full runner suite and the real-driver end-to-end window ride on CI. |
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed exact head 0ac4a7ef9c1f09563ab101116d858d9667bc6507.
The new open/close guards do block Alt+Up after the side-conversation re-key has entered its switch window, and both added regressions discriminate those two wrappers. However, an already-started retraction is still allowed to cross the re-key; see the inline P2.
Validation: clean npm ci; build:test; full typecheck, lint, format, ASF header, Windows inventory, and diff checks; focused side-open/close tests 2/2; complete CLI suite 1,356 passed / 3 skipped. Removing only the new open/close wrappers made both added tests fail with retractCalls 1 instead of 0. A separate temporary production-runner probe that started Alt+Up before /side reproduced the remaining race on this head. Hosted test is green. Current main is 19 commits ahead of the PR while the PR is 22 commits ahead of main; git merge-tree --write-tree completed without conflicts.
I did not exercise an interactive terminal against a separately running real Runtime Host, packaged Electron, or native Windows/macOS.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
| detaching = true; | ||
| try { | ||
| await adopt(); | ||
| await holdSwitchWindow(adopt); |
There was a problem hiding this comment.
[P2] Drain retractions that started before entering the side switch window
holdSwitchWindow only prevents new Alt+Up requests after its counter is incremented. It does not wait for a retraction already present in pendingRetractionTasks. This remains reachable: queue a follow-up, gate retractQueued(), press Alt+Up, then run /side. On this head the driver re-keys to the side Session before the retraction resolves; the Host-side retract removes the parent queue entry, and the response-time session fence at pi-tui-runner.ts:1591 then drops the returned text and quotes.
I reproduced that ordering with a temporary test through the real TUI key/command path: the session became side-1 while the retract gate was still closed. The ordinary /session path avoids the same loss by awaiting settleRetractions() inside its held window (pi-tui-runner.ts:2065). Please drain already-started retractions inside the held window before both side open and side close begin their driver re-key, and add coverage where Alt+Up starts before /side/close rather than only during them.
There was a problem hiding this comment.
Pushed 0f26b77c7 — all three re-key paths now drain before switching.
/sideopen:await settleRetractions()at the top of the sharedadopt, while the driver still points at the parent./sideclose: same drain at the top ofrunCloseSideConversation— an in-flight retraction lands while the driver still points at the side Session, before the re-key onto the parent.- Rewind had the same hole and is fixed in the same commit:
rewindToTurnre-keys onto a freshly minted branch Session, so a retraction already in flight when the window opened would hit the same fence drop; it now drains inside the held window first. Reachability checked: rewind only requires idle, and a pending retraction holds nobusygate.
Coverage follows the ordering you called out — Alt+Up starts before the switch, not during it. Four new tests with a retract-gate driver (open idle / open mid-turn / close / rewind; the rewind variant mints a fresh branch id per rewind, matching the real driver). All four fail on the previous head with exactly the loss ordering described (open-start:session-branch, open:side-1, close-start:side-1, close:session-branch, rewind 2 !== 1), pass with the drains, and fail again with only the three settle calls removed (ablation). Focused retraction/switch/side set 29/29.
Honest scope: reproduced through the real TUI key/command harness with a gated fake driver on the same event order, not against a live Runtime Host connection; the two SIGTERM failures in the full runner file are pre-existing (re-verified against the unfixed baseline).
holdSwitchWindow only blocks Alt+Up requests made inside a switch window; a retraction already in flight when /side, a side close, or a rewind started could still resolve after the driver re-keyed. The Host removes the queued entries as the retraction resolves, and the response-time session fence then discards the returned text and quotes - reachable by queuing a follow-up, pressing Alt+Up (retraction pending), and running the command before it settles; the reviewer reproduced the loss through real TUI key/command paths (apache#5265 review, P2). Copy the `/session` pattern (drain pendingRetractionTasks inside the held window, before the driver re-key starts) into the three remaining re-keying paths: the side open's adopt (idle runControl path and mid-turn detach path alike), the side close's runCloseSideConversation, and the rewind's branch-and-switch. Rewind had the same disease: every rewind re-keys onto a fresh branch Session, so a pending retraction crosses a session change and the branch fence discards its response exactly like the side fences. Tests park a gated retraction on the fake driver's Host call, start the open / close / rewind behind it, and assert the re-key waits: retract-done lands before the re-key and the retracted text reaches the editor. The rewind driver mints a fresh branch id per rewind so the second rewind crosses a session change the way the real driver does. All four tests fail on the unfixed runner (re-key observed while the retraction is still pending), pass with the fix, and fail again with only the three settleRetractions calls removed. Generated-by: GLM-5.3-Flash (ZCode)
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed exact head 0f26b77c7f24c4d554beaa22abf6f8a0d5c0106d. The new drains correctly order the Host retraction before each re-key, and all four focused regressions pass. However, the restored payload is still mishandled after the drain on both Side Chat transitions; see the two inline P2 findings.
Validation: Node 24.18.1 build:test; full typecheck, lint, format, ASF header, Windows inventory, and diff checks; focused new regressions 4/4; complete CLI suite 1,360 passed / 3 skipped. Two temporary production-runner assertions reproduced the remaining behavior: the parent text was visible in the new side editor, and the side text was absent after close. Both diagnostic edits were removed and the exact-head tests were rebuilt and rerun. Hosted test is green. The PR is 23 commits ahead / 20 behind current main 28cc4e64; git merge-tree --write-tree completed without conflicts.
I did not exercise a separately running real Runtime Host through an interactive terminal, packaged Electron, or native Windows/macOS.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
| // the retraction resolves, and a response landing after the re-key onto | ||
| // the side Session would be discarded by the side-session fence (#5265 | ||
| // review). | ||
| await settleRetractions(); |
There was a problem hiding this comment.
[P2] Keep the recovered parent draft out of the new side editor. Once this await resolves, acceptRetraction() has put the parent's retracted text into the shared editor. The code then re-keys to the side Session and records that text as parentDraft, but never replaces the editor with the empty sideDraft. The existing new test actually requires queued resend to remain visible after getSessionId() becomes side-1, so pressing Enter can submit the parent's recovered message in the side conversation and the same text is retained for the parent as well. This contradicts the established per-view draft behavior in Ctrl+/ toggles side views, preserves drafts. A temporary assertion that the new side editor was empty failed with actual value queued resend. Capture the parent draft and switch the editor to the side draft (or cancel the open) after the drain, and make the regression assert both sides' draft ownership.
There was a problem hiding this comment.
Fixed on bd25e82fc: after the drain, adopt captures the recovered parent text as parentDraft and now switches the editor to the side view's own (empty) draft, so Enter in the side conversation can no longer resubmit the parent's message and Ctrl+/ restores the parent text from parentDraft. The idle-open regression now asserts the side editor opens empty and that a close round trip brings queued resend back in the parent editor; the mid-turn variant asserts the empty side editor. Both failed on the previous head with exactly your reproduction (queued resend visible in the new side editor).
| // active must land while the driver still points at it — past the re-key | ||
| // onto the parent, the close's session fence discards the text and quotes | ||
| // the Host already removed from the side queue (#5265 review). | ||
| await settleRetractions(); |
There was a problem hiding this comment.
[P2] Re-check or preserve the side draft after this drain. The Ctrl+C close guard sees an empty editor before the pending retraction finishes; this await then restores side follow-up into the editor, but editor.setText(pair.parentDraft) below overwrites it and the close removes the side conversation. The new close test only checks ordering and the final Session id, so it passes while the retracted message is still lost. A temporary assertion that the recovered text remained visible after close failed. Please either abort the close once the drain produces a non-empty draft, or persist/transfer that recovered content before replacing the editor, and assert the final user-visible draft in the regression.
There was a problem hiding this comment.
Fixed on bd25e82fc by aborting the close: the close is only admitted from an empty draft, so a non-empty editor after the drain means the drain (or typing under it) restored content — the close now returns early with a notice ("Side conversation kept open — the retracted message was restored to the draft.", registered in the TUI copy inventory) instead of overwriting the recovered text with parentDraft. A later Ctrl+C clears the restored draft like any draft, and the next close goes through. The close regression now asserts the abort: no driver close call, session stays on the side, side follow-up remains visible in the editor, and the notice renders; it failed on the previous head where the text was overwritten and lost.
Eighth-round review (apache#5265 review) confirmed the drains order the Host retraction before each re-key but flagged two inline P2s in what happens to the restored payload afterwards: - Side open: after the drain, acceptRetraction() has already put the parent's recovered text into the shared editor; the open then recorded it as parentDraft but never swapped the editor to the side view's own (empty) draft, so the parent's message sat in the side editor where Enter would resubmit it inside the side conversation. The open now switches the editor to the side draft after capturing parentDraft, per the established per-view draft contract. - Side close: the Ctrl+C close guard admits the close against an empty editor, and the drain then restores the side's queued text into it — which the following editor.setText(parentDraft) overwrote, losing the recovered message. The close now aborts with a notice when the editor is non-empty after the drain: closing is only admitted from an empty draft, so anything there came from the drain (or typing under it), and a later Ctrl+C clears it like any draft. Both behaviors were red under the reviewer's own reproductions (parent text visible in the new side editor; side text absent after close) and green with the guards; the three updated regressions also failed with only the guards removed. Full runner suite 245/247 - the two failures are the known pre-existing SIGTERM tests. check:tui-copy (new notice literal registered) and check:locale-hygiene pass. Generated-by: GLM-5.3-Flash (ZCode)
|
Gentle ping on this one — it has been open for two and a half weeks, and every finding across the review rounds is addressed. The current head @zhiiw could you take a look for a human review when you have a moment? Happy to address anything that comes up. |
Astro-Han
left a comment
There was a problem hiding this comment.
Reviewed exact head bd25e82fc2044fee586c8a16660129fc88417605 (incremental from 0f26b77c). Thanks for the quick iteration. The two message-text losses from the last round are fixed:
- Opening
/sidenow saves the recovered parent text as the parent draft and opens the side editor empty. - Closing the side chat is cancelled with a notice when the editor is non-empty after the wait, so text is no longer overwritten.
- With the
0f26b77crunner restored and the new tests kept, the idle-open, mid-turn-open and close tests fail, so they discriminate the fix.
Two P2 losses remain (inline):
- Retracted quotes are dropped on every session change. They are restaged for the old session, then
applySwitchResultclears staging unconditionally. This affects side open,/session, a quote-only side close and rewind. The Host has already removed them, so the TUI held the only copy. A probe confirmed resubmitting after a side round trip sends no quotes. The tests only assert the quotes the fake driver returned, not what the TUI submits. - Ctrl+/ still loses retracted text. The toggle captures the draft before waiting for the pending retraction, then overwrites the editor with that stale draft. A probe with Alt+Up in flight followed by Ctrl+/ left the side editor, parent editor and Host queue all empty, with no notice.
P3:
- The close-cancel check looks only at text, so a retraction that restored only quotes still closes and loses them.
- The cancel notice always says "retracted message", even when the text was typed.
- Rewind replaces the retracted message's quotes with the rewound turn's quotes; the test uses the same quote on both sides, so it doesn't show this.
Checks run locally: build, typecheck, ASF headers, git diff --check, Windows test inventory, TUI copy check, Biome; focused CLI tests 45/45, full CLI suite 1359 pass / 0 fail (one pre-existing cancellation in an untouched file). Not run: a live Runtime Host, Node 24, Windows/macOS.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
| @@ -2126,6 +2316,12 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> { | |||
| parentDraft: editor.getText(), | |||
There was a problem hiding this comment.
P2: by the time this runs, applySwitchResult has already called clearStagedQuotes() unconditionally, so any quotes the retraction just restaged for the old session are gone (the same applies to /session, a quote-only side close, and rewind). The Host has already dropped them from the queue, so this was the only copy. Carrying staged quotes into parentDraft (and restoring them on close), or deferring the clear until after the retraction settles, would keep them. A test that asserts what the TUI actually submits after the round trip would catch this.
There was a problem hiding this comment.
{"body":"Fixed on 5e538681b: retraction-restored quotes now ride the recovered draft text, so the #5109 rule stands while the quotes stop dying.\n\n- applySwitchResult still clears turn-staged quotes outright on every session change (silent resurrection stays impossible), but quotes staged by acceptRetraction now carry a followDraft lane that the clear spares. They stay keyed to wherever their text lives — the editor the switch does not discard.\n- Where the text is parked, the quotes are parked with it: the side pair gained parentQuotes/sideQuotes slots captured next to the draft on open/toggle and staged back for the owning view on return/close.\n- A rewind that branches without its own quotes keeps the drained quotes; one that carries its own still replaces them (the #5109 replacement semantics, now pinned by a test).\n\nTests assert what the TUI actually submitted (driver.submittedQuotes), not what the fake returns:\n\n- carries retraction-restored quotes through a side conversation round trip — red on bd25e82fc exactly as your probe (resubmit after the side round trip sent no quotes), green now.\n- carries retraction-restored quotes across a /session switch — red/green the same way.\n- keeps drained quotes when a rewind branches without quotes of its own — red/green the same way (the quote-less branch is the case where nothing replaces the drained quotes)."}
| // A session switch waits for an in-flight retraction: the retracted text | ||
| // and quotes must land in the session they were asked for before the | ||
| // driver re-keys (#5109 review). | ||
| await settleRetractions(); |
There was a problem hiding this comment.
P2: the Ctrl+/ toggle path captures the draft (around line 2229) before this wait for the pending retraction, then writes that stale draft back into the editor. With Alt+Up in flight and then Ctrl+/, the side editor, the parent editor and the Host queue all end up empty, with no notice. Capturing the draft after the wait (as the /side open path now does) would fix it.
There was a problem hiding this comment.
{"body":"Fixed on 5e538681b: the toggle now captures the draft — and the staged quotes riding it — after switchSession/switchAwayMidTurn resolve. Both paths already drain the pending retraction first, so the capture lands on what the drain restored, the same ordering the /side open uses with its explicit settleRetractions().\n\nTest: captures the draft a retraction restores while a side toggle waits it out — Alt+Up held on the Host call, Ctrl+/ parks behind it (asserted: no switch-start while the gate is closed), gate resolved; toggling back to the side view shows the recovered side follow-up in the side draft. Red on bd25e82fc with your reproduction (side draft empty after the round trip), green now."}
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed exact head bd25e82fc2044fee586c8a16660129fc88417605. The previous parent-text leak on side open and the post-retraction text overwrite on side close are addressed by this revision. I found one additional P2 in a later close window (inline). The two P2 findings already published in review 5362482066, concerning lost retracted quotes on session changes and stale drafts in Ctrl+/, remain and are not duplicated here. This head is not ready to merge.
Validation: Node 24 clean npm ci, build:test, focused TUI switch/retraction/close tests 8/8, git diff --check, and a clean merge-tree against current main ed38ccbb. Exact-head hosted test passed. The new finding follows the async close control flow and the editor input contract; I did not run a dedicated live-TUI close-gate reproduction. Native Windows/macOS and a real separated Runtime Host were not tested.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
| // keeps the recovered side text visible instead of overwriting it with | ||
| // the parent draft below, and a later Ctrl+C clears it like any draft | ||
| // (#5265 review). | ||
| if (editor.getText().length > 0) { |
There was a problem hiding this comment.
[P2] Recheck the editor after the asynchronous side close
This new empty-editor guard runs before await input.driver.closeSideConversation(...). The real driver awaits a session switch and cleanup (runtime-host-session-driver.ts:984-993), while runControl only sets editor.disableSubmit = true, not a typing lock (pi-tui-runner.ts:1131-1159). A user can type a new side draft during that wait; when the promise resolves, line 2386 unconditionally replaces it with pair.parentDraft. The new guard therefore prevents text restored before the close RPC, but silently discards text entered while it is in flight. The adjacent rewind test explicitly exercises typing during a control wait; no close test covers this interval. Preserve or recheck the live editor text before overwriting it after the await.
There was a problem hiding this comment.
{"body":"Fixed on 5e538681b: after the driver call and applySwitchResult land, the close now reads the live editor instead of unconditionally restoring pair.parentDraft. Input typed during the window outranks the parked parent draft — it is kept as-is, with the parent draft appended below a blank line when both are non-empty, so nothing is silently dropped.\n\nTest: keeps text typed while a side conversation close is in flight — closeGate parks the fake driver inside closeSideConversation, the test types during the hold, and the text must survive the re-key onto the parent. Red on bd25e82fc (editor restored to the empty parent draft), green now."}
…m dying in the switch paths Ninth-round review (apache#5265 review) flagged three P2s and three P3s in what happens to a drained retraction's payload after the drain: - Quotes lost across sessions (P2): every switch path cleared the staging a drained retraction had just staged, and the Host already removed the queue entries the quotes came from - the TUI copy was the only one, so side open, /session, a quote-only side close, and a rewind all lost them. Retraction-restored quotes now ride the recovered draft text, wherever it goes: applySwitchResult still clears turn-staged quotes outright (the apache#5109 rule stands) but spares the draft-riding lane; the side conversation's draft slots carry quotes alongside their text (parked on toggle/open, staged back for the owning view on return); a rewind that branches without its own quotes keeps them; a rewind that carries its own quotes still replaces them (apache#5109 semantics, now visible and pinned by a test with distinct quotes per side). - Ctrl+/ lost the recovered text (P2): the toggle captured the draft before waiting a pending retraction out, then wrote the stale draft back over what the drain had restored - side editor, parent editor, and Host queue all empty with no notice. The capture moved after the switch's drain, like the side open. - The close window overwrote live typing (P2): a close admitted against an empty editor left input unlocked while its driver call was in flight, then setText(parentDraft) discarded whatever was typed during the wait. The live editor content now outranks the parked parent draft (kept, parent draft appended below when both exist). - Quote-only close (P3): the abort guard only looked at editor text, so a retraction returning quotes with no text still closed and dropped the staging; staged quotes now count as a keep-open reason. - Notice wording (P3): the kept-open notice no longer claims a retracted message for what may be the user's own typing - two neutral literals replace the old one (check:tui-copy registry updated). - Rewind test (P3): the drain-before-rewind test minted the same quote on both sides, so the designed replacement could not be distinguished from survival; each rewind now carries a distinct quote and the resubmit asserts the rewound turn's quote won. Every finding verified red under the reviewer's reproduction on the pre-fix runner and green after; new coverage asserts what the TUI actually submitted (driver.submittedQuotes) across the side round trip, the /session switch, and the quote-less rewind. Full pi-tui-runner suite: 252 pass / 2 known pre-existing SIGTERM failures. check:tui-copy, check:locale-hygiene (vs upstream/main), check:asf-headers, and biome on the touched files pass; merge-tree against upstream/main is clean. Generated-by: GLM-5.3-Flash (ZCode)
|
{"body":"Addressing the three P3s from #5265 (review) on |
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed exact head 5e538681b1f84439796923b06f977f32cf40ea5a. The direct session-switch, side-toggle, and side-close paths now preserve the retracted payload or live input as intended. One retry path still loses restored quotes after a failed admission followed by a switch (inline finding), so I would not treat this head as ready to merge.
Node 24 build:test, eight focused TUI cases, TUI copy validation, diff-check, the exact-head hosted test, and a static merge with current main 5ac266b1 pass. I did not run an interactive TUI against a separate real Host or native Windows/macOS. The finding is based on the staging control flow; I did not add a dedicated dynamic failure-plus-switch probe.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
| // flight has since bumped it and must not inherit context meant for the | ||
| // original conversation (#5109 review). | ||
| const originGeneration = stagedGeneration; | ||
| const originFollowDraft = stagedQuotesFollowDraft; |
There was a problem hiding this comment.
[P2] Capture the draft-following provenance before clearing the staged quotes. When a retraction-restored quote is submitted, line 1499 calls clearStagedQuotes(), which sets stagedQuotesFollowDraft to false. This new read therefore always records false for a quoted submission. If admission then fails or returns no receipt, restageForRetry() restores the quotes as ordinary turn-staged quotes; a subsequent /session or side-view switch clears them in applySwitchResult() rather than carrying the retry context with the recovered draft. The new switch tests cover the quote before submission, and the failure tests retry without a switch, so they miss this combined path.
Summary
/rewindon a turn that carried quotes used to fail closed withrewind_unsupported_quotes(#5109, behavior C): the TUI could only refill the human-facing text, so the replacement submit would silently drop the turn's structured context.The runtime-host driver now returns the rewound turn's
QuoteRefs verbatim onrewindToTurn, andsubmitMessageforwards quotes throughturn.message.submit— the protocol admission already accepts client-authored quotes (only session-context attachments are Host-owned, and #4804 made a quote alone meaningful content). Attachments and directory references still fail closed, since the TUI cannot re-attach files.The TUI stages the restored quotes keyed to the branched session:
quotes:<n>segment while staging is live (accent salience, dropRank with the other chrome),/quoteslists the staged excerpts;/quotes clearis the explicit removal,The now-dead
rewind_unsupported_quotescode and its localized copy are removed together with the branch that emitted them.Part of #5109 — the remaining scope (editing a selected message that itself carries attachments) still needs the Host to expose the rewritten target-owned refs.
Verification
/quotes cleardrops them)pi-tui-runnerfull suiterestores the terminal before exiting on SIGTERM,forces signal exit when outer cleanup never settles) fail identically on pristineupstream/mainbuilt in a clean worktree on this machine: pre-existing Windows signal-handling failuresruntime-host-session-driverfull suiteSession cwd no longer exists: /tmpPOSIX fixtures); none of them is a test added or modified heretui-copy-catalog,tui-primary-guidance, coreslash-command-catalogcheck:asf-headersformat:checkbiome check --formatter-enabled=true --linter-enabled=falseindividually, and upstream CI runs the repo-wide gateAI use
Select exactly one:
Tool(s) and scope: GLM-5.3-Flash (ZCode) implemented the driver/TUI changes and tests under human direction and review.
Checklist
Does this PR entail a change in behavior?
/quotesis a new TUI command.