Conversation
dc28ae0 to
fa781b6
Compare
JensenChen28
left a comment
There was a problem hiding this comment.
复核当前 head 后发现 1 个阻塞问题:快照初始化路径宣称已完整限制挂死文件系统探测,但仍会立即执行一次无界的 workspace canonicalize,因此原来的永久卡住风险尚未闭合。其余变更与当前 required checks 未发现新的阻塞项。
b82847b to
1f69acd
Compare
|
评审 thread 清理(2026-09-28):45 个提交压缩为 1 个(61cb769be → 1f69acd,最终树与压缩前完全一致,CI 结论可直接沿用),round-6b/7/8 等逐轮流水账评论已删除,其结论并入 PR 描述的 per-area 清单与 Known limitations;原 round-6 完整审阅记录仍在 #63。清理前引用的旧 commit SHA 已全部失效。附带 PR #78(human-wait-pragma)已 rebase 到新 head,内容不变。JensenChen28 的 CHANGES_REQUESTED(快照安全检查无界二次 canonicalize)已由该压缩提交内的 snapshot 部分修复,inline 回帖已改为自包含内容,请复审。 |
JensenChen28
left a comment
There was a problem hiding this comment.
复核当前 head 1f69acd:上轮指出的快照初始化无界二次 canonicalize 已修复,安全分类整体通过同一有界文件系统入口执行;新增的流断线续传、超时进程组回收和审批结算路径未发现新的阻塞问题。验证:git diff --check 通过;快照安全分类、app-server 中途断流续传、Unix 超时进程组回收 3 项针对性测试均实际执行且各 1 passed / 0 failed;当前 required checks(Signed-off-by、Gitleaks、check、gate)均通过。当前环境缺少 rustfmt 组件,格式结论采用 CI check 回执。
|
Fresh full re-review of the current head (1ee9b80, 18 commits, directly on 61cb769 — no rebase needed, mergeable). 11 area reviewers over the whole diff plus a full diff-vs-body accounting; every load-bearing finding below re-verified by hand against the tree. Bottom line: the PR does real, root-caused work and is honest about its debts (all seven "Known limitations" verified present and pre-existing on base; no smuggled change requires splitting; commits and DCO clean; CI green). Four MAJOR findings need a decision before merge — two are objective fixes, two are completeness/claims judgments. Verification run
MAJORM1 — gate timeout now fabricates M2 — the MCP doc's unbounded-send-leg disclosure is false for the client the paragraph documents. M3 — "the cancellation contract is pinned red-green" overstates coverage of the exec fix. The M4 — a killed MINOR (should fix, non-blocking)
Body corrections / disclosures wanted
Verified good (evidence-backed)
Recommendation: land M1 + M2 (objective), resolve M3 (pin or reword), decide M4 (fix or declared follow-up), fold in the two cheap MINORs (isolated-path health gate, tasks.rs:1426 clippy) and the body corrections. Everything else can merge as-is after that. |
|
All four MAJORs and the actionable MINORs/NITs are now fixed in eight commits pushed to the branch (26 total; CI re-running). Every new pin was mutation-checked red against its reverted fix, then green; on the round-9 tree M1 — fabricated exit code: M2 — false MCP doc claim: M3 — overstated exec pin: resolved by rewording the body (no harness exists for M4 — unswept ref locks: MINORs/NITs fixed:
Not fixed, disclosed in the body's Known limitations instead (judgment calls, follow-up scale): the panic-only lost-publish window between delivery and publish (closing it means restructuring the delivery linearization), the |
#63 (d349f25) merged mid-review, so its final review wave never reached `pinvou3-clean`. This PR carries that wave plus the fixes from the subsequent review rounds onto the current base, as one commit; per-area detail lives in the PR description. - exec: headless runs resolve `request_user_input` via `cancel_user_input` instead of parking forever once the engine wait became unbounded; the cancellation contract is pinned. - snapshot: every pre-git window on a wedged mount is bounded (workspace canonicalize, the safety classifier's re-canonicalization, the first-init size walk, honoring `max_workspace_gb = 0`); a killed `git init` heals instead of wedging (HEAD-less, half-written, leftover config.lock/HEAD.lock); wedge failures surface as errors with stderr evidence and stage-keyed remedy hints; drain captures cap at 16 MiB per stream. - runtime: every approval exit publishes `approval.decided` and is race-free by construction (delivery races, monitor-death settlement, channel-closed exit); compat streams forward the approval posture. - tasks: liveness heartbeats short-circuit before the manager-wide state lock. The human-wait clock pause lives in #78 instead: as first landed it changed task-lifecycle behavior and regressed the TUI, so it was taken back out. - vision: the envelope timeout classifies as `ToolError::Timeout`, and the inference permit is acquired inside the envelope. - client: envelope-exceeded requests mark connection health and run the recovery probe on every dialect arm, including the Anthropic transport arm; the non-streaming budget got an injectable test seam. - app-server: a dropped stream resumes from the last consumed seq (bounded reconnects) before falling back to a best-effort interrupt, on every turn surface; the `/health` boot probe bounds each attempt. - tools: drain aborts are one helper covering every exit including cancel/drop; timeouts SIGKILL the child's process group so forked commands no longer outlive the call. - rlm: the sub-query budget is floored at 1s and the session default references `CHILD_TIMEOUT_SECS`. - mcp: the send-timeout error states the budget instead of tokio's `Elapsed` display. - docs: the 1800s timeout family anchors one complete roster; MCP/SUBAGENTS disclose the per-leg budget and wall-clock interactions; the RUNTIME_API.md restore contract is corrected. - tests: every fix above has a regression pin (mutation-verified red-green), timing margins are widened, and the Anthropic endpoint tests hold the env lock. Deliberately not fixed here (pre-existing on base unless noted, follow-up scale): Windows job-object group-kill parity; the contested inference-permit shape in web_search/voice/ prompt_suggestion; the pandoc.rs old-shape timeout; the two bounded runners' parallel drain implementations; the engine-side MCP stdio proxy's unbounded send leg; debounce starvation while heartbeats stream; the `CODEWHALE_MAX_OUTPUT_TOKENS` test-isolation race. Verified: `cargo fmt --check`; clippy `-D warnings` on codewhale-tui, codewhale-app-server, codewhale-core and codewhale-mcp; `cargo test -p codewhale-tui --lib` 11969/0 and `-p codewhale-app-server --lib` 103/0 on the final tree; the load-bearing pins were each mutation-checked to fail red. No-Issue: review-fix replay for the merged #63; no separate tracking issue exists. The full findings ledger is the round-6 review comment on #63. Signed-off-by: asto <asto18089@126.com>
…ry parsers The 16 MiB drain cap appended a truncation note that no parser consumed: an over-cap `ls-tree -r` listing would silently shrink the restore diff's target set, making restore delete files that exist in the snapshot (the pre-restore safety snapshot makes the loss recoverable, but the resulting tree is still wrong), and a truncated `git log` could end in a half-parsed pseudo entry. Both parsers now refuse a capture that ends with the cap note, and the note itself is pinned by a real-child drain test. Signed-off-by: asto <asto18089@126.com>
…ugin surface `drain_truncated` only matched the drain-grace note and only on stderr, so a plugin whose stdout hit the 16 MiB capture cap came back as `Ok(success)` carrying cut-off JSON plus a tail note no detector read. The detector now matches both truncation notes on both streams, the plugin escalation message names both causes, and a combination test pins the detector. Signed-off-by: asto <asto18089@126.com>
…am failure The cursor was only persisted on the `Ok` path, so a thread whose turn died past the resume budget replayed the failed turn's events from the store on every retry (turn_id filtering kept it correct; the cost grew with the history). The error path now persists the last consumed seq, pinned by extending the exhausted-resume test. Signed-off-by: asto <asto18089@126.com>
The GIT_INIT_FAILED hint told the user to remove the side-repo directory first, but that state does clear itself: the open-path sweep clears a lock once it has been stale for an hour (this PR's own mechanism, pinned by its own tests), and a second host initializing the same workspace concurrently lands here transiently. Following the old hint discarded the workspace's whole undo history for a state that heals within the hour; the hint now leads with waiting and names removal only as the persistent-failure fallback. The hint test's negative assertion pinned the wrong premise and now pins the new one. Signed-off-by: asto <asto18089@126.com>
…wing it The interrupted/channel-closed exit published `approval.decided` with `.ok()`, so a failed publish silently stranded the client's pending UI; it now logs at error severity like the settle path does. Signed-off-by: asto <asto18089@126.com>
The heartbeat short-circuit covered the main event loop, but the post-execution drain called `apply_execution_event` directly, taking the manager-wide state lock for a no-op against the invariant the short-circuit states. Signed-off-by: asto <asto18089@126.com>
`non_streaming_request_envelope` was widened to `pub(crate)` for a caller in the `client::anthropic` child module, which can already see the parent's private items. Signed-off-by: asto <asto18089@126.com>
…in code The debt was only stated in the PR description; `Connection::send` now carries the disclosure where a reader of the code will meet it. Signed-off-by: asto <asto18089@126.com>
- MCP: the per-leg budget shape holds for stdio servers only; HTTP servers are bounded by a single total timeout. Scope the sentence and state the HTTP bound (en + zh). - RUNTIME_API: scope "every resolution is published as `approval.decided`" to approvals whose `approval.required` event was published and disclose the event-failure exit; the compat stream maps an absent `posture` to `null` while the raw stream leaves it absent. - zh_hans/SUBAGENTS: the example `default_max_steps = 120` and the missing omitted/zero sentences were stale against the en doc and the code (defaults are zero/unbounded); sync them. Signed-off-by: asto <asto18089@126.com>
The drain detector matched its notes anywhere in the buffer, while both writers append their note as the buffer's last bytes and stop retaining past it. Anchor the match to the tail — the same contract the snapshot capture's capture_hit_the_cap pins — so a stream that merely quotes a note mid-body is not truncation evidence, and pin the stricter semantics with mid-buffer negative cases. Also close the integration gap the round-7 review flagged: a real child flooding 17 MiB of unparseable stdout through the plugin surface must be refused as truncated, not reported as a silent success. Signed-off-by: asto18089 <asto18089@126.com>
…them Four approval.decided publishes still ended in .ok(), so a storage failure silently dropped the event that tells clients to clear their pending approval UI — the same failure mode the forced-resolution arm already logs loudly. Log all four (auto-approve, auto-review deny, user allow, user deny) with the approval id; no control-flow change. Signed-off-by: asto18089 <asto18089@126.com>
…ement The per-entry comment still claimed the send cannot deliver because the receiver died with the monitor, contradicting the block comment above it: entries whose receiver is still live are settled too, and the deny is then pushed through the decision channel (the first settle fixture pins exactly that). Signed-off-by: asto18089 <asto18089@126.com>
The init-failure remedy already led with the self-healing sweep, but the user-facing hint never said what the removal fallback costs — that warning lived only in a code comment. State it inline, and pin the ordering with a test assertion so a regression that puts removal before the sweep cannot come back green. Signed-off-by: asto18089 <asto18089@126.com>
The biased select polls the event arm before the flush arm and the debounce sleep restarts every iteration, so a stream denser than the debounce interval defers persistence until the stream pauses or the loop's trailing flush lands it. Heartbeats never arm dirty, so the silent-tool tick stream cannot starve it; a dense run of unpersisted deltas can. Disclose it where the delay lives instead of leaving the debt only in the commit message. Signed-off-by: asto18089 <asto18089@126.com>
"Omitted or zero max_steps remains unbounded even when an operator default is configured" is half false: only an explicit zero ignores the operator default (resolve_max_steps keeps 0 as unbounded), while an omitted budget falls back to it — pinned by resolve_max_steps(Scout, None, Some(90)) == 90 and contradicted by the priority list two paragraphs up. State the real rule in English and sync the Chinese doc that round-7 translated from the wrong sentence. Signed-off-by: asto18089 <asto18089@126.com>
…g one path "The tool call is denied" only literally describes the registration branch; when the auto-approve branch's required emit fails, the tool call is stopped by the monitor's cancel instead. The externally observable contract is the same either way — the tool never executes — so state that. Signed-off-by: asto18089 <asto18089@126.com>
The budget paragraph read as if both stdio legs were bounded, but the send leg has no deadline — a server that stops draining stdin can park the write past every budget (disclosed at the write site itself in round-7). Mirror that caveat in the English and Chinese docs so the contract matches the code. Signed-off-by: asto18089 <asto18089@126.com>
The gate moved onto the bounded runner and mapped a timed-out child to the conventional 128+SIGKILL so the record schema always carried an integer. That integer reaches hook `exit_code` conditions through tool metadata, and `reported_tool_exit_code` exists precisely so an `exit_code` condition never matches a fabricated value (128+SIGKILL doubles as the OOM-kill code, so OOM hooks fired on every gate timeout). Base recorded no exit code for a timed-out gate; `timed_out: true` in the same metadata stays the timeout signal. Pin the absence with a real timed-out gate, and fix the bool-assert comparison in the neighbouring spawn-evidence test while there. Signed-off-by: asto18089 <asto18089@126.com>
The recovery work covered config.lock/HEAD.lock at init and index.lock at open, but the commit path's `update-ref HEAD` (and prune's, and pack-refs) takes refs/heads/<branch>.lock or packed-refs.lock, and a git killed mid-update leaves those behind on exactly the wedged mounts this recovery targets. Nothing ages them out, every later snapshot fails at the ref update, and the failure is ErrorKind::Other with no marker, so the once-per-workspace notice gate swallows it: silent undo-history loss. Sweep both ref-lock shapes on the same staleness rule as index.lock when the side repo opens; fresh locks stay untouched. Signed-off-by: asto18089 <asto18089@126.com>
The new transport-failure marking in the Anthropic arm fires on every request, including the isolated Auto-classifier one. That clone shares connection_health through the Arc, so a read-only inspection wrote the shared health and could fire a real /models recovery probe — the same contract the generic send paths enforce by dispatching isolated requests to send_with_isolated_retry. Gate the transport arm on isolated_request_state and pin the isolation: two stalls from an isolated client must leave the shared health untouched and fire no probe. Signed-off-by: asto18089 <asto18089@126.com>
Two approval-settlement follow-ups: The approval.required-emit-failure rollback removed its registration with a blanket map remove. Providers reuse tool-call ids across threads, so under a reused id the entry may already belong to another thread's live waiter and the rollback would strand it — the exact case the cancel_closed_pending_approval doc forbids. Remove only a same-thread match (cancel_thread_pending_approval), which retires the now-unused blanket helper, and pin both sides of the guard. The monitor-death settlement logged a dropped approval.decided publish without naming the approval — the one path where several approvals can be stranded at once, and the only one of the six error logs missing the id. Add it. Signed-off-by: asto18089 <asto18089@126.com>
kill_the_run returned without any kill when kill(-pgid) failed with ESRCH, on the reasoning that an empty group means the child is dead. That holds only for a child that stayed in its group: a direct child can setpgid itself into another same-session group and stay alive, so the empty group left the un-timed child.wait() running past the budget with nothing enforced. Fall through to the child-only kill on ESRCH — a no-op on a genuinely dead child, the last chance to stop an escaped one. Signed-off-by: asto18089 <asto18089@126.com>
The truncation refusal embedded the whole captured stdout in the ToolError message, so a 16 MiB capped capture rode the error text toward the transcript. Echo the first bounded characters instead (codewhale_hooks::bounded_text also strips control bytes, so a capture of raw NULs echoes nothing); the diagnostic job is the note plus a preview. Signed-off-by: asto18089 <asto18089@126.com>
snapshot_dir_for now runs the workspace canonicalization under the bounded probe on every call, so the module doc's "cheap to call repeatedly" promise and the comment's premise that the turn pipeline already canonicalized the workspace no longer described this code. State the actual contract: every caller pays one bounded probe, hot paths should cache, and the raw-path fallback hashes identically for canonical input. Signed-off-by: asto18089 <asto18089@126.com>
The send-leg disclosure landed in the Server Fields paragraph, but that paragraph documents the TUI pool's per-server settings — and that pool's send leg is deadline-bounded (the request budget wraps the send at mcp.rs call_method), so the sentence was not true of the client the settings govern and contradicted the per-leg sentence above it. The unbounded blocking write belongs to the engine-side stdio proxy, which does not consume these knobs at all; drop the false sentence and give the proxy its own short paragraph with its fixed budgets, in English and Chinese. Signed-off-by: asto18089 <asto18089@126.com>
797fbd6 to
ea07845
Compare
update-ref HEAD takes the HEAD lock and the resolved branch's lock one after another, so a git killed mid-update can leave a lone HEAD.lock. A leftover one does not make the side repo unready, so the init-path sweep never sees it again on an initialized repo, and the open-path ref-lock sweep only covered packed-refs.lock and refs/heads/**.lock — every later snapshot then fails at the ref update with the loss surfacing nowhere, the exact wedge this sweep exists to age out. Add the HEAD lock to the open sweep and pin both the stale-removed and fresh-kept rule for it (mutation-checked red against the reverted sweep). Signed-off-by: asto <asto18089@126.com>
The round-9 isolation gate covered only the transport-failure arm; check_anthropic_response still marked shared connection health (and could fire the recovery probe) for HTTP 4xx/5xx envelopes on the isolated Auto-classifier request, while the generic dialect's isolated dispatch path writes nothing. Gate the status arm's failure and success marks the same way, and pin it with a two-500 isolation test whose expect(0) probe would go red against the reverted gate. Signed-off-by: asto <asto18089@126.com>
The default arm of the stage-keyed remedy router promised a stale index.lock for every non-init git timeout, but an interrupted update-ref/pack-refs leaves a ref lock (HEAD.lock, refs/heads/**.lock, packed-refs.lock) instead — the sweep covers all of them, so the hint should not send a debugging user to one file name. Signed-off-by: asto <asto18089@126.com>
Heartbeats never arm dirty, but their ~200ms cadence still restarts the 250ms debounce, so dirty state armed just before a silent-tool phase stays unpersisted for that whole phase — the old note claimed the tick stream cannot starve the flush at all. Signed-off-by: asto <asto18089@126.com>
The child leads its own process group and a group leader cannot leave it, so the setpgid-escape scenario the ESRCH comment described is not reachable; the fall-through stays correct as a defensive last resort on any kill error. Signed-off-by: asto <asto18089@126.com>
RUNTIME_API: posture rides execution-policy denies only (the full-access auto-approve also forces outcomes but carries auto: true, not a posture). MCP: the engine-side stdio proxy ignores the timeout settings, not the whole server config — command/args/env still apply (en + zh_hans). Signed-off-by: asto <asto18089@126.com>
|
Fresh full-diff review wave (round-10), run from scratch after rebasing the branch onto the advanced base ( Verdict: the PR is sound at its core; the wave found one incomplete heal and one incomplete isolation gate, both fixed and mutation-pinned in this push, plus five small corrections. No smuggled changes, no perf regressions, no material body falsehoods. What the wave confirmed
Findings fixed in this push (six commits)
Disclosed, not fixed (follow-up scale)
Verification on this head (0362c2c)
|
Summary
#63 (merged as d349f25) closed mid-review, so its final review wave never reached
pinvou3-clean. This PR lands that wave plus the fixes from the review rounds that followed, on top of the current base (pinvou3-clean@ 61cb769). After review stabilized the branch was squashed into one commit; the PR diff is the single source of truth. Full findings and evidence for the original audit are in the round-6 review comment on #63.What's in the diff
request_user_inputviacancel_user_inputinstead of parking forever once the engine wait became unbounded; the engine half of the cancellation contract is pinned red-green (cancel_user_input_resolves_the_headless_wait_deterministically), while the exec-side arm itself is untested —run_exec_agenthas no test harness (neither do its sibling arms).max_workspace_gb = 0); a killedgit initheals instead of wedging (HEAD-less, half-written, leftoverconfig.lock/HEAD.lockall recover); a killedgit update-ref/pack-refsleaves a ref lock the open sweep now clears likeindex.lock; wedge failures surface as errors with stderr evidence and stage-keyed remedy hints; drain captures cap at 16 MiB per stream.approval.decidedand is race-free on every reachable path (delivery races, monitor-death settlement, channel-closed exit; the one panic-only window is disclosed under Known limitations); theapproval.required-emit-failure rollback removes only a same-thread match, so a reused id cannot strand another waiter; compat streams forward the approval posture.ToolError::Timeout; the inference permit is acquired inside the envelope.seq(bounded reconnects) before falling back to a best-effort interrupt, on every turn surface; the/healthboot probe bounds each attempt.CHILD_TIMEOUT_SECS.Elapseddisplay.Round-9 review follow-ups (eight commits on top of round-8)
From the fresh review wave of the full diff:
exit_code = 128+SIGKILL: the integer reached hookexit_codeconditions through tool metadata and violated the documentedreported_tool_exit_codecontract (anexit_codecondition must never match a fabricated value; the code doubles as the OOM-kill convention, so OOM hooks fired on every gate timeout). The timeout signal staystimed_out: true; the absence is pinned by a real timed-out gate. Also fixes the bool-assert comparison the stricter local clippy flags in the neighbouring test.update-ref HEAD(commit and prune) andpack-refstakerefs/heads/**.lock/packed-refs.lock, a killed git leaves them behind on exactly the wedged mounts this PR targets, nothing ages them out, and the resultingErrorKind::Otherfailure fell through the once-per-workspace notice gate — silent undo-history loss. The open sweep clears them on theindex.lockstaleness rule; fresh locks stay untouched. The pin covers stale removal (flat and nested) and fresh-kept./modelsprobe (the isolated-dispatch contract the generic send paths already enforce). Pinned with a two-stall isolation test (expect(0)on the probe).setpgiditself out of its own group and stay alive, and the early return left the un-timed wait running past the budget.codewhale_hooks::bounded_text, 512 chars, control bytes stripped) instead of embedding up to 16 MiB of captured NULs in the error text.Review also disclosed two drive-bys the earlier sections had not named: the app-server
drop(config)of the resolved endpoint config before the upstream round trip (releases the config read guard so writes are not blocked for up toUPSTREAM_TOTAL_TIMEOUT), and the core/turn.rs hunks (notify-gate widening forErrorKind::Otherinit failures, the stage-keyed remedy-hint router, and their marker-based tests), which belong to the snapshot bullet above.Round-10 review follow-ups (six commits on top of a clean rebase onto
pinvou3-clean@ 44921cf / #80)From the next fresh review wave of the full diff:
.git/HEAD.lock:git update-ref HEADtakes the HEAD lock and the resolved branch's lock one after another, so a git killed mid-update can leave a loneHEAD.lock, and a leftover one never makes the side repo unready — the init-path sweep (previously the only one covering it) never runs again on an initialized repo, so every later snapshot would fail at the ref update with the loss surfacing nowhere. The pin covers stale removal (branch, nested, HEAD) and fresh-kept (packed-refs and HEAD), mutation-checked red against the reverted sweep.check_anthropic_responsestill marked shared connection health (and could fire the/modelsrecovery probe) for HTTP 4xx/5xx envelopes on the isolated Auto-classifier request, while the generic dialect's isolated dispatch path writes nothing. The status arm's failure and success marks are gated the same way, pinned by a two-500 isolation test (expect(0)on the probe), mutation-checked red.index.lockfor every non-init git timeout: an interruptedupdate-ref/pack-refsleaves a ref lock instead, and the sweep covers all of them.dirty, but their ~200ms cadence still restarts the 250ms debounce, so dirty state armed just before a silent-tool phase stays unpersisted for that whole phase); the group-kill ESRCH comment no longer describes a setpgid-escape scenario (the child leads its own group and a leader cannot leave it — the fall-through stays as a defensive last resort on any kill error); the snapshot open-path comment no longer claims the whole path degrades "within one probe budget" (several bounded legs each degrade within their own); RUNTIME_API scopespostureto execution-policy denies (the full-access auto-approve also forces outcomes but carriesauto: true, not a posture); MCP (en + zh) says the engine-side stdio proxy ignores the timeout settings — per-servercommand/args/envstill apply.Testing
cargo fmt --check; clippy-D warningson codewhale-tui / codewhale-app-server / codewhale-core / codewhale-mcp; on the round-9 treecargo test -p codewhale-tui --libis 11975 tests / 13 ignored and-p codewhale-app-server --lib103/0, green in isolation and green modulo the documentedremote_controlload flakes plus twosandbox::process_hardeningno_new_privstests that fail identically on the unmodified wave head (environmental on this box); the load-bearing pins were each mutation-checked to fail red (round-9 adds four more pins — gate exit-code absence, ref-lock sweep, anthropic isolation, rollback guard — each verified red against its reverted fix; round-10 adds two — the HEAD.lock sweep above and the anthropic status-arm isolation, likewise red-verified).Round-7 review follow-ups (nine commits on top of the squashed wave)
ls-treewould have made restore delete files that are present in the snapshot; the pre-restore safety snapshot kept the loss recoverable, but the resulting tree was still wrong). The cap note itself is pinned by a real-child drain test.drain_truncatednow matches both truncation notes on both streams, so a plugin whose stdout hit the capture cap can no longer pass a cut-off output as a success; the escalation message names both causes.seqback to the thread cursor, so the thread's next message no longer replays the failed turn's events from the store.GIT_INIT_FAILEDremedy hint now leads with the stale-lock sweep (~1 h self-heal) and names directory removal only as the persistent-failure fallback; the old text pointed straight at removal, which discarded the workspace's whole undo history for a self-healing state (a concurrent second init lands in the same arm transiently).approval.decidedpublish on the interrupted exit is logged at error severity instead of.ok().non_streaming_request_envelopestays private; theclient::anthropicchild module can see the parent's private items.Connection::send, not only in this description.approval.decided" to approvals whoseapproval.requiredevent was published, and states the raw-vs-compatpostureshapes; zh_hans/SUBAGENTS syncs the staledefault_max_stepsexample (120 → 0/unbounded) and the missing omitted/zero sentences. Also declared here: the zh_hans sync and restore'starget_shortget(..12)panic-safety hardening were drive-by fixes in the squashed wave that this description had not called out.Round-8 review follow-ups (eight commits on top of round-7)
drain_truncatedanchors the match to the capture tail: both writers append their note as the buffer's last bytes, so output that merely quotes a note mid-body is no longer truncation evidence (the same tail contract the snapshot capture'scapture_hit_the_cappins); mid-buffer negatives are pinned too, and a real-child end-to-end test now covers the plugin surface refusing a 17 MiB unparseable stdout instead of passing the cut capture as a success..ok()-swallowedapproval.decidedpublishes (auto-approve, auto-review deny, user allow, user deny) log at error severity with the approval id, matching the round-7 forced-arm fix; no control-flow change. The settle path's per-entry comment now matches live-receiver settlement instead of contradicting its own block comment.GIT_INIT_FAILEDhint names what the removal fallback costs (the workspace's snapshot history), and its test pins sweep-before-remove ordering so the destructive-first regression cannot come back green.biasedpolls the event arm first and the debounce sleep restarts every iteration, so a stream denser than the debounce interval defers persistence to a stream pause or the trailing flush (heartbeats never armdirty; dense unpersisted deltas can). Debt disclosed, not yet fixed.max_stepsignores an operator default; an omitted budget falls back to it (the previous sentence contradicted the pinnedresolve_max_steps(Scout, None, Some(90)) == 90and the priority list above it — round-7 had translated the wrong sentence into Chinese). RUNTIME_API: theapproval.requiredemit-failure parenthetical now says "the tool call never executes" instead of pinning one deny path (the auto-approve branch stops the call via the monitor cancel, not an explicit deny). MCP (en + zh): the budget contract discloses the unbounded stdio send leg, matching the in-code disclosure.Known limitations (deliberate, follow-up scale; pre-existing on base unless noted)
CODEWHALE_MAX_OUTPUT_TOKENSis set process-wide by env-locked tests while unlocked readers can interleave) — separate hygiene PR.approval.decidedpublish, settlement cannot rescue the entry (it only rescues map-resident ones), leaving a journalapproval.requiredwithout adecided— disclosed, follow-up scale; closing it means restructuring the delivery linearization.create_message,create_message_stream,translate,synthesize_speech,fim_completion, the no-cache classifier request, and provider-native search all acquire the inference permit before the bounded request — pre-existing on base, nothing new introduced here.open_existingreports a half-initialized side repo as "no restore points" instead of the underlying error until the next open heals it (deliberate healing-first design; a review-observed truthfulness nit).Failed/Interruptedalso skips the seq-cursor writeback (stream_result?propagates before the insert), so the next message replays that turn's event tail once (turn-id filtering keeps it correct); the round-7 writeback covers stream failure only. Self-heals after one turn.No-Issue: review-fix replay for the merged #63; no separate tracking issue exists.