Repository navigation
fix(managed-agent): close the three Critical H0c follow-ups from #13300 - #13355
Conversation
…to docs/h3-shell-monitor-design
PR 1 of #13300, fixing the three Critical findings from the round-3 review of #12855. R3-1: the Broker-record execution mapping now reads the record's dispatchGeneration: SETTLED/cancelled with generation 0 (the record's own proof it was never claimed, per ToolExecutionRecord) maps to not_started_proven instead of settled, removing the illegal intent -> settled shape that left a Monitor cancelled before dispatch with no committable settling revision. A shared brokerExecutionCases row (settled-cancelled-unclaimed) replays it in both languages, the TS divergence pin gains the second wire-reading divergence, and Decision 10 of the authority design is reconciled with the H0b record-contract doc in both language pairs. R3-2: the store now runs the event-level half of the envelope checks for every event line at commit time: closed envelope with an optional subject, version, declared sequence, well-formed event id outside the reserved <domain>:<n> namespace, this Session's closed key, valid time, and kind from a mirrored EVENT_KINDS vocabulary; every domain.committed payload is validated whether or not a body is registered (reusing the pinned DOMAINS); unknown-subtype lines follow the scanner's "after the Managed header" condition; a transaction carries at most one Stage H record; and per-kind byte caps (maxEventBytes, maxCommitMarkerBytes) are pinned in the shared limits contract and enforced per line kind. All refusals answer the existing 409 so the commit rolls back. Replay commits return before any of this runs, as before. R3-3: task.updated rides its own outbox (managed_agent_task_event, V35) written in the same commit transaction and keyed by a unique source key, drained later by the task-events slice; managed_agent_event stays turn and lifecycle events, so an announcement between two text deltas can no longer split a message Part under the frozen projection version. The route description is scoped to what the server does (a Session being deleted announces nothing while its tasks stay readable), and the design's replay-idempotency sentence is corrected. Test helpers that wrote journals a real authority could not read back (ActionJournal's reserved event ids, bare event/marker lines in the publication and session-store integration suites) now write well-formed lines. Mutations of every new check were verified red against the suites.
…ures How: wrap the delegated-root probes so a missing or unreadable root answers the documented isolation error on Linux too (macOS refused at the platform check first, which is why only CI saw the raw ENOENT), and document the background Shell environment allowlist in the process.env guard. Why: the Test lane on 4fb9f0a went red on exactly these two items; both are this branch's own changes, not flake. Test: hook-command-cgroup and process-env-guard suites green; tsc clean.
How: registerManagedContextRoutes builds a ManagedChildRunSupervisor from the delegated cgroup root the Hook commands already use (QWEN_MANAGED_HOOK_CGROUP_ROOT) and passes it as the executor's fifth argument; a boot without the delegation keeps the executor's committed refusal instead of failing to boot. Why: executeV3Background landed in the previous increment with the supervisor parameter unwired, so every real-stack background start answered the committed no-supervisor refusal. Test: new wiring case asserts the supervisor is injected exactly when the root is set; managed-context-worker, process-env-guard and managed-background-shell suites green (775); tsc clean.
…esses How: the runtime-ownership section now says the ledger holds two rows — the foreground-length start invocation settling with the handle, and a second execution admitted at start that carries no model result and stays non-terminal until physical exit evidence. Why: the Java ledger writes a result exactly when an execution settles, so a single row that is both handle-delivered and active cannot exist; the two-row shape keeps the settled-if-and-only-if-result invariant while preserving every cited behavior (Runtime holds, evidence-only settle).
How: HostedChildRunSession funnels every record write through one serialized, replay-safe commitExtensionRecord path — admit first with the start call's args as commandRef, dispatch and the set-once managed-runtime-receipt after the physical start, outputRef advance-only, and settlement only on proven exit evidence, a proven failure, or an honored stop request. Live revisions alternate the run line (no self-loops); the authority freeze refuses anything after a terminal revision. Why: the dual path puts product records on the hosted authority, but nothing commits child_run from the hosted side yet — the worker-owned registry intentionally does not touch records. Test: four authority-level cases — full chain with projection, stop request to cancelled, pre-start failure frozen on not_started_proven, and serialization with deep-equal skips.
How: a background Shell start now settles success with a capture object whose status is 'detached' — no reason, no manifest — because the live output streams through the child_run record's growing manifest rather than the result; the shared schema pins both invariants, the TS parser mirrors them, the shared fixtures gain one valid and two invalid envelope cases replayed on both sides, and the Java projector accepts the missing manifest for unavailable or detached captures. Admission refusals settle not_started with a null capture, the shape the durable unstarted family already owns. Why: every Session tool.receipt event lands in the Java delivery projection, which requires a capture object and a committed-if-manifest pairing for anything that started — a success result with a null capture fails that projection and corrupts replay. Test: core contract suite 519 (three new envelope cases), TS serve suites 33 + turn/harness 323 + background 6, Java publication contract and projector suites 8; tsc clean on both packages.
… record lines How — the four Criticals first: the stream capture publishes an ended stream's revision only after its seal decision, so a sealed-or-incomplete descriptor never changes again (R2-3); finalize's settle path degrades to an unavailable capture instead of throwing past a writability failure, and the last published revision stands like the worker-cut cap (R2-5); the child_run body now refuses a settled or cancelled run that is not settled execution, a failed run off its two ending lines, start_failed outside not_started_proven, and a process-level failure without a settled execution, on both languages with new witnesses replayed from the shared fixtures (R2-6, R1-30); and the supervision suite skips win32 instead of spawning a shell that cannot exist (R2-7). How — the Suggestions land as: supervisor start now proves membership by cgroup.procs with an fd-3 status channel, failing closed as isolation (R2-37); the bounded EOF wait caps daemon-inherited pipes instead of hanging, and a spawn-time error drains the entry (R2-1/R2-14); registry hasHolds requires its session and setProcessResult carries the full result shape (R2-13/R2-15); create() validates its caller-named unit like attach() (R2-26); the executor checks the journal before any effect, mirrors the closing/is_active re-check after prepare, gates the ninth live Shell as a committed quota refusal, and keeps cgroup membership out of an empty QWEN_MANAGED_HOOK_CGROUP_ROOT (R2-23/R2-19/R2-24/R2-18); the spawn uses the configured shell and the session-context environment (R2-21/R2-20); invalid fixture cases each pin their refusing clause on both languages (R2-36); projection fixtures pin the draining precedence rows and the Java replay reads stopRequested (R2-28/R2-38); the draining Javadoc states the rule the code implements (R2-39); the duplicated ref helper is gone (R2-40); the stream-capture suite is typed and gains the seal-before-publish, page-cursor, late-failure and settle-degrade witnesses (R2-29/30/31/32); and the design doc names the cgroup switch decision, the cursor's single ordinal space, the unconditional host-scope evidence gate and the zh stopped-object (R2-8/9/10/11/12). Test: core suites 788, cli serve suites 776 + background 11, Java record/ projection/store suites 24; tsc clean.
…ed receipt family How: HostedSession gains its session-scoped childRuns orchestrator next to hooks/mcp, the tool turn receives it stored-ahead of the admission branch, the reopen verifier and the recovery replay both accept the third durable receipt family — a blocked delivery with a detached capture, beside the complete and not-started ones — with the publication store correctly left out of its proof, since its durable truth is the child_run record. Why: the H3 background start settles with a handle whose output lives on the record's growing manifest; without the third family, any Session holding a background receipt could never reopen or be replayed. Test: recovery-session suite 52 with two new detached-family cases; harness-session and tool-turn suites green; tsc and lint clean.
How: drop the as-unknown cast on the FakeSink return and implement the failCapture member the cast had been hiding (R2-29's cli location). Why: an unchecked double can drift from the interface it claims to match — and it already had. Test: background-shell suite 10/10; tsc clean.
…ol turn How: the turn replaces its two deliberate is_background refusals — exactly when the Session owns its child_run orchestrator and the domain is enabled — with orchestration that mirrors the shared line: the record intent precedes every physical effect, dispatch_started lands the moment the checkpoint commits, a settled detached handle attaches the physical start to the record from the same facts and lands as the third durable receipt family (blocked delivery with a null manifest, replay-validated exactly like the not-started family), and a proven-unstarted refusal settles start_failed on not_started_proven while riding the existing unstarted family unchanged. Malformed is_background values keep their old refusal, and the family stays out while the domain is disabled — the same probe governs both paths. Why: the background start is the first record-bearing, user-visible H3 effect, and without a hosted history family for the handle a Session that ran one could never reopen or be replayed. Test: three new authority-level turn cases — admitted detached flow with admit/dispatch/attach order and the blocked receipt family, a proven-unstarted refuse closing NOT_STARTED, and the disabled-domain refusal preserving its exact text; turn suite 146, plus harness-session, recovery, background and child-run suites 246; tsc clean.
Re-verification (round 2) — #13355 at
|
|
@qwen-code /triage |
qqqys
left a comment
There was a problem hiding this comment.
Reviewed head: a43bbc7bbdd95a02d539aab4bd67c644220ea028 (base main).
Approve. The one merge-blocker ever raised on this PR is closed and now moot, no other historical blocking finding stands, and a Critical-only scan at this head found nothing blocking. An earlier review of mine at 000e984e was dismissed by head movement; this is a fresh pass over the current code, not a restatement of it.
Historical blocking findings
The Flyway version collision is closed, and the PR no longer ships a migration at all. The blocker was that this branch's V35__managed_task_event.sql shared a version with main's V35__managed_session_tool_profile.sql, which makes the merged server refuse to start. At the current head the diff contains no db/migration file whatsoever — the task-journal migration reached main independently as V45__managed_session_task_journal.sql. Main's directory now tops out at V47__managed_channel_route_delivery.sql while this branch's tree tops out at V45, so the branch is behind main but adds nothing that can collide, and a merge cannot remove main's V46/V47 because this diff does not touch them. The Flyway migration version uniqueness gate passes. I re-checked main's versions at review time rather than relying on the earlier head, since main advanced twice today.
Nothing else blocks. All 33 review threads are resolved, there are no [Critical] inline comments and no review ledger. The two dismissed reviews — yiliang114's LGTM and my own earlier pass — were dismissed by head movement and are not standing positions. reviewDecision reads CHANGES_REQUESTED but no live CHANGES_REQUESTED review exists in the data; it is an artifact of those dismissals, so I did not treat it as a blocker.
Critical-only scan
Production code is ~265 lines across six Java files; the remaining 23 files are tests, shared fixtures and the bilingual design pair. I checked the three fixes the PR exists to make.
1. The execution mapping's premise holds, and the precedence is right. executionOf now takes dispatchGeneration and maps a settled record to not_started_proven when:
case SETTLED -> "not_started".equals(executionStatus)
|| "cancelled".equals(executionStatus) && dispatchGeneration == 0
? "not_started_proven" : "settled";&& binds tighter than ||, so this reads not_started || (cancelled && generation == 0) — a claimed cancel stays settled, which is the conservative direction since nothing records whether a claimed call was actually sent. The whole mapping rests on generation 0 meaning "never claimed", and that premise is enforced rather than assumed: ToolExecutionRecord throws IllegalArgumentException("claimed dispatch generation must be positive") whenever a dispatch owner is present, so a claimed record cannot carry 0. That removes the impossible intent → settled step the record contract refuses and gives a Monitor cancelled before dispatch a committable settling outcome.
2. Commit-time validation cannot drift from the reopen reader, which is how it could otherwise brick a Session. The risk in moving validation onto the commit path is refusing writes the reader would have accepted. The change avoids that by sharing one source instead of duplicating rules: ManagedExtensionRecordStore validates through ManagedExtensionRecords.count/id/closed/oneOf/durableRef, the shared ManagedExtensionRecords.EVENT_KINDS vocabulary (now carrying its seventeenth kind, message.retracted, in all three mirrors), and ManagedExtensionProjection.RECORD_BODIES for both the domain registry and the reserved-id namespace pattern. The two new constants in ManagedSessionStoreModels — MAX_EVENT_BYTES (1 MiB) and MAX_COMMIT_MARKER_BYTES (64 KiB) — are documented as the limits the authority's reader parses and digests, and the commit path reads those same fields, applying the tighter cap to commit markers and the ordinary cap to event lines. A malformed line now fails the commit with a refusal instead of storing fine and bricking the Session at the next open, which is the direction the finding asked for.
3. The split assistant message is closed on both sides. The write side drops the public stream announce entirely, so nothing announcement-shaped can land between two streamed text deltas and the bounded per-task journal from #13265 is the single feed. The read side is fixed symmetrically: materializeText selects the previous part of the same series by greatest last_sequence, with the comment recording that this holds "whatever events sit between the two deltas — an announcement row must not split an assistant message in two". So even if some other row does land between deltas, the projection still continues the same Part rather than starting a second one. Fixing only the writer would have left the reader fragile to the next in-stream row; fixing both is what makes the guarantee structural.
No Critical found.
CI
No failing checks at this head: 22 pass, 26 skipped, 1 pending. Nothing to attribute to this PR.
Not scanned — disclosed, not asserted clean
I verified the three fixes above and their load-bearing premises in source. I did not read all 848 lines of ManagedExtensionRecordStore or the whole of ManagedExtensionRecords.java (+41/−14), did not audit the ~19 test and shared-fixture files beyond their bearing on the above, executed no suite, and ran no real database migration or reopen against a committed journal. In particular the deletion-race behaviour described in the PR body — the announcement point re-reading the latest committed status FOR UPDATE inside the record commit's own transaction — is something I read only as design intent and did not verify against a concurrent-deletion witness. I report no Critical in those areas because I found none where I looked, not because I proved absence.
Scope note
Approval is bound to commit a43bbc7b. The branch is behind main by two migrations; that is not a defect, but the usual pre-merge refresh of main is still worth doing so the Java lanes run against the merged tree.
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 2235 passed · 0 failed · 2235 total Flakiness gate: ✅ 2 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:2235 通过 · 0 失败 · 2235 总计 抖动门:✅ 2 changed test file(s) x 5 identical rounds, no divergence Verification reportPR 13355 — deep verificationVerdict:
中文摘要结论: A/B 结论:base( 主要发现:
未覆盖:MySQL/MariaDB failsafe 通道(容器内无 ScopeThe PR bundles three Critical follow-ups from #13300. Effective diff
Out of scope and listed under Not covered: the MySQL failsafe lanes, SpotBugs as evidence, per-commit attribution, and repo-wide TS gates beyond the two changed test files. Central claim + A/BSuite cells (base vs head)See
Δ = +2 tests, +0 failures. The single error is Base arm built in an isolated local repo ( Mutation matrix — is each new guard load-bearing?Each mutant reverts one guard the PR introduces, then runs
9/9 targeted guards killed; the positive control killed too, so the harness demonstrably can turn these suites red — the survivors are not an artifact of a dead runner. M1 reproduces the PR's own claim ("removing the generation arm turns exactly that row red"): the shared fixture row Central claim verdict: proven load-bearing. 8 of the 11 mutants are commit-time-validation guards and every one is pinned by a test that fails with the expected refusal missing. Secondary claim 2 — the announcement is genuinely goneCensus, not reading: CorrectionsThe PR body describes the split mechanism accurately — "the projection merges deltas by the event at sequence−1". The code comment this PR adds does not; see F1. This is a correction to the description carried by the new comment, not a request to change the fix's approach. FindingsF1 (Major) — the added comment states a merge mechanism the unchanged code does not implement
// The previous part of the same series is the one with the greatest
// last_sequence, whatever events sit between the two deltas — an
// announcement row must not split an assistant message in two
// (#13300 R3-3, symmetric with the write-side identity lookup).
List<String> preceding = jdbc.query("SELECT part_id FROM"
+ " managed_agent_item_part WHERE tenant_id = ?"
+ " AND session_id = ? AND item_id = ? AND"
+ " part_type = ? AND last_sequence = ?",
..., event.sequence() - 1);The query is unchanged by this PR and matches Reproduce (harness cd packages/sdk-java && mvn -B -f managed-agent-server/pom.xml test \
-Dtest=Verify13355TextPartSplitTest -Dsurefire.failIfNoSpecifiedTests=false \
-Dgpg.skip=true -Dcheckstyle.skip=true -Dspotbugs.skip=trueObserved ( The second delta is at sequence 4 and needs The interloper is not synthetic. Attribution, stated separately as the house rules require. The behaviour is pre-existing: base has the identical query, and this PR does not change it. Whether a given interloper should split is a design question — a Minimal suggested fix (comment-only; measured)// The previous part of the same series is the one at sequence - 1: the
// lookup is strict adjacency, so any event that consumes a sequence
// between two deltas opens a new Part. Task announcements must therefore
// stay out of the stream entirely rather than be tolerated here
// (#13300 R3-3).This is a comment-only change with no behaviour to measure: the affected suite's counts are unchanged by construction (checkstyle at head is clean and the file's 4 planted-violation liveness probe confirms the gate reads it). If instead the code is meant to match the original comment, that is a behavioural change to F2 (Suggestion) — the PR orphaned a public interface method and left a write-only fieldDeleting
Two knock-ons:
The repo's own gates do not catch this, and I proved that rather than assuming it: planting an unread Not a merge condition — a cleanup the PR is now the natural owner of, since it removed the consumer. F3 (Note — consequence partly inferred) — the deletion guard dropped the
|
yiliang114
left a comment
There was a problem hiding this comment.
Approving at a43bbc7. Re-checked the three Critical fixes at this head:
- The record-side execution mapping is correct:
&&binds tighter than||, so SETTLED/cancelled maps tonot_started_provenexactly whendispatchGeneration == 0, and ToolExecutionRecord enforces the claimed-⟺-positive-generation invariant the arm relies on. Thesettled-cancelled-unclaimedfixture row discriminates in both languages, and the wire-vs-record divergence is pinned to exactly that row plusdispatching. - Commit-gate parity holds. The store parses as strictly as the reopen reader (nesting depth 64 via the shared MAX_JSON_DEPTH, strict duplicate keys, no trailing tokens), and requireEvent/requireDomainCommitted cover the envelope, sequence, id, scope binding, time, the 17-kind vocabulary, payload/subject shapes, and both directions of the reserved-
<domain>:<n>rule. Every domain.committed payload is checked whether or not its domain parses, so the reviewed store-now-brick-at-reopen path is closed. - The announce() removal is complete: no consumer of a public-stream
task.updatedremains, and the deletion guard re-reads the session status FOR UPDATE at the same transaction point where main's appendLiveSessionEventIfAbsent already took that row lock, so no new lock-ordering edge. The early return skips only the derived journal; the authoritative record row is already written above it.
CI is green on this head. Two non-blocking nits inline.
|
Gate pass — with one note up front: this PR is already merged ( Template ✓ — every required heading is present and filled in, including the Risk & Scope bullets and a full Chinese translation. One nit: the template asks you to link both language versions of a design doc when the change updates one; the body says the pairs were updated but doesn't link them. Problem — observed, not theoretical. All three defects came out of the post-merge review of #12855 and are tracked as the Critical group of #13300, and each one lands with a named witness plus a mutation witness (removing the Direction — aligned. These are the shared mapping and validation paths that Stage H4–H6 domains inherit, so landing the Critical group before opening new domains for submission is the right order. No CHANGELOG reference, and none expected: this is managed-agent server internals with no CLI-surface analog. Size — 2,096 changed lines over 29 files, broken down:
So ~393 production lines: under the 500-line core threshold and well under the 1,000-line large-PR advisory. Title type is Core paths are touched, but narrowly: the only Approach — the scope feels right, and the third fix took the simpler of the two available paths: rather than teaching the projection to look past announcement rows, it stops writing announcements to the public stream at all, which removes a whole class of interference instead of special-casing one event type. The gate did grow during review (subject-must-be-object, the reserved Risk — no elevated signals. None of the 29 files match the high-risk path set (no shell execution, MCP client/pool, streaming parser, content generator, ACP, sandbox or relaunch paths), so no extra review depth is prescribed on that basis. The real risk is the one the body names itself: commit-time validation is newly strict and the blast surface is every commit writer. Moving on to code review. 🔍 中文说明门禁通过 —— 先说明一点:本 PR 已经合入( 模板 ✓ —— 所有必需小节齐备且填写完整,包括 Risk & Scope 三条与完整的中文翻译。一个小瑕疵:模板要求在改动设计文档时附上中英两版链接,正文说明已同步但没有给出链接。 问题 —— 已观测,非理论加固。三条缺陷均来自 #12855 的合入后审阅,并作为 #13300 的 Critical 组跟踪;每条都带命名见证与变异见证(去掉 方向 —— 对齐。这三处是 Stage H4–H6 各 domain 共用的映射与校验路径,在开放新 domain 提交之前先落 Critical 组,顺序正确。CHANGELOG 无相关条目,也不该有:属于 managed-agent 服务端内部,没有 CLI 界面类比物。 规模 —— 29 个文件、2,096 行改动,拆分如下:生产逻辑(Java 即约 393 行生产改动:低于核心路径 500 行阈值,也远低于 1,000 行大 PR 建议线。标题类型为 确实触及核心路径,但范围很窄: 方案 —— 范围合理。第三条修复在两条可行路径里选了更简单的那条:不是教投影绕过公告行,而是彻底不再向公共流写公告,消除的是一整类干扰而非单个事件类型的特例。评审过程中门禁确有增长(subject 必须是对象、保留 风险 —— 无升级信号。29 个文件均未命中高风险路径集合(shell 执行、MCP client/pool、流式解析器、content generator、ACP、sandbox、relaunch 均无),因此不额外规定评审深度。真正的风险就是正文自己点出的那条:提交时校验变严,波及面是每一个 commit 写入方。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewStatic review of the diff at What follows is one item worth a real follow-up and five smaller ones. None of them is a correctness regression against the pre-PR state — the gate is strictly stricter than before, the mapping is strictly more accurate, and the split cause is gone. They are about what the change claims versus what it shipped, and about state it left behind. 1. The public contract still advertises the event this PR removed, and the design doc says it was updated (follow-up)This is the one I'd act on. The PR drops But The design doc this PR updated claims the opposite on both counts:
Neither is true of the shipped spec. On today's Suggested follow-up (small): drop the two 2. The commit gate's javadoc claims more than the gate enforces — and more than its own design doc says
The design doc item 2 in the same PR is accurate and explicitly narrower — per-kind payload schemas and subject rules, the commit marker's sequence body and digests, the scanner's per-log state and the per-domain event count "are not re-derived here … so only a writer that bypasses the authority could still store a line that the next open refuses." I checked the code and the doc is the correct one: One narrower case I did confirm as reader-equivalent: a bare event-subtype line with no envelope, which the old code tolerated as inert, is refused by both sides ( The new 3. The new comment on
|
| File | What changed |
|---|---|
packages/sdk-java/…/store/ManagedExtensionRecordStore.java |
The substance of the PR. Every journal line now goes through a per-line gate: closed envelope with an optional object subject, version 1, declared sequence, well-formed id kept out of (or inside, for Stage H) the reserved domain namespace, this Session's key, a valid time, a kind from the shared vocabulary, and a per-kind byte cap. domain.committed payloads are checked whether or not the domain parses. At most one Stage H record per transaction. announce() is deleted and replaced by a FOR UPDATE deletion guard in front of the journal append. |
packages/sdk-java/…/store/ManagedExtensionRecords.java |
Adds the 17-name EVENT_KINDS mirror, opens oneOf to the package, and converts the refusal messages on the per-line path to lazy Suppliers so the hot path stops concatenating strings it throws away. |
packages/sdk-java/…/store/ManagedExtensionProjection.java |
executionOf gains a dispatchGeneration parameter: a SETTLED record with status cancelled at generation 0 is now not_started_proven, since the record contract refuses a non-positive claimed generation. A claimed cancel stays settled. |
packages/sdk-java/…/store/ManagedSessionStoreModels.java |
Names the two line caps (1 MiB event, 64 KiB commit marker) that were previously literals. |
packages/sdk-java/…/store/ManagedSessionStore.java |
One literal swapped for the new shared cap constant. |
packages/sdk-java/…/store/ManagedAgentStore.java |
Comment only — see finding 3. |
packages/core/src/managed-runtime/contracts/managed-extension-projection-v1.fixtures.json |
Every Broker row gains dispatchGeneration, plus the new discriminating settled-cancelled-unclaimed row. |
packages/core/src/managed-runtime/contracts/managed-session-store-v1.fixtures.json |
Pins maxEventBytes, maxCommitMarkerBytes and the 17 event kinds as the single shared source. |
packages/core/src/managed-runtime/managed-extension-projection.test.ts |
Asserts the wire/record divergence set is exactly dispatching and settled-cancelled-unclaimed. |
packages/core/src/managed-runtime/managed-session-store-contract.test.ts |
Asserts the TS limits and kind list equal the fixture, in order. |
docs/design/2026-09-27-managed-extension-authority.md + .zh-CN.md |
Decision 10 rewritten for the generation arm; Java store items 1, 2 and 4 rewritten for the gate and the journal move; fixture counts updated. Both languages changed in step. Carries the spec claim in finding 1. |
docs/design/2026-09-27-managed-extension-record-contract.md + .zh-CN.md |
Two-line status touch-ups. |
packages/sdk-java/…/HostedCommittedEventLineReplayIT.java (new) |
Runs fixture-minted event lines through the real TypeScript reader under node plus tsx, so a Java fixture drifting from the TS authority fails here while the Java suites stay green. |
packages/sdk-java/…/TurnEventLines.java (new) |
Authority-valid event-line composer for the tests that previously minted off-vocabulary lines. |
| …and 14 more test files | Legacy expectations rewritten toward authority-valid lines (a fictional tool.progress kind becomes tool.intent, bare {} commit markers become a real marker), the three stream-announce channel tests removed, the racing-proxy mid-commit IT replaced by a deterministic guard IT, plus the negative and mutation-witness arms. |
Testing evidence
This run carried CI evidence only. Per the gate's rules an unattended run never builds or executes PR-derived code, so nothing here was re-run locally and there is no tmux capture — the change has no TUI surface anyway (N/A for real-scenario testing). Everything below is the PR's own CI on the reviewed commit, fetched through the check-runs API: real names, real conclusions.
All 25 substantive lanes are green on a43bbc7b, including the three that carry this PR's weight — the Java matrix, the MySQL/MariaDB contract lanes and the TS unit lane. The only two non-success entries are bot orchestration on the same SHA, not PR CI: review-pr (cancelled) and fallback-comment (queued). Test (macos-latest), Test (windows-latest) and Integration Tests (CLI, No Sandbox) are skipped by their path filters, which is worth knowing since the body's local runs were on macOS — the Linux lane is what CI actually exercised.
| Check | Conclusion |
|---|---|
| Test (ubuntu-latest, Node 22.x) | success |
| Lint & Static (ubuntu-latest, Node 22.x) | success |
| Integration Tests (no-AK, No Sandbox) | success |
| ubuntu-latest / Java 21 | success |
| ubuntu-latest / Java 17 | success |
| ubuntu-latest / Java 11 | success |
| macos-latest / Java 21 | success |
| windows-latest / Java 21 | success |
| Runtime Broker and Managed Agent MariaDB / Java 21 | success |
| Hosted process fault gates / MySQL 8.4 / Java 21 | success |
| Real daemon E2E / Java 11 | success |
| Flyway migration version uniqueness | success |
| Desktop Shell (ubuntu-22.04) | success |
| Desktop Shell (windows-2022) | success |
| web-shell E2E Smoke (ubuntu-latest, Node 22.x) | success |
| label | success |
| Test (macos-latest, Node 22.x) | skipped |
| Test (windows-latest, Node 22.x) | skipped |
| Integration Tests (CLI, No Sandbox) | skipped |
Sandboxed verification would settle one thing this review could not: @qwen-code /verify — that the commit-time gate really equals the reopen reader. Finding 2 is exactly the gap, and it is not observable from the diff or from a green suite, because the suite passes with the gate accepting a payload the reader would refuse. The PR is merged, so this is for the follow-up rather than a merge precondition; /tmux does not apply (no TUI surface).
Not verified, and why: the author's local numbers (965 Java tests, 2135 TS tests, ManagedAgentMySqlIT 20/20 on a fresh MySQL 8.0.46, Flyway 45/45) are the author's claim from the PR body, not evidence I reproduced — the corresponding CI lanes above are green, which is the signal I can stand behind. I also did not verify whether any production writer (as opposed to a test helper) emits journal lines the new gate refuses; the body discloses that the blast surface is every commit writer, and finding 6 names the one configuration whose behaviour I could not pin down.
中文说明
代码审查
针对 a43bbc7b 的 diff 及同一提交的上下文代码做静态审查(本地检出早于合入,故通过 API 读取)。在看 diff 之前我先写下了自己对三条缺陷的方案:改读记录自身的派发认领代际而非从 settle 状态推断;把读取器的词汇表与字节上限提到共享契约里,并在提交事务内对每一行执行;对于 Part 劈分,与其教投影绕过公告行,不如不再向公共流写公告——因为 #13265 之后 bounded 任务 journal 已经是被服务的 feed。三条本 PR 都与我的方案一致,第三条还选了两者中更省的那条路:消除干扰源,而不是在读模型里为单个事件类型开特例。我没有想到既能关闭缺陷又更简单的设计。
以下是一条值得认真跟进的问题和五条较小的问题。它们都不是相对 PR 之前状态的正确性回退——门禁严格变严、映射严格变准、劈分成因已消失。问题在于「声称的」与「实际落地的」之间的差距,以及改动遗留下来的状态。
1. 公共契约仍在宣告本 PR 删掉的事件,而设计文档称已更新(后续项)。 这是我会真正去处理的一条。PR 彻底移除了流中的 task.updated:announce() 被删除,AgentStateStore.appendLiveSessionEventIfAbsent 在 main 上已无任何调用方(代码搜索只返回接口声明与 ManagedAgentStore 实现),PR 自己的测试也在两处断言流中 .doesNotContain("task.updated")。但 a43bbc7b 上的 OpenAPI 规范版本为 1.31.0、未被本 PR 触及,且仍在两处描述里声称由 task.updated Session 事件宣告任务视图的每次变化(顶层版本沿革的 v1.19 那句,以及任务列表路由描述,连 data.taskId、data.state 都写着)。按公共契约订阅的客户端会等一个永不到来的事件。而本 PR 更新的设计文档在两点上都声称相反:把「宣告移出 Session 事件流」记为 1.32.0,并称列表路由已改为记录按任务事件 journal 与删除后不再追加。落到 main 上规范已是 1.33.0,其中 v1.32 是 W2 cwd 切片、v1.33 是 Stage H5 channel,都没有记录这次移动,两句陈旧的 task.updated 也仍在。建议的小后续:删掉规范里这两句、把 journal 与删除跳过规则写进契约并递增版本;若规范改动是有意留给某个切片,那就改文档——因为文档现在是读者唯一会去看的地方,而它说契约已更新。
2. 提交门禁的 javadoc 声称的比它执行的更多,也比它自己的设计文档更多。 apply() 的 javadoc 写「读取器会在下次打开时拒绝的行,这里就拒绝,因此没有提交能写坏自己所写的 Session」。同一 PR 的设计文档第 2 条是准确且明确更窄的:按 kind 的 payload schema 与 subject 规则、提交标记的 sequence 正文与摘要、扫描器的 per-log 状态、按 domain 的事件计数「均不在此重新推导……因此只有绕过 authority 的写入方仍可能存下下次打开会被拒绝的行」。我核对了代码,文档是对的那一方:Java 侧 EVENT_KINDS 只是 17 个名字的平面词汇表,PAYLOAD_FIELDS 只覆盖 domain.committed,而 TS 读取器执行 EVENT_SCHEMAS[kind]、payload 闭键、必填字段、assertPayloadRules、subject 交叉校验与 actor 类别。所以一条信封闭合、kind 合法、sequence 在位但 payload 会被读取器拒绝的事件行,今天仍能提交成功,并仍在下次打开时让 Session 变砖——正是 javadoc 声称已不可能的失效形态。较窄的一种情形我确认为与读取器等价:旧代码当作惰性而容忍的「无信封的裸事件行」,两侧都拒绝(parseManagedSessionEvent 同样因缺 v/kind/payload 失败),所以那处的变严是站得住的。新增的 HostedCommittedEventLineReplayIT 是个真正的好主意——把 fixture 生成的行喂给真实的 TypeScript 读取器,这是 oracle 而不是镜像断言——但它自己也点破了机制(「不重新推导按 kind 的 payload schema」),且只回放四种合法形态,因此抓不到这一类。建议:把 javadoc 收窄到门禁实际执行的范围(设计文档已有现成措辞),或者加一条负向回放,生成会被读取器拒绝的 payload 并断言提交拒绝它——那条测试今天会红,而这正是写它的意义。
3. materializeText 上新增的注释描述了一个并不存在的查询。 本 PR 在该方法只加了四行注释、无代码改动,注释称「同系列的前一个 Part 是 last_sequence 最大的那个,无论两条增量之间夹着什么事件」。紧邻其下的查询是 … AND part_type = ? AND last_sequence = ?,绑定 event.sequence() - 1——精确前驱,而非最大 last_sequence。任何夹在两条增量之间的事件仍会切断 Part,在今天的 main 上与 a43bbc7b 上完全一样。这本身是有意的行为(同一 turn item 的两次文本之间夹一次工具调用,应当产生两个 Part),而修复之所以成立,是因为写侧不再发出公告——注释的后半句说对了这点。前半句把读侧并不具备的容错算到了读侧头上,而且恰恰写在这个注释要解释的缺陷(#13300 R3-3)上。值得改一行,免得后来者基于错误的不变量继续搭建。
4. 移除 announce() 留下的死状态。 private final AgentStateStore sessions 在构造器里被赋值之后再未被读取——announce() 是它唯一的消费者。因此两个 AgentStateStore 构造参数现在注入的是一个用不到的依赖;单参构造器从包内可见放宽为 public(新 IT 位于父包,这部分是必要的),但它的 javadoc 仍写着「 beside 无公共 Session 表的 store,什么都不宣告」——这句话如今已无所指,因为谁都不宣告了。AgentStateStore.appendLiveSessionEventIfAbsent 成为死公共 API:接口方法加约 20 行实现,自带一次 FOR UPDATE 读,且无调用方。
5. OPENING_COMMAND_QUERY 现在成了同一条 SQL 的两份副本。 常量还在,但唯一的读者变成了 ManagedAgentMySqlIT 的执行计划探针;生产调用点在 applyRevision 里内联了一份文本完全相同的字面量。同一次改动删掉了说明「为什么两者必须相同」的那段注释——「逐字就是 ManagedAgentMySqlIT 所解释的语句,因此它探测的计划就是 store 实际拿到的计划」。如今两者之间没有任何联系,IT 可以静默地继续为一个生产已不再发出的查询解释执行计划,而那正是这个常量存在的唯一理由。
6. 删除守卫是无条件的——在原本不加锁的路径上新增加锁(疑问,非阻断)。 isBeingDeleted 对每一次任务视图变化都会执行,与 store 的装配方式无关。旧的 announce() 在 sessions == null 时立即返回,因此经由 ManagedExtensionRecordStore(JdbcTemplate) 构造的 store——也就是 ManagedSessionStore(JdbcTemplate) 使用的形态,非测试调用方有 WorkspaceCsiWorkerAckMain——此前完全不触碰 Session 行,现在会在记录提交内对其加 FOR UPDATE。行缺失的语义也翻转了:appendLiveSessionEventIfAbsent 在 session.isEmpty() 时提前返回,而 isBeingDeleted 把行缺失映射为「未在删除」并照常写 journal。锁形态本身没问题,正文的说法也成立:我核对了 appendLiveSessionEventIfAbsent 确实对 managed_agent_session 做同样的 SELECT … FOR UPDATE、使用同样的 DELETING/DELETED 词汇,因此对已装配的配置而言这确实「与 main 上 announce() 的形态相同」。我无法从 diff 确定的是:未装配的那种配置在生产中是否真的会提交 task-kind 记录,也就是新增加锁与行缺失路径在那里是否可达。
测试证据
本次只携带 CI 证据。 按门禁规则,无人值守运行绝不构建或执行 PR 派生的代码,因此这里没有任何本地重跑,也没有 tmux 抓取——该改动本身也没有 TUI 界面(真实场景测试记为 N/A)。以下全部是该 PR 自己在被审提交上的 CI,通过 check-runs API 取得:真实的检查名与真实的结论。
a43bbc7b 上 25 条实质车道全绿,其中承载本 PR 重量的三条——Java 矩阵、MySQL/MariaDB 契约车道、TS 单元车道——均为 success。仅有两条非 success 是同一 SHA 上的机器人编排任务而非 PR CI:review-pr(cancelled)与 fallback-comment(queued)。Test (macos-latest)、Test (windows-latest) 与 Integration Tests (CLI, No Sandbox) 因路径过滤被 skipped;这点值得知道,因为正文的本地验证是在 macOS 上做的——CI 实际跑到的是 Linux 车道。
沙箱化验证能了结一件本次审查无法了结的事:@qwen-code /verify —— 提交时门禁是否真的等同于重开读取器。第 2 条正是这个缺口,它既无法从 diff 看出,也无法从绿色套件看出,因为即使门禁接受一个读取器会拒绝的 payload,套件照样通过。PR 已合入,所以这是给后续项的,而不是合入前置条件;/tmux 不适用(无 TUI 界面)。
未验证项及原因:作者的本地数字(Java 965、TS 2135、全新 MySQL 8.0.46 上 ManagedAgentMySqlIT 20/20、Flyway 45/45)是正文中作者的自述,不是我复现的证据——上述对应的 CI 车道为绿,这才是我能背书的信号。我也没有验证是否存在生产写入方(而非测试辅助)会发出被新门禁拒绝的 journal 行;正文已披露波及面是每一个 commit 写入方,第 6 条则点名了我无法确定的那一种配置。
— Qwen Code · qwen3.8-max-2026-09-02
Reviewed at a43bbc7bbdd95a02d539aab4bd67c644220ea028 · re-run with @qwen-code /triage
|
Confidence: 3/5 — the three fixes are correct and better tested than most PRs that land here, but two of the change's own claims about what shipped don't survive checking, and one of them is in the public contract. Stepping back. I came in expecting to spend my time on the mapping arm, since a one-token precedence mistake in The third fix is the one I'd have written. Faced with "an announcement between two deltas splits a message", the tempting change is in the projection: look further back, skip non-message events. Dropping the announcement from the public stream instead removes the interference source and leaves the read model alone — fewer moving parts, and the bounded per-task journal was already the served feed after #13265, so this deletes a redundant channel rather than adding one. Deleting three tests that asserted the removed channel, and replacing a racing-proxy IT with a deterministic guard IT for the same property, is the right call and is disclosed rather than buried. So why not higher. Because the two things I'd most want a future maintainer to be able to trust — the public contract and the invariant comment on the gate — are both wrong after this change, and both were introduced or rewritten by it. The spec still tells clients a The smaller items point the same way: a comment on On the gate's own questions: the problems are real and observed — they came out of a post-merge review of #12855 and each lands with a named witness plus a mutation witness, so this is not theoretical hardening. I checked the fixes against the code at the reviewed commit rather than accepting the framing, and that's what turned up findings 1 and 2. Scope grew during review, but the Suggestion groups stayed in #13300 instead of riding along, which is the discipline the repo asks for after several rounds. Six months from now I'd thank the author for the mapping and the gate, and curse the Verdict: defer — and no review action is submitted. The PR merged at 16:29 UTC with approving reviews from @qqqys and @yiliang114 pinned to this exact commit, so both @wenshao — six follow-ups, in the order I'd take them. #13300 stays open for the Suggestion groups, so it's the natural home; only the first is user-visible.
Escalating to @yiliang114 as well, since the confidence cap here is a judgement about contract truthfulness rather than anything I could settle from the diff. One process note: the skill's deterministic owner resolver runs through 中文说明信心度:3/5 —— 三条修复本身正确,测试质量也高于本仓库多数合入的 PR,但这次改动对「落地了什么」的两处自述经不起核对,其中一处还在公共契约里。 退一步看整体。我原本以为时间会花在那条映射分支上: 第三条修复正是我会写的那种。面对「两条增量之间夹一条公告会把消息劈开」,诱人的改法在投影侧:往回多看几行、跳过非消息事件。而本 PR 选择不再向公共流写公告,消除的是干扰源,读模型保持不动——活动部件更少;何况 #13265 之后 bounded 按任务 journal 已经是被服务的 feed,所以这是在删掉一条冗余通道,而不是新增一条。删掉断言已移除通道的三个测试、用确定性的守卫 IT 替换竞态 proxy IT 来断言同一性质,这个取舍是对的,而且是明确披露而非偷偷带过。 那为什么不给更高。因为我最希望后来的维护者能够信任的两样东西——公共契约,以及门禁上的不变量注释——在这次改动之后都是错的,而且都是由这次改动本身写入或改写的。规范仍告诉客户端「由 较小的几条指向同一处: 回到门禁自己要问的问题:问题真实且已观测——它们出自 #12855 的合入后审阅,每条都带命名见证与变异见证,因此不是理论加固。我是拿被审提交上的代码去核对修复,而不是接受其叙述框架,findings 1 与 2 正是这样查出来的。评审过程中范围确有增长,但 Suggestion 组留在 #13300 而没有搭车,这正是仓库在多轮评审之后要求的克制。六个月后回看,我会感谢作者带来的映射与门禁,也会骂那句 结论:defer —— 且不提交任何评审动作。 PR 已于 16:29 UTC 合入,@qqqys 与 @yiliang114 的 approve 均钉在这一提交上,因此 @wenshao —— 六条后续项,按我会处理的顺序。#13300 为 Suggestion 组继续开着,是它们的自然归属;其中只有第一条对用户可见。
同时转给 @yiliang114:这里的信心度上限是关于「契约真实性」的判断,而不是我从 diff 里能了结的事情。一条流程说明:本 skill 的确定性 owner 解析器需要经 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
Sandboxed verification: Skipped because the PR is not open for verification (state=MERGED, draft=false). 中文 — 判定:
|
…-harness Absorbs fc42ab9 (models.dev catalog keys), 07e905a (mobile), ac81c07 (#13355, H0c managed child_run + record-follow-ups), b585508 (#13407, native approval cards) and ac497ae. No conflicts; marker greps on the branch's recovery witnesses hold. Verified on the merged tree: hosted suites 275/275, typecheck clean, HostedHarnessClientTest 32, QwenHostedHarnessConnectorTest 38, HarnessCoordinatorTest 60 (incl. the round-10 pacing witness), ManagedAgentServerIntegrationTest 29. One class is RED on main itself, not on this merge: ManagedSessionStoreIntegrationTest. holdsRestorePagesInsideThePerPageByteBudget fails identically on a pristine ac81c07 checkout locally (fd4af70 clean, ac81c07 red, both standalone) — a main-side regression absorbed here; reported upstream with the attribution chain.
…e-budget test How: holdsRestorePagesInsideThePerPageByteBudget now builds each dense transaction from 210 well-formed message.delta lines (text at the reader's 4096-byte cap, ~1 MB per transaction) plus the commit marker, through a shared TurnEventLines composer, and asserts that revisions 1..9 fit the 8 MiB page budget while a tenth does not. The reader replay IT now also replays that delta shape. The class turns off MockMvc print-on-failure. Why: #13348 added the test with placeholder lines ({"subtype":"event_v1"} plus a padded commit_v1 line), and #13355 (merged later, never run against it) made the commit gate refuse any non-event line inside the event range, so every commit after genesis answered 409 and main's SDK Java lanes went red. The failure was masked as timeouts: Spring Boot printed the failed test's ~1.3 MB request body as one log line, which stalled the Actions log pipeline for ~45 min, so the Hosted Verify and O4 steps blew their ceilings and the MariaDB lane hit its job timeout. Test: ManagedSessionStoreIntegrationTest and ManagedExtensionRecordStoreTest green; full managed-agent-server surefire (1077) and checkstyle green; HostedCommittedEventLineReplayIT green through the real TS reader and red when the delta text exceeds 4096 bytes; dropping the page byte budget in ManagedSessionStore fails the test (10 rows instead of 9) with no log line over 400 chars.
…e-budget test (QwenLM#13551) How: holdsRestorePagesInsideThePerPageByteBudget now builds each dense transaction from 210 well-formed message.delta lines (text at the reader's 4096-byte cap, ~1 MB per transaction) plus the commit marker, through a shared TurnEventLines composer, and asserts that revisions 1..9 fit the 8 MiB page budget while a tenth does not. The reader replay IT now also replays that delta shape. The class turns off MockMvc print-on-failure. Why: QwenLM#13348 added the test with placeholder lines ({"subtype":"event_v1"} plus a padded commit_v1 line), and QwenLM#13355 (merged later, never run against it) made the commit gate refuse any non-event line inside the event range, so every commit after genesis answered 409 and main's SDK Java lanes went red. The failure was masked as timeouts: Spring Boot printed the failed test's ~1.3 MB request body as one log line, which stalled the Actions log pipeline for ~45 min, so the Hosted Verify and O4 steps blew their ceilings and the MariaDB lane hit its job timeout. Test: ManagedSessionStoreIntegrationTest and ManagedExtensionRecordStoreTest green; full managed-agent-server surefire (1077) and checkstyle green; HostedCommittedEventLineReplayIT green through the real TS reader and red when the delta text exceeds 4096 bytes; dropping the page byte budget in ManagedSessionStore fails the test (10 rows instead of 9) with no log line over 400 chars.
…wenLM#13174) * feat(managed-agent): adopt the next Hosted Harness generation (G3) A Hosted Session no longer dies with the Harness process generation that first served it (QwenLM#12952 G3, Steps 1+2). On a generation signal the control plane renegotiates once through the connector instead of failing every bound Session until its own restart; pending Turns re-attach through the takeover load as their retries come due. Takeover refusals split into a typed terminal decline (deterministic in the journal) and the existing retriable code; the takeover load is idempotent against a lost reply and gets its own timeout; negotiation requires the journal-contract feature token so a rolled-back Harness is refused once; a marked-but-never-admitted Turn withdraws its submission mark, backstopped by journal commandId idempotency. The failover runner gains a Harness-only restart arm on all three scenarios (harness_boot_id moves with no Java restart) and a SIGSTOP/SIGCONT frozen former-owner fencing arm, wired into hosted-harness-mysql together with the previously local-only basic mode. Design docs included in English and Chinese. * fix(managed-agent): tolerate the bootstrap 404 of a restarting Hosted Harness The serve delegating app answers every routed path with a bare pre-contract 404 while its runtime is still starting. The Harness-restart arms raced that window: a prompt submitted milliseconds after the new process bound the port could meet the bare 404, which the client read as a generation contract violation and ended the Turn as hosted_harness_protocol_error without any retry. A missing boot header on 404 now maps to a transient transport error so the coordinator retries through the window, and the failover runner waits on deep health (?deep=1) before driving turns into a restarted Harness. Terminal turn failures now also log their underlying exception. * fix(managed-agent): freeze only the Harness writer in the former-owner fencing arm SIGSTOPping both original owners left the original Spring's workspace binding unreclaimable: the reclaim proves death from /proc liveness, and a stopped JVM still reads as alive there, so the replacement's reconcile timed out (runtime_broker_reconcile_timeout) and the Turn never completed. The arm now stops only the original Harness (reclaiming its workspace binding requires the Spring to die anyway — the continuation mode's crashProcess), completes the Turn with a full replacement, then wakes the frozen writer and asserts the journal, transcript, and session binding are untouched. Design D7 updated in both languages with the discovered boundary: resource-level takeover of a surviving owner is structurally out of reach today, which is exactly what this issue's sequential-generation scope excludes. * ci: retrigger sdk-java workflow after event delivery stall * fix(managed-agent): pin the fencing proof on writer identity, not head revision The first merge-base run of the freeze arm failed its own gate: after waking the frozen Harness the journal head moved (rev 30 -> 45) while the writer generation, boot binding, transcript, and terminal count all held. The moved revisions were the replacement's own legal churn, not old-writer mutation, so the head-equality assertion conflated the two. The exit-check claim is identity: no transaction out of the old writer generation after it wakes. Count that straight off qwen_managed_session_journal_tx (which carries writer_generation), keep the binding/transcript/terminal assertions, and align design D7 in both languages. The error string now also reports the old-writer tx count. * fix(managed-agent): qualify the journal tx schema in the freeze-arm gate The identity-based old-writer count queried qwen_managed_session_journal_tx unqualified; the runner's MySQL connection has no default schema, so the arm died with ERROR 1046 instead of evaluating the fence. Qualify it as qwen_managed_agent.qwen_managed_session_journal_tx like every other query in this runner. No behavior change to the assertions themselves. * fix(managed-agent): harden takeover correctness from the R1 review - settle turn_settled checkpoints through the load route's own tail instead of a terminal decline, and keep transient store verdicts (missing_state/missing_checkpoint) retriable on both authorization reads - exempt only takeover-shaped 409s from the pre-admission retry budget; every other failure kind meets it - scope the 120s load-timeout to recovery loads so plain attaches under the CHM bin locks stay on 30s - adoptGeneration: move map churn out of the connector monitor (lock-order inversion), evict only stale-boot entries on equal-boot mismatches - answer a duplicate submit of an already-settled prompt with a 202 replay anchored at its input.accepted sequence - withdrawSubmissionAttempted: guard on the same status window as its peer CAS methods; complete capability mismatch terminally in both remaining coordinator layers - runner: freeze-arm pre-freeze liveness assertion, /health-proven wake non-vacuity, teardown SIGCONT, exact-string dispatch-generation bounds - CI: job ceiling 95 with corrected ceiling math; pin all seven failover arms in hosted-process-ci.test.js - sync both design-doc languages with the shipped behavior * fix(managed-agent): close the R2 review round on the replaceable-host-harness slice - withdraw + cancel interference (R2-1, Critical): a CANCELLING Turn whose never-admitted mark was withdrawn is fast-cancelled instead of being re-dispatched into a second live execution at the adopted generation - the generation-mismatch catch escapes the pre-admission budget only on a bound Session, like every sibling arm (R2-3) - bound the remote-supplied decline reason to the error_message column so the terminal write itself never throws (R2-5) - the last fail(...) in the catch ladder is failTerminally with the exception logged, like its siblings (R2-6); the withdrawal itself is logged too (R2-7) - freeze arm: the replacement Spring inherits the original port so the woken writer meets a live fencing control plane, not a dead socket (R2-2); the wake block asserts the head actually advanced past the frozen owner's writer generation, drops the wrong heartbeat claim (R1-8 follow-up), separates the two disk-audit meanings, and asserts the retained Harness home (R2-8) - the hosted /capabilities feature list derives from the registry instead of a hand-maintained literal (R2-9) - docs: D7 scoped to the journal half of the Q2 exit check, the binding half and the pre-contract-404 classification recorded in Boundaries (R2-4, R2-10) * docs(sdk-java): correct the load-timeout coverage comment in the client test * fix(managed-agent): reference the frozen Harness through the children registry in teardown The R1-4 teardown SIGCONT referenced the try-block-scoped `harness` binding from the finally block, so every freeze-arm run died with ReferenceError after all fencing assertions had already passed (CI run 37009843147). Reach the child through the children registry by its start() name instead. * docs(managed-agent): scope the adoption sentence to this slice's takeover reach * fix(managed-agent): close the R3 review round at its five shared roots - retract the turn_settled settle tail to a typed decline: the tail answered 200 with no recovery watermark (Java then failed the bind, orphaned a completed journal turn), fabricated state:'completed' on phase alone, ran mutating work under a passive load, and retained no snapshot for the lost-reply retry. D10 already owns rebind-and-keep-reading as the Step 3 row (R1-25-re, R3-2 .. R3-5) - the load route's restore guard applies the same durable-vs-transient verdict read, because the bundle collapses both into one blocked bit and the durable decline was unreachable from production (R1-26-re) - the pre-admission exemption keys on lease-shaped 409 codes only: configuration-shaped 409s (e.g. hosted_tool_profile_conflict) meet the budget, and the same 409 exempts nothing on an unbound Session (R1-1-re, R1-30-re, R3-13) - equal-boot adoption keeps pendingRecovery: the markers were minted by the same live client (R1-29-re) - lifecycle capability mismatch: revert completing-as-unconfirmed — that skipped the drain, flipped the session, and recorded a clean row with no FAILED vocabulary to tell it apart; the reason-loud retry is the honest end of the line until an operator realigns versions, and the README sentence says so. Action-response outbox keeps its honest FAILED-with-code terminal (R1-45-re, R3-5, R3-15) - test hardening: concurrency close-once now surfaces worker assertion failures via Future.get and pins that no real client build happened (R3-12); loadTimeout-vs-requestTimeout discriminated at the call site (R3-9); single-walk acceptedInputSequence (R3-18) - runner: 5-column execution queries pin runtime_session_id unchanged under --harness-only (R3-14); frozen-arm teardown looks the child up by a shared label constant and fails the run on a drift (R3-17) - CI: ceiling 105 covers the nine summed step ceilings (92) plus the uncapped setup steps (R3-6); the seven-arm pin also asserts each script's flags and the install step's existence (R3-16) - docs: both languages re-synced (D5 retraction, D9a code allow-list, D7 teardown, D8 runtime identity, honest validation digests, the four missing changes-table rows, README retry clause) * fix(managed-agent): replay takeover snapshots only to a same-shape request A passive load only asks broker.status(), so its snapshot keeps phase='await_runtime'; replaying it to a drive load handed the coordinator a checkpoint that isContinuationReady() can never accept, terminally failing an otherwise continuable Turn. Record the request shape with the snapshot and replay only on a mode match; the clear conditions stay promptId-only. Also make the adoption race test deterministic: a package-private createClient() seam lets the test inject the replacement instead of a real /capabilities call (proven flaky on CI), worker assertion failures now surface through Future.get, and the pendingRecovery fixture seeds its marker through the recovery path. * fix(managed-agent): close the self-audit round on the replaceable-harness slice Three rounds of undirected plus adversarial audit over the whole PR diff (31 files), with three parallel deep-dive passes over the Java, TypeScript and runner/docs surfaces. Every finding was re-verified against the tree before acting; five reported defects were rejected on evidence. Correctness: - the load route read the blocked-restore verdict AFTER managed.close(); sealing the journal makes every later store read fail as "writer is not active", which the authority erases into missing_state, so the durable branch this PR added could never fire and a corrupt checkpoint stayed in the retry-forever class. Read the verdict first, and pin the order with a witness that goes red when the read moves back (the local authority's seal does not disable reads, so only the ordering assertion catches it). - drop hosted_session_already_attached from the lease-bounded 409 set: the daemon removes an attachment only on an explicit detach or delete and this control plane never detaches, so a Spring restart against a surviving Harness retried a permanent refusal forever instead of ending the Turn. - adoptGeneration evicts by boot identity in both branches instead of clearing, so a marker minted by a concurrent rebuild can no longer be erased between two adjacent clears (which left a live attachment with a null recovery report), and markers follow their attachment. - the post-withdrawal fast-cancel reads the row again: a cancel landing after the claim is invisible in the claimed snapshot, and acting on it submitted a live execution only to cancel it below. - a durable verdict re-read after settling now declines as checkpoint_blocked, matching the identical pre-settle classification. - an unrecognized blocked reason throws instead of declining, so a reason core adds later cannot silently become a terminal Turn failure. - the operator-settable load-timeout is validated at connector construction; a bad value previously threw on every client() call and surfaced as an endless transient retry naming only the exception class. Tests: already_attached and lost-withdrawal-CAS witnesses; the fast-cancel witness now pins the re-read (cancel after claim); the concurrent-adoption test surfaces worker assertions through Future.get and injects the replacement through a package-private createClient() seam instead of standing up a live /capabilities call; the CI pin gains each arm's mode flag and a ceiling-sum invariant. Docs (both languages): D1 now describes the boot-identity eviction and lists what this slice really added to HostedHarnessClient; D3 no longer claims the bind-refusal path stops being reached (it is D4's trigger); D6 records the same-shape replay clause; D7's fence rationale no longer blames heartbeats (lease renewal touches writer_lease_until only); D9a names the two exempt codes and why already_attached is not one; the validation digest states which claims are pinned and which are not; the CI arithmetic reads 12 + 8x10 = 92; the stale workflow nit is dropped. * chore(ide): regenerate NOTICES for the merged dependency set (email channel) * fix(ide): replace the workspace-polluted NOTICES with the pristine regeneration The file committed in f7a960c was generated under a polluted module layout left by an aborted install: its resolution hoisted [email protected] and produced 663 total dependencies, so CI's own regeneration (pristine: 662) mismatched and the up-to-date gate failed. Replace it with the pristine worktree's output, which the gate's own regeneration produces to a no-diff state. * fix(managed-agent): close the R4 review finds on the takeover and refusal paths - classify missing_checkpoint and a bare missing_state as durable verdicts so a permanently unrecoverable session declines instead of being retried past the lease window; only a missing_state carrying the erased store error's message stays retriable - map a fenced predecessor's managed_session_writer_conflict to the coded retriable 409 in the load route so the wait provably ends with the lease, and allowlist that code in LEASE_BOUNDED_409_CODES (dropping the inert hosted_prompt_recovery_required, which can only arrive post-mark where the exemption gate is already bypassed); null-guard the membership test against codeless error bodies - replay a takeover snapshot only to a request whose parsed store identity matches the attached session's, never to any takeover-shaped caller - fetch the harness client AFTER resolving the attachment at the six entry points, so an adoption completed during a create/load round trip cannot strand a call on the closed client - pin the reply watermark of the journal-replayed prompt admission, the construction-time loadTimeout guard, the marker-eviction half of adoptGeneration, and the withdrawal CAS ordering with witnesses, and widen the load-timeout test's durations so the capabilities negotiation keeps headroom on contended lanes - narrow the README's typed-decline sentence to the surviving row and generation binding, and mirror the exempt-code list in both design docs * fix(managed-agent): answer the round-5 waits without terminalizing them - a takeover now splits three ways: drivable answers recovered, a deterministically unrecoverable drive keeps its typed decline, and anything whose wait belongs to someone else (a requested approval, or the whole non-drivable space on a cancellation-only load) answers inapplicable so the route falls through to the plain attach — a user-cancelled Turn cancels instead of failing as managed_runtime_recovery_blocked, and an approval wait survives its generation rather than dying while the answer is still deliverable; the coordinator keeps the await_action guard as a retriable path even if a Harness still declines it - an unconfirmed cancellation re-drives with its snapshot restored: the cancel admission's rollback is now symmetric, so the retrying takeover load replays instead of being refused already-attached - an unrecognised blocked reason at the final re-read stays retriable on the drive side like the pre-settle read, instead of terminalizing on unresolved_after_settle; only durable verdicts may end a Turn - the hosted persona's curated tag set is declared once in the registry (hostedPersona descriptor flag) instead of being filtered by a third hand-maintained list at the route, pinned by the docs-contract test - the harness-only arms pin a surviving worker through the runtime session row's own liveness metric, not the insert-frozen id column - witnesses: declined reason on the wire, cancel-report replay after a lost answer, inapplicable shapes at the recovery kernel (both loads), the drive disjunct of the load-timeout predicate, the one rebuild per adoption race, and coordinator corners for cancelling/await_action * fix(ci): qualify the runtime-session heartbeat query with its schema The harness-only arms' liveness beat read qwen_runtime_session bare; mysql runs without a default database, so the Harness-restart in-flight arm died on ERROR 1046 with 'No database selected'. Every other probe in the runner already addresses qwen_managed_agent.<table> — this one now does too. * fix(managed-agent): close the round-6 shape and wedge findings - the pre-kernel no-tool refusal and the restore guard now honour the same shape rule the kernel does: a cancellation-only load answers inapplicable and attaches plain (or the baseline retriable refusal where no attach is possible) instead of terminalizing a Turn the user asked to cancel; a takeover that answers inapplicable no longer trips the bare-load refusal on its empty recovery - the lease-bounded exemption is keyed on the writer lease's own wire code only: hosted_turn_recovery_required covers arbitrary takeover failures, and exempting it wedged durable refusals in an unbounded pre-admission retry; a refusal carrying that code now meets the budget like every other body shape while managed_session_writer_conflict keeps its D9a window - witnesses: the no-tool plain-attach coincidence for cancel vs the typed model_start decline for drive, the durable restore verdict on the cancellation arm, and the budget-hit for a durable 409 on the bound path (the old takeover-409 premise rewritten, its writer- conflict twin already covered) * fix(managed-agent): settle a cancel first on a plain-attached replacement A real-stack round-4 wedge: cancelling a Turn whose Harness died mid model wedge (Work realized after the merge) failed as hosted_harness_generation_mismatch — the plain attach leaves the bind CAS unwinnable (boot-old != boot-new), the cancel was never issued, and everything queued behind the Turn then retry-forever: later prompts 409, a close 409, no public-API escape. runClaimed's recurrence without recovery now cancels before it binds: a CANCELLING Turn does not submit, and a bind exists for submissions — the next Turn's takeover adopt moves the generation when something actually continues. cancelledOnAttach suppresses the tail's later re-cancel. The R5 witness now also pins: bindHarness is never asked, withdrawSubmissionAttempted is never touched, and the Turn is neither recovery-blocked nor generation-mismatched. Design docs D3/D4 gain the bypass rule in both languages. * fix(managed-agent): close the inapplicable aftermath the round-8 review found - an inapplicable takeover must not latch session.blocked — a 200 answer that refuses every settlement route is a lie (R5-2's latch regression: nothing outside settleCancelledHookTurn clears it for a non-hooks session). The blocked latch now splits the same shape rule: only a genuinely unsettled takeover parks there, not an inapplicable answer. - a plain attach of a bound Session after adoption rebinds the Session row with its own CAS (owner + live lease + expected = the row's boot), since bindHarness provably refuses a turn that already posted its epoch — the Turn is neither generation-mismatched nor stranded (R8-1). - a Turn written turn_settled completes, so it is never stamped as managed_runtime_recovery_blocked: the kernel answers inapplicable on BOTH load shapes (the plain attach lets the daemon's own projection, not a declined verdict, write the terminal record), retaining the rebind-and-keep-reading rationale for Step 3 (R8-2). - the QwenLM#13388 pinning test's mock endpoint now advertises managed_session_journal_delta_v1 like every other Harness fixture of this branch — a merger's follow-up angry about the gate (D9c), not an upstream flake. - witnesses: turn_settled pinned as inapplicable on both shapes at the kernel; plainAttachAfterAdoptionRebindsBeforeItSubmits asserting the store CAS adoption; R5-2's corner witnessing 200 without recoveryRequired; coordinator 46/46, hosted client + pinning tests, TS session+recovery fully green. * fix(managed-agent): close the inapplicable aftermath the round-9 review found - the no-tool arm now refuses with the baseline retriable one again instead of minting a plain attach whose parked Turn nothing may resolve (R5-2'), and a takeover load without broker options shares that refusal; - the journal prompt replay answers 202 only when the request body's digest proves identity against the admission its input.accepted holds; carrying the same Id with different bytes is a real prompt_conflict, never a silent swap of watermark (R9-1); - a plain attach after adoption moves the Turn's stream epoch through recordRecoveryAdmission to the attach's epoch, on both the binding ladder and the cancel-on-attach arm — a stream opened with the claimed epoch would be rejected on the new generation as epoch mismatch (R8-1'). - witnesses: the no-tool cancel arm refusing retriably while drive declines with its typed reason, the digest mismatched replay timing out as hosted_prompt_conflict, and R8-1 streaming the adopted generation's epoch on both arms (plainAttachMoves* and cancelOnAttachStreams*) with recordRecoveryAdmission pinned. * deflake: carry QwenLM#13403's pinning fixture under the journal-delta gate + harden the 13328 mount-holder waitFor The merge of QwenLM#13403 turned the Java CI lanes red with DaemonProtocolException: Endpoint does not advertise managed_session_journal_delta_v1 on every burst caller: this branch's D9c capability gate (from QwenLM#13388's pinning work) rejects endpoints without that token, and QwenLM#13403's new /capabilities fixture did not advertise it. Same fixture-parity fix as QwenLM#13388's — the token is a fixture property, not production behavior; the deliberate negative stub (capabilitiesJsonWithoutJournalToken) stays as the gate's witness. The ubuntu Test lane red on the same head was the issue-13328 mount-holder vi.waitFor defaulting to its 1 s window against the admission pipeline on the loaded runner ("expected undefined to be defined", repeated across both retries). Give it an explicit 10 s timeout, assertions unchanged — the same deflake pattern as QwenLM#13411/QwenLM#13323/QwenLM#13430. * fix(managed-agent): close the R11 review round (inapplicable pays only what it can) R11 (qwen-code-ci-bot review, 5 threads, 3 new + 2 re-checks) landed on the shared consequence of R10's fixes: an inapplicable/plain-attach answer stays honest only where a settlement route can pay. - R11-1: the prompt route's session-scope guard now answers the SESSION-level wedge code (hosted_turn_recovery_required); the prompt-scoped code names only the requested prompt's own unsettled duplicate, so the duplicate-admission adopt branch can no longer be forged by a refusal about someone else's parked Turn — and the budget treats the session-level code as pre-admission whenever the Turn provably recorded no epoch, recording the wedge's own code on exhaustion (provenance-gated on an existing submission mark). - R11-2 / R10-1 re-check: both takeover arms compute the bare branch's settle projection inline (`settleProjectablePromptId`): approval waits keep the plain attach (resolve pays), turn_settled is settled by the load itself via the extracted `runSettleProjection`, and an unpayable park keeps the baseline retriable 409 instead of a healthy-looking 200. - R10-2 re-check: the kernel's passive inapplicable is narrowed to those two payable states; initial / durably blocked / model-start / unknown phase / wrong-checkpoint throw into the retriable refusal. - R11-3: createOrLoad's final attachment put restores the pendingRecovery marker for snapshot-carrying passive re-attaches, or the cached branch of a later recoverManagedRuntime answers "nothing parked" over a snapshot it holds. Witnesses: 245/245 CLI across the three hosted suites (2 new route witnesses + updated session-code assertion + 2 recovery-suite flips), QwenHostedHarnessConnectorTest 34/34 (marker witness), HarnessCoordinatorTest 52/52 (session-busy witness; R6 budget witnesses pin the load side as recorded). Docs updated in both languages. * fix(managed-agent): settle the no-tool cancellation takeover instead of refusing forever (Arm B) The round-6 real-stack wedge: a Turn with no Runtime work parked mid model round, its Harness generation dead, cancelled — the no-tool arm of the takeover branch answered the baseline retriable 409 on every cancellation-side redispatch, and with the Turn still carrying its submission mark, the retry budget never fired. Turn 2 stayed CANCELLING forever and every later Turn met the same wall. The fix keeps the wire honest and the settlement explicit: - LoadHarnessSession carries `cancellationTakeover`, set ONLY by recoverManagedRuntime's cancellation arm. A plain passive re-attach shares the passive wire shape and deliberately never sets it — nothing may mint a canned CANCELLED record for a wait whose owner could still exist. - With the signal present, the no-tool arm of the takeover branch settles the ownerless park into the daemon's journal itself (turn_result/cancelled, landing turn.settled); the writer fence on the very load is the safety boundary (it proves the producing generation can never write again). On write failure the arm keeps the baseline retriable 409. Without the signal the arm keeps its merge-baseline answers — retriable 409 on cancel, typed model_start decline on drive — so the collision guard holds by construction. Witnesses: `settles the cancellation of an ownerless parked no-tool Turn on the takeover load` (200 plain attach + durable settle tag + cancel reads settled-at-tail as 204 + next prompt admits; the revert flips the verdicts red) with the collision-guard passive-only rerun still at 409, plus wire-side `carriesTheCancellationTakeoverFlagOnlyOnTheCancellationLoad` asserting the flag travels only with the signal. Verification: TS hosted suites 259/259, qwencode 32/32, QwenHostedHarnessConnectorTest 34/34, HarnessCoordinatorTest 52/52, typecheck, eslint, prettier. Design doc updated in both languages. * fix(managed-agent): converge P1-1 (tool-config vs unpaid Runtime work) and P1-2 (stale approval copies) Round-7 re-review of 275f934 found two remaining wedge families inside the cancellation settlements this PR introduced. Both converge on durable proof, never on the stale checkpoint copy. P1-1: a takeover cancellation must separate "the Session is configured for tools" from "the Turn owes unpaid Runtime work". A files-profile session whose owner died BEFORE its first tool call carried no unsettled Runtime work (checkpointless or a bootstrap checkpoint naming no Turn) — and the kernel's different-Turn check threw on the bootstrap's null turnId, so the load answered the retriable 409 forever. The load route now hoists that split ahead of the kernel: with the explicit cancellationTakeover signal (and only with it — the collision guard stays), any ownerless park without unpaid Runtime work settles into the journal itself; work in flight follows the recovery-cancel report; the no-signal shape keeps its baseline refusal unchanged. P1-2: the durable action record — not the checkpoint's stale copy of the approval — decides an owner-died approval. When the record says the wait ended, the recovery kernel no longer maps it to the inapplicable plain attach: expired/cancelled is transient (route refusal), decided is advanced through its own durable gate ONLY on a drive load (a cancellation never crosses it), and every other shape (still requested, or undefined) keeps the plain attach a resolve route can pay. The cancel/expired arrive by the same separation (settle with the signal; decided is settleable with the signal too — a CANCELLING takeover does not resume approved work no matter which side the user took). Witnesses: `settles the cancellation of an ownerless parked Turn on the takeover load (profile=null|FILE_PROFILE)` parameterized exactly as the re-review demanded; three recovery-kernel witnesses for the cross-read (resolves-inapplicable only while requested; throws for ended-without-a- decision; drive declines checkpoint_blocked on an undrivable decided wait); route-level `settles a cancellation takeover whose durable approval already ended (state=cancelled|decided)`. Design doc updated in both languages. Suites: hosted-harness-session 222→(+10 new), hosted-runtime-recovery 36→(+3 new), issue-13328 33→, all green; typecheck and eslint clean. * chore(vscode-ide-companion): regenerate NOTICES for the merged dependency layout * fix(managed-agent): read owed Runtime work off the work itself and pay coded plain-cancel refusals with the cancellation takeover (R9) Round-9 real-stack measurement found the cancellation separation still wedging on two families: every cancelled Turn after the first, on both profiles, and every cancel row of P1-2 — where no load ever left the coordinator, because a plain-cancel retry can never settle a parked Turn and no redispatch was forthcoming. - The park gate now reads owed work off the work itself: a checkpoint naming an earlier Turn whose tool items all settled and were consumed owes nothing to a cancelled Turn that never reached a tool call; and a no-tool Session cannot owe Runtime work by definition, so its cancellation arm settles unconditionally again (the round-8 behavior the gate had displaced onto a blocked restore basis). - The admitted-cancel path turns a plain cancel's coded 409 hosted_turn_recovery_required into an immediate cancellation takeover load over the same attachment (recoverManagedCancellation), and the connector sends it even with a healthy cached attachment — the shortcut would swallow it and the load would never leave the process. A recovered Runtime park it reports is cancelled through its checkpoint admission; the already-running stream lands the settle either way. Witnesses: hosted-harness-session settles the second-Turn cancellation park on both profiles (the round-9 rig witness adopted); the coordinator drives the takeover load exactly on the coded refusal (plain park and recovered park) and on no other answer; the connector always sends the cancellation load over a cached attachment. Suites: hosted 229, HarnessCoordinatorTest 55, QwenHostedHarnessConnectorTest 35, ManagedAgentServerIntegrationTest 29. The decided-approval drive arm of P1-2 (rows where the user answers allow or the approval expires unanswered) remains open: it needs the plain-attached replacement to drive the parked Turn itself once the durable wait ends, and lands as its own slice. * fix(managed-agent): pay the cancellation signal identically on an attached re-answer (R9-2) A cancellation takeover load the coordinator forces onto a Session ALREADY attached in this daemon used to skip the entire settle separation: the attached branch called the passive kernel directly, which never advances a durable wait passively — so a park whose approval ended after the attach (the round-10 allow, then cancel repro) was thrown back as an unknown phase and refused retry-invariant, while the first-load branch settled the identical shape. Hoist the separation above the attachment split: the attached branch now reads the signal too and runs the same no-tool arm, owed-work gate and durable cross-read, settling through settleCancelledHarnessTurn into the journal the already-running stream lands. Witnesses parameterize both ended states: attach plain while the durable approval says requested, end the wait after the attach with the stale checkpoint copy untouched, then redrive with the cancel signal — 200 with the redriven-load settle log, and the plain cancel route reads settled-at-tail. Suites: hosted session 231, recovery 44. * fix(managed-agent): close the durable wait before the cancelled terminal lands (R9-3) settleCancelledHarnessTurn only wrote turn_result/cancelled into the journal, and the record sink's turn.settled only advances next-turn checkpoints at a model-start phase — so a cancelled Turn parked at an approval wait ended in the journal while its checkpoint stayed at await_action, and the Session's next prompt met `ManagedHarnessBlockedError: await_action is not a model-start phase` (the round-11 repro: old Turn completed cleanly, new Turn failed as hosted_turn_failed before the model was ever called, closing 1 P1). The cancelled settle now closes the ended wait first through the wait's own durable gate (resolveDurableWait → model_output_committed, a model-start family phase) whenever — and exactly when — the durable approval already ended, mirroring the cross-read the caller's gate performed. The decision remains the USER's and nothing resumes: the Turn dies immediately after. Runs of requested waits or of in-flight Runtime work keep the kernel's existing paths. Witness (both answers): requested approval attaches plain on the passive takeover, the record ends after the attach with the stale copy untouched, and the attached cancellation takeover settles — 200 with the redriven-load settle log, plain cancel reads 204, and the settle provably commits the model_output_committed boundary ahead of the terminal record. Suites: hosted session 231, recovery 44; typecheck, prettier, eslint clean. The unit suite cannot stage "approval-first park + literal owner death" today (DELETE refuses active Turns, and abort unwinds the wait cleanly), so the end-to-end relief-Turn acceptance stays with the real-stack probe; the annex-level witnesses cover the settlement payment and the wait-close commit. * chore(vscode-ide-companion): regenerate NOTICES from a clean-room install The previously committed file was generated in the long-lived worktree whose pnpm layout had drifted ([email protected] + [email protected] + fdir/picomatch slot differences) even though pnpm-lock.yaml is byte-identical to main. Regenerated in a fresh worktree with a frozen-lockfile clean install: 662 deps, and the output matches the lint lane's renewed file byte for byte, which the polluted layout could not reproduce. * fix(managed-agent): pace forced takeover loads per Turn and make the cancelled settle idempotent (R9-4) Two round-10 verdict items: 1. The coded-409 escalation (R9-P1-2) ran on every ~500ms lease-renewal tick, so a Turn cancelled while its wait still owns a live durable record paid ~4 requests/s of daemon work forever (21 forced loads and 20 cancels in the verdict's captured window). cancelAdmittedTurn now paces the forced cancellation takeover load per Turn (5s minimum interval): the cheap plain-cancel retry keeps carrying the wait, and a wait that ends inside the interval still settles on the next paced attempt — far below the approval timeout's 45s budget. 2. A settle that already landed was not idempotent: the coordinator's next forced load inside the stream's landing window met `event id turn:<id> is already committed` (3/3). The cancelled settle now re-reads the journal's own turn.settled for this prompt before writing, so the redriven load is a re-answer, never a second turn_result racing the event-id CAS. Witnesses: three paced coded-refusals (t0 / t+500ms / t+5s) yield three plain cancels but two forced loads (HarnessCoordinatorTest 60); the round-9-2 witness pair now races a second identical load inside the landing window and asserts one cancelled record and no takeover_unavailable line (hosted suites 275). * test(managed-agent): commit authority-valid deltas in the restore byte-budget test How: holdsRestorePagesInsideThePerPageByteBudget now builds each dense transaction from 210 well-formed message.delta lines (text at the reader's 4096-byte cap, ~1 MB per transaction) plus the commit marker, through a shared TurnEventLines composer, and asserts that revisions 1..9 fit the 8 MiB page budget while a tenth does not. The reader replay IT now also replays that delta shape. The class turns off MockMvc print-on-failure. Why: QwenLM#13348 added the test with placeholder lines ({"subtype":"event_v1"} plus a padded commit_v1 line), and QwenLM#13355 (merged later, never run against it) made the commit gate refuse any non-event line inside the event range, so every commit after genesis answered 409 and main's SDK Java lanes went red. The failure was masked as timeouts: Spring Boot printed the failed test's ~1.3 MB request body as one log line, which stalled the Actions log pipeline for ~45 min, so the Hosted Verify and O4 steps blew their ceilings and the MariaDB lane hit its job timeout. Test: ManagedSessionStoreIntegrationTest and ManagedExtensionRecordStoreTest green; full managed-agent-server surefire (1077) and checkstyle green; HostedCommittedEventLineReplayIT green through the real TS reader and red when the delta text exceeds 4096 bytes; dropping the page byte budget in ManagedSessionStore fails the test (10 rows instead of 9) with no log line over 400 chars. * fix(managed-agent): propagate the settle's own wait-check fault instead of degrading it to a successful cancel (R9-5) The cancellation settle's authorization re-read was wrapped in `.catch(() => undefined)`, so a transient store fault surfacing between the caller's gate and the settle's own wait-check degraded into 'nothing to close' — the cancelled terminal landed over a wait the fault hid, and the Session's next prompt then found the same obsolete checkpoint that family wedges on (the round-11 fault injection at this exact boundary: authorization flips runnable → blocked/missing_state → runnable once, the settle still mints turn_result, and the run dies on `await_action is not a model-start phase`). A read failure is a retriable store fault, not 'nothing to close': it now propagates into the caller's try-catch, which keeps the baseline retriable refusal — the fault's own retry ladder owns the retry, and the replayed shape then settles identically. Witness mirrors the probe: the wait-gate reads fine, the settle's own check faults once — load refuses hosted_turn_recovery_required with no Cancelled record and an honest 409 on the plain cancel, then the same shape settles after the fault clears (settle log, the model_output_committed boundary, plain cancel 204). Suites 276/276 (hosted session 232, recovery 44), typecheck/prettier/eslint clean. * fix(managed-agent): refuse the cancelled settle on fault-shaped authorization verdicts too (R9-5') R9-5 propagated THROWN authorization faults into the caller's retriable refusal, but the authority never throws the same class of failure: the real path takes a retry-exhausted ManagedSessionStoreTransportError and converts it in place into `{status: 'blocked', reason: 'missing_state'}`. From there the cancelled settle still marched on — it skipped the wait advance and wrote the cancelled terminal anyway, spending the Session's next prompt on the obsolete `await_action` checkpoint the verdict hid (the round-11 verdict at `964b87a50a`, reproduced with the production error type: 200 on the takeover, the terminal lands, and the next turn dies `await_action is not a model-start phase`). Fault-shaped authorizations are never "nothing to close": `missing_state` (the TransportError conversion), `opaque_state` and `invalid_state` now all answer the same retriable refusal — the settle refuses rather than minting what it cannot prove. `missing_checkpoint` stays payable on purpose: it is the Arm B park's honest form (a no-tool Session with history provably has no checkpoint) and its settle is the already-verified behaviour. The witness covers both faces of the store fault (`it.each(['exception', 'missing_state'])`): same park, same stage, either a thrown fault or a folded `blocked/missing_state` — the load refuses with zero settle and an honest plain cancel 409, and the replay settles the moment the fault clears. Suites 277/277 (hosted session 233, recovery 44), typecheck/prettier/eslint clean; design docs note the boundary in both languages. * docs(design): split the R9-5' verdict paragraph into its own bullet The R9-5' text rode mid-bullet after R9-5's run-on, which Prettier then flattened to column zero — breaking the list structure the lint lane rejects outright. Both languages carry the split now, and the lint-facing check passes again.




What this PR does
This PR closes the three Critical follow-ups from the H0c post-merge review (#12855), collected in #13300 (Critical group only — the Suggestion groups stay in #13300 for their own PRs).
First, the Broker-record execution mapping now reads the record's own dispatch-claim generation: a record settled or cancelled before any dispatch claim maps to "never started, proven" instead of "settled", removing an impossible intent-to-settled step the record contract refuses and giving a Monitor cancelled before dispatch a committable settling outcome. The corrected mapping is pinned by a shared fixture row replayed in both languages, plus an explicit pin of the one deliberate divergence between the wire reading and the record reading.
Second, Session commits now validate every event line at commit time instead of only the one body-bearing line: the same closed envelope, event-kind and subtype vocabularies, domain checks, reserved id namespace, and per-kind byte caps that the reopen reader enforces are now enforced by the commit path, with the vocabularies and caps shared through the record contracts rather than duplicated. A malformed line now fails the commit with a clear refusal, where it previously stored fine and bricked the Session at the next open. On top of the reviewed finding this rolls up the gate hardening the review round surfaced afterwards: every domain.committed payload is checked whether or not the domain field parses (a missing or non-textual domain no longer skips all payload checks and bricks the reader — mutation-witness proven red→green), a Stage H event id must sit in its own domain's reserved
<domain>:<n>namespace, and an event subject must be a JSON object. The shared event vocabulary carries its seventeenth kind, message.retracted, in all three mirrors (TypeScript list, shared fixture, Java mirror).Third, the task-change announcement no longer writes to the public Session event stream at all. The reviewed defect was that an in-stream task.updated between two streamed text deltas splits one assistant message into two durable Parts, because the projection merges deltas by the event at sequence−1. The investigation in this PR's review found that the post-#13265 form on main — a bounded per-task SQL journal serving the task events routes, with a stream row written alongside — still carries the split in the stream row itself. This PR therefore drops the stream announce entirely: the bounded per-task journal (already on main via #13265) is now the single feed, record revisions journal their task view changes at the H0c announcement point, and text continues one Part because nothing announcement-shaped lands between deltas at all. A Session that is being deleted journals no further: the announcement point re-reads the latest committed status FOR UPDATE inside the record commit's own transaction (taking the Session row's lock there for the first time — it may wait across connections, the same shape main's announce() already has), so a mid-commit deletion leaves the journal empty without reading the commit's own stale snapshot.
Why it's needed
H0c's merged state passed its own gates, and the post-merge review found three Critical latent defects: a mapping that can fabricate a settled outcome for work that never ran; a commit path that silently stores lines the reader refuses (one bad line bricks the Session at reopen with no commit-time error); and a projection-sequence mechanism that durably corrupts assistant messages whenever a task changes mid-turn. Stage H slices H4–H6 need to open new domains for submission; these are the shared mapping and validation paths those domains inherit, so the tracker requires the Critical group to land first.
Reviewer Test Plan
How to verify
The defects and fixes are record-level; focus on behaviors:
settled-cancelled-unclaimeddiscriminates and is replayed by both the TypeScript gate test and the Java projection contract test; removing the generation arm turns exactly that row red in both languages.<domain>:<n>id on an ordinary line, cross-domain reserved id on a Stage H line, unknown domain.committed body, missing/non-textual payload domain, non-object subject, over-cap marker, two Stage-H lines in one transaction — and the whole commit refuses withmanaged_session_extension_record_rejectedand rolls back, the Session staying openable. The negative suite covers these (and a body-less faithful control that commits), with per-arm mutation witnesses. In particular, with the gate reverted to skip payloads missing a textual domain, the two new witnesses go red because the commit answers 200 — this one is reproduceable end-to-end by the suite's own refuseOrdinary cases.journalsNothingAfterTheSessionIsDeleted, plus the mid-commitjournalsNothingWhenTheDeletionCommitsMidCommit): the guard re-reads the latest committed status FOR UPDATE in its own transaction, so it cannot miss an interleaved deletion through its own snapshot; the discarded guard shape was a separate sessions-connection FOR UPDATE inside the commit's critical section, which wedged a mid-commit test at a 50 s lock-wait.#13265and advanced beyond it: full module suite 965 tests green, theManagedAgentMySqlITclass 20/20 on a fresh MySQL 8.0.46 schema (45 migrations applied, including the mid-commit deletion IT), TypeScriptsrc/managed-runtime2135/2135, Flyway uniqueness gate 45/45, checkstyle and SpotBugs green, Prettier green.Evidence (Before & After)
N/A (no user-visible surface). Local verification on the new head
326ef6cd7a:npx vitest run src/managed-runtime→ 41 files / 2135 tests passed.mvn testinmanaged-agent-server→ Tests run: 965, Failures: 0, Errors: 0, Skipped: 1; runtime-broker BUILD SUCCESS;ManagedAgentMySqlIT(fresh MySQL 8.0.46) → 20/0/0 including the mid-commit deletion IT; checkstyle and SpotBugs → exit 0.node scripts/check-flyway-migrations.js packages/sdk-java/managed-agent-server→ 45 migrations, all versions unique.Tested on
Environment (optional)
JDK 21 + Maven 3.9.11 with an isolated local Maven repository; MariaDB 10.11 with a throwaway datadir for the MySQL-contract IT (MySQL 8.4 covered by CI lanes).
Risk & Scope
Linked Issues
Refs #13300 (Critical group only; the issue stays open for the Suggestion groups) · Refs #12827 · Post-merge review findings of #12855
中文说明
本 PR 做了什么
本 PR 关闭 H0c 合入后审阅(#12855)收集在 #13300 里的三条 Critical 修复(仅 Critical 组——Suggestion 组留给 #13300 各自的 PR)。
第一,Broker 记录的执行映射改读记录自身的派发认领代际:一条在任何派发认领之前就被 settle/cancel 的记录现在映射为「可证明未执行」,消除了记录契约拒绝的 intent 直达 settled 的非法迁移,也让派发前被取消的 Monitor 获得可提交的结算结果。修正后的映射由共享 fixture 行在两种语言中回放钉住,并显式钉住线缆读法与记录读法之间唯一的有意分歧。
第二,会话提交现在在提交时对每一行事件做校验,而不仅是唯一带正文的那一行:重开读取器所执行的闭合信封、事件 kind 与 subtype 词汇、domain 检查、保留 id 命名空间、按 kind 的字节上限,现在由提交路径同样执行,词汇与上限通过记录契约共享而不复制。畸形行以明确拒绝使提交失败,此前会静默写入并在下次打开时让会话变砖。在审阅发现之上,本 PR 还收拢了随后审计轮冒出来的门禁加固:每个 domain.committed 的 payload 不论 domain 字段能否解析都要检查(缺失或非文本 domain 不再跳过全部 payload 校验——变异见证红→绿)、Stage H 事件 id 必须落在其自身 domain 的
<domain>:<n>保留命名空间、事件 subject 必须是 JSON 对象;事件词汇的第 17 个 kindmessage.retracted以三种镜像一致携带(TS 列表、共享 fixture、Java 镜像)。第三,任务变更宣告彻底不再写公共会话事件流。审阅所指的缺陷是:序列中的 task.updated 落在两条文本增量之间时,会因投影按 sequence−1 事件合并而把一条助手消息劈成两条持久 Part。本 PR 在复盘中证实 #13265 之后的 main 形态(SQL bounded task journal 服务任务事件路由、同时仍并写一条 stream 行)本身仍携带同样的劈分。因此彻底摘掉 stream 公告:main 已有的 bounded 任务 journal 成为唯一 feed,记录 revision 在 H0c 公告点写任务视图变更;删除中的 Session 不再 journal——公告点以无锁方式在独立连接上读取最新已提交状态(绝不自锁 commit 已持有的 Session 行),mid-commit 删除不会拖死记录事务。
为什么需要
H0c 的合入状态通过了当时的门禁,但合入后审阅发现三条潜在 Critical 缺陷;阶段 H 的 H4–H6 即将开放新 domain 提交,需要的正是这些共享映射与校验路径,tracker 要求 Critical 组先落。
评审者验证计划
如何验证
settled-cancelled-unclaimed双语回放,删代际分支两边同时红。journalsNothingAfterTheSessionIsDeleted与中段并发的journalsNothingWhenTheDeletionCommitsMidCommit)——守卫在自己事务内以 FOR UPDATE 重读已提交状态,不会随自身快照漏掉中段插入的删除;被弃用的形态是经另一条 sessions 连接对 Session 行加锁,它把同一 mid-commit 测试撞上 50 秒锁等待失败,那是另一条锁路径而非本变体。ManagedAgentMySqlIT于全新 MySQL 8.0.46 schema 20/20(45 迁移全新应用,含中段删除 IT)、TS 2135/2135、Flyway 45 unique、checkstyle 与 SpotBugs 绿、Prettier 绿。证据(前后对照)
N/A(无用户可见界面)。见正文「How to verify」第 4 点数字,均在本 new head
326ef6cd7a实跑。已测试
macOS ✅;Windows N/A;Linux 走 CI。
环境(可选)
JDK 21 + Maven 3.9.11,隔离本地 Maven 仓库;MySQL 契约 IT 用 MariaDB 10.11 临时 datadir(MySQL 8.4 由 CI 覆盖)。
风险与范围
关联 Issue
Refs #13300(仅 Critical 组,issue 保留给 Suggestion 组)· Refs #12827 · #12855 合入后审阅发现。