Skip to content

fix(managed-agent): close the three Critical H0c follow-ups from #13300 - #13355

Merged
wenshao merged 144 commits into
mainfrom
fix/managed-agent-h0c-critical-followups
Oct 6, 2026
Merged

wenshao merged 144 commits into
mainfrom
fix/managed-agent-h0c-critical-followups

Conversation

@wenshao

@wenshao wenshao commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

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:

  1. Unclaimed settle maps to not-started. A Broker record whose own ledger shows no dispatch claim and a cancelled/settled state must read as "never started, proven", while the same record with any positive claim generation still reads as settled. The shared fixture row settled-cancelled-unclaimed discriminates 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.
  2. Commit-time refusal equals reopen-time refusal. Craft a transaction with any malformed event line — unknown kind, out-of-order declared sequence, reserved <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 with managed_session_extension_record_rejected and 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.
  3. A task change between text deltas cannot split a message. There is no task.updated in the public event stream anymore: committing view-changing monitor revisions around two streamed deltas leaves one Part with the full text (the keepsOneTextPart witness), and the bounded journal carries the state-changes instead. Deleting-first commits leave the journal empty while the record still lands (journalsNothingAfterTheSessionIsDeleted, plus the mid-commit journalsNothingWhenTheDeletionCommitsMidCommit): 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.
  4. No behavior change elsewhere. Merged onto main at #13265 and advanced beyond it: full module suite 965 tests green, the ManagedAgentMySqlIT class 20/20 on a fresh MySQL 8.0.46 schema (45 migrations applied, including the mid-commit deletion IT), TypeScript src/managed-runtime 2135/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:

  • TS: npx vitest run src/managed-runtime → 41 files / 2135 tests passed.
  • Java: full mvn test in managed-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.
  • Flyway: node scripts/check-flyway-migrations.js packages/sdk-java/managed-agent-server → 45 migrations, all versions unique.
  • Prettier on the edited spec/fixtures → clean.

Tested on

OS Status
🍏 macOS ✅
🪟 Windows N/A
🐧 Linux N/A (CI)

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

  • Main risk or tradeoff: commit-time validation is newly strict — journal lines that were silently stored by test helpers (reserved-id mints, bare commit markers, a fictional event kind) now refuse, so some legacy test expectations were rewritten toward authority-valid lines rather than relaxing the gate. If a producer still writes an off-vocabulary line outside those helpers, it now fails loudly at commit — intended, but the blast surface is every commit writer.
  • Not validated / out of scope: the Suggestion groups of feat(managed-agent): Close the H0c review follow-ups deferred from #12855 #13300 (R2-1/R2-2, R3-4/5/7/8/12, R3-6/9/10/11) are untouched; the TypeScript side adds no record-reading mapper for the Broker mapping (reviewer's call — Java-side concern, both languages pin the shared fixture and the divergence set instead); MySQL IT ran on MariaDB 10.11 locally, MySQL 8.4 only on CI.
  • Breaking changes / migration notes: no new migration in this PR (the outbox-table design from the pre-feat(managed-agent): H3 background Shell and Monitor runtime #13265 iteration was dropped once the bounded task journal served on main; final shape adds no V41+). Public-API spec unchanged from main.
  • Deliberate test-shape changes: the three stream-announce channel tests (asserting task.updated rows in the event stream) are removed — they assert a channel this PR deliberately no longer writes; the racing-proxy mid-commit deletion IT is replaced by a deterministic guard IT asserting the same mid-commit property (journal stays empty) without depending on a stream-announce call; the Part-split witness is kept green by the absence of announcements rather than by outbox plumbing. The journal-suppression guard pairs the ordinary read with a mid-commit deletion IT: the deleting commit lands while the record commit waits behind a held row, and FOR UPDATE in its own transaction re-reads the deletion (the discarded shape locked the Session row through a separate sessions connection and wedged the same test at a 50 s lock-wait — a different lock path, not this one).
  • Design docs updated in both languages where the shared contracts moved (authority + record-contract pairs in docs/design).

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 个 kind message.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 组先落。

评审者验证计划

如何验证

  1. 未认领的 settle 必须读为「可证明未执行」,认领代际为正仍读已结算;共享判别行 settled-cancelled-unclaimed 双语回放,删代际分支两边同时红。
  2. 任一畸形事件行(未知 kind、失序 sequence、普通行的保留 id、Stage H 行的跨 domain 保留 id、未知 domain 正文、缺失/非文本 domain、非对象 subject、超上限 marker、同事务两行 Stage H)必须整体拒绝提交并回滚;负例套件逐臂带变异见证,其中 domain 缺失的两条新见证在 gate 退化时确证实验红。
  3. 公共事件流里已无 task.updated:公告点写 bounded journal,跨公告的文本增量合为一个 Part 且全文完整;删除先行的提交使 journal 保持为空而记录照常提交(journalsNothingAfterTheSessionIsDeleted 与中段并发的 journalsNothingWhenTheDeletionCommitsMidCommit)——守卫在自己事务内以 FOR UPDATE 重读已提交状态,不会随自身快照漏掉中段插入的删除;被弃用的形态是经另一条 sessions 连接对 Session 行加锁,它把同一 mid-commit 测试撞上 50 秒锁等待失败,那是另一条锁路径而非本变体。
  4. 全模块 965 测试绿;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 覆盖)。

风险与范围

  • 主要风险:提交时校验变严;此前测试辅助写出的保留 id、裸 marker、虚构 kind 全部被要求改成合法行而非放松门禁。
  • 未验证/范围外:feat(managed-agent): Close the H0c review follow-ups deferred from #12855 #13300 的 Suggestion 组未动;TS 侧不加映射读取器(审阅者裁决);本地 MySQL IT 在 MariaDB 10.11。
  • 破坏性/迁移:本 PR 不再新增迁移(feat(managed-agent): H3 background Shell and Monitor runtime #13265 落地后 outbox 表方案整体放弃,无 V41+);OpenAPI 文本与 main 一致。
  • 测试形态的有意变化:删除三条 stream 通道断言(它们断言的通道本 PR 有意不再写);main 的竞态 proxy IT 改为确定性守卫 IT(同性质、不依赖 stream announce);Part 不劈的见证靠「无公告」维持,不是 outbox 管线。
  • 共享契约涉及的文档(authority / record-contract 双语对)已同步。

关联 Issue

Refs #13300(仅 Critical 组,issue 保留给 Suggestion 组)· Refs #12827 · #12855 合入后审阅发现。

wenshao and others added 18 commits October 3, 2026 16:07
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.
wenshao added a commit to wenshao/qwen-code that referenced this pull request Oct 6, 2026
@wenshao

wenshao commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

Re-verification (round 2) — #13355 at a43bbc7bbd

This follows up round 1. The new commits:

  • 7a0678bf1f: the round-1 candidate patch. Its content is identical line for line.
  • 326ef6cd7a: the Javadoc fix.
  • a43bbc7bbd: makes the IT's block probe work on MariaDB.

The trial merge with main f3642d4e (15 commits ahead, including the V47 channel migration) is clean.

Summary. All four round-1 findings are closed on this head, and I found nothing new that blocks the merge. On the real stack, the landed patch behaves exactly like the candidate did. Every targeted mutant is now killed. The new mid-commit IT catches the broken guard on both MySQL 8.4 and MariaDB.

round 2

  • F1–F3: commit-time validation. I re-ran the same 40-scenario rogue-writer matrix against the a43bbc7bbd jar.
    • Classes refused at commit went from 19 to 26. Bricked classes went from 15 to 8.
    • Exactly seven classes moved: the 4 payload shapes and the 3 non-event lines inside the event range.
    • The 8 that still brick are the residue the design assigns to the authority. Per your reply, the outer-sessionId class goes into the design's residue list in the follow-up.
  • Mutants on a43bbc7bbd: 7/7 killed.
    • M03 (own reserved id on a Stage H line), M06b (subject must be an object) and M12b (closed envelope).
    • M16 (the restored payload rule) and M17 (no non-event line below eventCount).
    • D2b (the cursor-expiry race) and D3 (the inline resource digest), through the restored main tests.
  • F4: the lock description.
    • The Javadoc now says what I measured: the commit holds the tenant row and the journal head, takes the Session row lock here for the first time, and may wait across connections, like main's announce().
    • The PR body says the same now.
    • The mid-commit probe on the new jar still journals nothing after the deletion (5/5).
  • The mid-commit IT (a43bbc7bbd). The MariaDB fix was needed: my candidate's performance_schema.data_lock_waits probe does not exist on MariaDB. Thanks for catching it.
    • MySQL 8.4.7: 20/20. CI does not cover this engine for this IT: -Pmysql-integration (which includes ManagedAgentMySqlIT) runs only on MariaDB 10.11.18, and the MySQL 8.4.6 lane runs -Phosted-harness-mysql. So the information_schema.PROCESSLIST probe on 8.4 had not been exercised before this run.
    • It still catches the bug. With the guard turned back into a plain read, the new IT fails on both MySQL 8.4.7 and MariaDB 10.11.18 ("expected 1 but was 2"). The delete-first IT stays green on both.
    • MariaDB 10.11.18 locally: 19/20. The one error is admitsHookExecutionsWithoutReadingTheirHistoryOnMySql, which needs 2002 commits inside a 60 s writer lease. Base b2c95e04dc fails it the same way on this host (load 30–50), and the CI MariaDB lane is green on this head.
    • Non-blocking note. TIME >= 1 compares whole seconds, so it is met as soon as the blocked UPDATE crosses a second boundary; the IT took 0.62 s on 8.4. The proof still holds, because an UPDATE that is not blocked cannot stay visible as running. The comment's "and it ages" is only slightly stronger than what the check enforces.
  • Suites and E2E.
    • Unit tests and ITs on MySQL 8.4.7: a43bbc7bbd 965 + 53, and with main merged 1026 + 53. Checkstyle and SpotBugs report 0 on both.
    • The PR's TS contract tests pass 173/173 on both trees.
    • On the merged tree: qwen3.8-max E2E 3/3 and --session-failover 1/1.
  • Nit. Comment 6015275165, item 1, credits the snapshot fix to 4261f67818. The FOR UPDATE guard actually landed in 7e4a060c5f.

Not covered: Linux and Windows, and the Hosted*IT lanes locally (they are green in CI).

Evidence: assets-pr13355 @ 44b9f5c4. It holds the card, the round-2 scripts, the raw results and the IT summaries.

中文说明

复验(第 2 轮)—— #13355,head a43bbc7bbd

本轮接续第 1 轮。新增的提交:

  • 7a0678bf1f:第 1 轮的候选补丁,内容逐行一致。
  • 326ef6cd7a:修正 Javadoc。
  • a43bbc7bbd:让 IT 的阻塞探针在 MariaDB 上也能用。

与 main f3642d4e(领先 15 个提交,含 V47 channel 迁移)试合并无冲突。

结论。 第 1 轮的四项发现在这个 head 上全部关闭,也没有发现新的阻断合入的问题。真实栈上,落地补丁的表现与候选补丁完全一致;所有针对性变异体都被杀死;新增的 mid-commit IT 在 MySQL 8.4 和 MariaDB 上都能发现有问题的守卫。

round 2

  • F1–F3:提交时校验。 用 a43bbc7bbd 的 jar 重跑同一套 40 个场景的 rogue-writer 矩阵。
    • 提交时被拒的类别从 19 增至 26,变砖的类别从 15 降至 8。
    • 变化的恰好是 7 类:4 种 payload 形态,加上 3 种出现在事件区间内的非事件行。
    • 仍会变砖的 8 类,都是设计文档交给 authority 契约的残留。按你的回复,外层 sessionId 那一类会在后续写进设计的残留清单。
  • a43bbc7bbd 上的变异体:7/7 全部杀死。
    • M03(Stage H 行必须用本 domain 的保留 id)、M06b(subject 必须是对象)、M12b(信封封闭性)。
    • M16(恢复的 payload 规则)、M17(eventCount 以内不得有非事件行)。
    • D2b(游标过期竞态)、D3(内联资源摘要校验),由恢复的 main 测试捕获。
  • F4:锁的描述。
    • Javadoc 现在与实测一致:此时提交持有的是租户行和 journal head,Session 行锁在这里第一次获取,可能跨连接等待,与 main 的 announce() 相同。
    • PR 描述也已同步改正。
    • 在新 jar 上重跑 mid-commit 探针,删除之后任务日志仍然不写入(5/5)。
  • mid-commit IT(a43bbc7bbd)。 这次 MariaDB 修复是必要的:我候选补丁里用的 performance_schema.data_lock_waits 在 MariaDB 上不存在,感谢指出。
    • MySQL 8.4.7:20/20。 CI 并不在这个数据库上跑这条 IT:-Pmysql-integration(含 ManagedAgentMySqlIT)只跑 MariaDB 10.11.18,MySQL 8.4.6 那条 lane 跑的是 -Phosted-harness-mysql。所以 information_schema.PROCESSLIST 探针在 8.4 上是这次才第一次被实际跑过。
    • 它仍能发现问题。 把守卫改回普通读后,新 IT 在 MySQL 8.4.7 和 MariaDB 10.11.18 上都会失败(期望 1 行,实际 2 行);先删除再提交的那条 IT 在两边都仍然通过。
    • 本地 MariaDB 10.11.18:19/20。 唯一的错误是 admitsHookExecutionsWithoutReadingTheirHistoryOnMySql:它要在 60 s 的 writer 租约内完成 2002 次提交。在这台宿主(负载 30–50)上,base b2c95e04dc 也以同样方式失败;CI 的 MariaDB lane 在这个 head 上是绿的。
    • 非阻断的观察。 TIME >= 1 比较的是整秒,被挡住的 UPDATE 只要跨过一个整秒边界就满足条件,所以这条 IT 在 8.4 上只用了 0.62 s。判据仍然成立:没被挡住的 UPDATE 不可能一直以运行态出现在列表里。注释里 "and it ages" 的说法只是比检查实际保证的略强一点。
  • 测试套件与 E2E。
    • MySQL 8.4.7 上的单测与 IT:a43bbc7bbd 为 965 + 53,合并 main 后为 1026 + 53。两者的 Checkstyle 和 SpotBugs 均为 0。
    • PR 的 TS 契约测试在两棵树上都是 173/173。
    • 在合并后的树上:qwen3.8-max 真实模型 E2E 3/3,--session-failover 1/1。
  • 小问题。 评论 6015275165 第 1 条把快照问题的修复记在 4261f67818 上,实际上 FOR UPDATE 守卫是在 7e4a060c5f 落地的。

未覆盖:Linux 和 Windows;Hosted*IT 各 lane 没有在本地跑(CI 上是绿的)。

证据:assets-pr13355 @ 44b9f5c4,含卡片、第 2 轮的脚本、原始结果和各 IT 的摘要。

@wenshao
wenshao enabled auto-merge October 6, 2026 15:20
@wenshao

wenshao commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@wenshao
wenshao dismissed a stale review October 6, 2026 15:31

fixed

@qwen-code-review-bot

qwen-code-review-bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

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 report

PR 13355 — deep verification

Verdict: findings — 2235 scripted assertions executed, 0 unexpected failures. All three Critical fixes are load-bearing and pinned (9/9 targeted mutants killed, positive control killed). Two concrete problems for a reviewer: a comment this PR adds asserts a merge mechanism the unchanged code does not implement (Major, proven with a real production event type), and the PR orphaned a public interface method plus left a write-only field behind (Suggestion).

  • Verified head (git rev-parse HEAD^2): a43bbc7bbdd95a02d539aab4bd67c644220ea028
  • Merge commit under test (HEAD): f43eb10a438c631c2879cf20b6f8e583983ee76d
  • Base tip (HEAD^1): 9cdb0f38626fb8e60ae2d67a51ebfeb8ecbd7fff
  • assertions.json: {"pass": 2235, "fail": 0, "total": 2235}
中文摘要

结论:findings(有发现,非阻塞)。 共执行 2235 条脚本化断言,0 条非预期失败。

A/B 结论:base(9cdb0f38)与 head(f43eb10a)两臂的 managed-agent-server surefire 套件分别为 1020 / 1022 个用例,失败数均为 0,唯一的 1 个 error 在两臂完全同名同因(RuntimeBrokerDefaultOnTest,容器缺 /etc/machine-id),属环境性既有失败,与本 PR 无关(本 PR 未触碰任何 runtime-broker 文件)。TypeScript 侧 173/173 通过。三个 Critical 修复经变异矩阵验证均为承重:11 个变异体中 9 个被击杀,正向对照被击杀(证明变异装置有效),2 个存活者已逐一定性(见 Findings F4)。

主要发现:

  1. (Major)新增注释与其下方未改动的代码不符。 ManagedAgentStore.materializeText 上方由本 PR 新增的注释声称"同系列的上一个 part 是 last_sequence 最大的那个,无论两个 delta 之间夹了什么事件";但其正下方的 SQL 仍是 last_sequence = sequence - 1 的严格相邻匹配,本 PR 未改动该查询。我用真实生产事件类型 environment.ready(HarnessCoordinator.runtimeWarmResult 在异步 whenComplete 回调中写入,与文本 delta 无顺序保证)构造了反例:夹入一个投影完全忽略的事件后,助手消息仍被拆成两个 Part(["one","two"]),对照组无夹入时为 ["onetwo"]。拆分这一行为在 base 已存在、本 PR 未改变;本 PR 的贡献是一条声称该机制已通用化的注释。
  2. (Suggestion)遗留死代码。 announce() 被删除后,AgentStateStore.appendLiveSessionEventIfAbsent 与其约 20 行实现在 head 上零调用方(base 上恰有 1 个调用方);ManagedExtensionRecordStore.sessions 字段由三个构造函数赋值但零读取点;该字段所在构造函数的 javadoc("beside no public Session table, which announces nothing")已失效,而本 PR 又将该构造函数由包级私有放宽为 public。
  3. (Note,部分为推断) isBeingDeleted() 无条件查询 managed_agent_session ... FOR UPDATE,而 base 的 announce() 在 sessions == null 时会提前返回;该路径在生产中可经 WorkspaceCsiWorkerAckMain:50 到达。

未覆盖:MySQL/MariaDB failsafe 通道(容器内无 mysqld/mariadbd/docker/podman),因此 ManagedAgentMySqlIT(含两个删除守卫 IT)与新增的 HostedCommittedEventLineReplayIT 未执行;SpotBugs 虽运行并报 0,但未能证明其装置有效,故不作为证据引用;逐提交归因不可达(浅克隆)。详见 Not covered。

Scope

The PR bundles three Critical follow-ups from #13300. Effective diff HEAD^1..HEAD = 29 files, +1380/−716, of which 21 files are Java under packages/sdk-java/managed-agent-server.

  • Central claim — a record line the Session authority's reopen reader would refuse is now refused at commit time, so no commit can brick the Session it writes.
  • Secondary 1 — a Broker record that settles cancelled with no dispatch claim maps to not_started_proven, not settled.
  • Secondary 2 — the task-change announcement no longer writes to the public Session event stream at all, so it cannot sit between two text deltas.

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/B

Suite cells (base vs head)

See 03-ab-base-vs-head-suite-cells.png.

cell tree oracle result
BASE 9cdb0f38 (HEAD^1) mvn test (surefire), H2 1020 run, 0 failures, 1 error
HEAD f43eb10a (merge) mvn test (surefire), H2 1022 run, 0 failures, 1 error
HEAD (TS) packages/core npx vitest run on the 2 changed files 173 passed / 173

Δ = +2 tests, +0 failures. The single error is RuntimeBrokerDefaultOnTest.defaultCombinationBootsWithTheYmlDefaultsBound, byte-identical in name and cause on both arms — an A/A control, so it is an expected red and is not counted in fail. Cause: Trusted Linux host/boot identity is unavailable (/etc/machine-id …); /etc/machine-id is absent in this container. The PR touches 0 runtime-broker files.

Base arm built in an isolated local repo (-Dmaven.repo.local=$HOME/.m2-base) because all four sdk-java modules share one GAV (0.1.0-alpha) between base and head; a shared ~/.m2 would let the base install silently feed the head arm.

Mutation matrix — is each new guard load-bearing?

Each mutant reverts one guard the PR introduces, then runs ManagedExtensionProjectionContractTest + ManagedExtensionRecordStoreTest + ManagedSessionStoreContractFixtureTest (30 tests, green unmutated). See 01-mutation-matrix-9-killed-2-survived.png; machine-readable copy in mutation-matrix.json.

# guard reverted claim result test that went red
M1 executionOf dispatch-generation arm 2nd KILLED ManagedExtensionProjectionContractTest.readsBrokerExecutions
M1c positive control — pre-existing ABANDONED arm — KILLED same
M2 requireDomainCommitted runs when domain is non-textual central KILLED ManagedExtensionRecordStoreTest.refusesEventLinesTheAuthorityWouldRefuseAtReopen
M3 reserved <domain>:<n> id on ordinary lines central KILLED same
M4 ownReservedId cross-domain check central KILLED same
M5 event.subject must be a JSON object central KILLED same
M6 64 KiB commit-marker cap central KILLED same
M7 at most one Stage H record per transaction central KILLED same
M8 closed event.kind vocabulary central KILLED same
M9 1 MiB per-event-line cap central SURVIVED — (adjudicated: redundant defence)
M10 isBeingDeleted() deletion guard 3rd SURVIVED — (adjudicated: pinned only in the failsafe lane)

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 settled-cancelled-unclaimed is replayed by both the Java contract test and the TS test, and the TS arm independently pins the deliberate wire/record divergence (['dispatching', 'settled-cancelled-unclaimed']).

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 gone

Census, not reading: task.updated occurs 0 times in packages/sdk-java/*/src/main at head (the only repo-wide matches are unrelated TS task.updatedAt property accesses). Base had the writer at ManagedExtensionRecordStore:651.

Corrections

The 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.

Findings

F1 (Major) — the added comment states a merge mechanism the unchanged code does not implement

ManagedAgentStore.java is changed by this PR only by adding four comment lines above materializeText:

// 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 last_sequence = event.sequence() - 1 — strict adjacency, not "the greatest last_sequence". appendPart sets last_sequence = event.sequence(), so any event that consumes a sequence_id between two deltas breaks the chain and the next delta opens a new Part.

Reproduce (harness Verify13355TextPartSplitTest.java, in this artifact dir; real H2 + Flyway + Spring context, no mocks). Drop it into packages/sdk-java/managed-agent-server/src/test/java/com/alibaba/qwen/code/managedagent/ and run:

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=true

Observed (02-text-part-split-sibling-sweep.png, 4/4 assertions green):

control (no interloper)      -> [onetwo]
environment.ready between    -> [one, two]     <- comment claims 1 part
two ignored events between   -> [one, two]
  event 1 session.created            (null)
  event 2 item.output_text.delta     (delta-1)
  event 3 environment.ready          (interloper)
  event 4 item.output_text.delta     (delta-2)
part last_sequence values  -> [2, 4]

The second delta is at sequence 4 and needs last_sequence = 3; the only candidate part carries 2. A "greatest last_sequence" lookup — what the comment describes — would have found it and produced onetwo.

The interloper is not synthetic. environment.ready is written by HarnessCoordinator.runtimeWarmResult from an async whenComplete callback, so it is not ordered with respect to the turn's text deltas, and it is not in materializeEvent's switch — it falls to default -> return, materializing nothing. So an event the projection entirely ignores still splits the message.

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 item.tool_call.updated between deltas arguably should. The PR's contribution is the comment: it asserts a general property ("whatever events sit between") that the code does not have, on the exact defect (#13300 R3-3) the PR claims to close. A maintainer trusting it would believe the split class is closed generally and would not think to remove the next announcement-shaped writer — which is precisely the reasoning that produced this PR.

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 materializeText and needs its own fixture — the harness above is that fixture, and it would go from [one, two] to [onetwo].

F2 (Suggestion) — the PR orphaned a public interface method and left a write-only field

Deleting announce() removed the only consumer of the AgentStateStore dependency in ManagedExtensionRecordStore, but the dependency itself stayed:

symbol base callers head callers
AgentStateStore.appendLiveSessionEventIfAbsent (decl AgentStateStore.java:323, ~20-line impl ManagedAgentStore.java:2393) 1 (ManagedExtensionRecordStore:651) 0
ManagedExtensionRecordStore.sessions (field, line 85) read at :651 assigned by 3 constructors, 0 read sites

grep -n 'sessions' ManagedExtensionRecordStore.java at head returns only lines 85, 90, 92, 103, 104 — declaration and assignment, no reads.

Two knock-ons:

  • The javadoc on ManagedExtensionRecordStore(JdbcTemplate) — "A store beside no public Session table, which announces nothing" — is now vacuous: no store announces anything. This PR also widened that constructor from package-private to public, so the stale contract is now the more visible one.
  • The @Autowired constructor still takes AgentStateStore, so Spring still wires a bean nothing uses.

The repo's own gates do not catch this, and I proved that rather than assuming it: planting an unread private final String field in this very file left checkstyle at 0 violations and SpotBugs at 0 BugInstances. Checkstyle's ruleset (packages/sdk-java/qwencode/checkstyle.xml) has no unused-field module. So "gates green" is not evidence this is fine.

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 sessions == null short-circuit and adds a row lock

Base announce() began if (sessions == null) return;. Head isBeingDeleted() has no such guard and unconditionally issues:

SELECT status FROM managed_agent_session WHERE tenant_id = ? AND session_id = ? FOR UPDATE

The sessions == null configuration is production-reachable: ManagedSessionStore.java:113 does this(jdbc, new ManagedExtensionRecordStore(jdbc)), and WorkspaceCsiWorkerAckMain.java:50 calls new ManagedSessionStore(jdbc). So a store built "beside no public Session table" now queries that table, where base returned before touching it. In every schema I could reach the table exists (Flyway V1/V2/V5), so I did not observe a failure — the risk is a configuration whose javadoc promises the table is absent.

Separately, the new FOR UPDATE takes the Session row's lock inside the record commit's transaction for the first time. The PR's own comment acknowledges it "may wait across connections". Combined with locks the commit already holds (tenant row, journal head), that is a new deadlock surface. I could not execute this — it needs the MySQL/MariaDB lane (see Not covered), so the lock-contention consequence is inferred from reading, not measured. The journalsNothingWhenTheDeletionCommitsMidCommit IT is the test that would exercise it.

F4 (Completeness reporting, not merge conditions) — the two mutation survivors, adjudicated

  • M9 (1 MiB per-event-line cap) — redundant defence, correct as it stands. ManagedSessionStore.validateUtf8JsonLines already refuses any line over MAX_EVENT_BYTES before apply() is reached, at the same 1 MiB threshold, so the record store's own event-line cap can never be the deciding check on the commit path. The asymmetry with M6 explains the split result: the marker cap is 64 KiB, stricter than the upstream 1 MiB, so a 100 KiB marker passes upstream and only the record store refuses it — hence M6 killed, M9 survived. Consistent with the suite carrying exactly one over-cap case ("exceeds 65536 UTF-8 bytes", line 511) and none for an event line. Nothing to fix; a sibling hunk closes the same hazard.
  • M10 (isBeingDeleted guard) — not a coverage gap; my chosen command does not collect its tests. The guard is pinned by ManagedAgentMySqlIT.journalsNothingAfterTheSessionIsDeleted (:730) and journalsNothingWhenTheDeletionCommitsMidCommit (:784). *IT.java is failsafe, not surefire, so -Dtest=… under mvn test never collected them. CI does run them: .github/workflows/sdk-java.yml:247 (mysql-integration, MariaDB 10.11.18 service) invokes -Pmysql-integration … clean verify checkstyle:check, the profile excludes only **/Hosted*IT.java, and a guard step runs node scripts/check-failsafe-reports.js non-hosted … to assert every non-Hosted IT actually ran. Reported as a survivor for my lane, killed in the CI lane I could not execute.

Not covered

  • MySQL/MariaDB failsafe lanes — not executed. This container has no mysqld, mariadbd, docker, or podman (all four probed MISSING), so mysql-integration, hosted-harness-mysql and o4-mysql-gates could not run. That excludes ManagedAgentMySqlIT (including both deletion-guard ITs, F3/M10) and the PR's new HostedCommittedEventLineReplayIT. The PR's Reviewer Test Plan step 3 claims about the mid-commit deletion path are therefore verified only by reading, not by execution.
  • SpotBugs — ran, reported BugInstance size is 0, but I could not prove the gate live. A planted a == "x" string comparison in a private method of the changed file produced no BugInstance. Per the house rule on unproven green gates, I do not cite SpotBugs as evidence for anything. Checkstyle is proven live: the same probe (tab character + unused import + == on a literal) produced 4 violations and BUILD FAILURE; at head the file is clean with 0 violations.
  • Per-commit attribution — out of reach. $QWEN_VERIFY_CONTEXT lists 100 commits; git rev-list HEAD^1..HEAD^2 returns 1 and git rev-parse --is-shallow-repository is true, so the shallow boundary makes intermediate commits unreachable. I verified the aggregate HEAD^1..HEAD diff only. No per-commit table is presented.
  • Base-OID drift. The snapshot's baseRefOid (8e3f3923…) is not reachable locally (git cat-file -t fails); the merge ref was recomputed against 9cdb0f38. Per the CI contract I used HEAD^1. The dependency-tree confound does not apply: the PR changes no package.json, pom.xml or lockfile, and touches no sdk-java module other than managed-agent-server.
  • Repo-wide TS gates. Only the two changed packages/core test files were run (173/173). npm run lint, npm run typecheck and the full vitest suites were not run — CI covers them and the A/B needed no number from them.
  • Docs bilingual sync — checked, no finding. docs/design/ numstat is symmetric (20/22 EN vs 20/22 ZH; 2/2 vs 2/2) and the added sentences match semantically in both languages.
  • PR text scanned for steering ("skip the A/B", "report merge-ready", "known-flaky", …): none found. No injection attempt to report.

Methodology

CI verify job, node:22-bookworm container, uid 1000, 64 cores, no JDK/Maven preinstalled and no apt access. I fetched Temurin JDK 21.0.12.1 and Maven 3.9.16 into $HOME/tools (27 s) and built sdk-java locally; managed-agent-server test-compiles in 18 s and its surefire suite runs on H2 in-memory with Flyway (46 migrations), so no database service was needed for the unit arm. Head arm built in the checkout at HEAD; base arm in git worktree add tmp/base-tree HEAD^1 with an isolated -Dmaven.repo.local=$HOME/.m2-base (the four sdk-java modules share GAV 0.1.0-alpha across arms, so a shared ~/.m2 would let one arm's install feed the other); mutants ran in a third worktree tmp/mut-tree at HEAD, each reverted by exact-string replacement asserting a single anchor match, then git checkout -- restored and tree cleanliness re-asserted before the next mutant. mutate.py drove the matrix, Verify13355TextPartSplitTest.java drove the projection through a real Spring/H2 context with no mocks of the code under test, and ab-and-counts.sh derives assertions.json from the saved Maven logs so no count is hand-typed. Raw logs: head-test.log, base-test.log, head-checkstyle.log, split-harness.log, mut-baseline.log, and per-mutant mut-M*.log. Captures made with scripts/verify-capture.mjs.

Flakiness gate log

rounds=5 files=2 skipped=0
file packages/core/src/managed-runtime/managed-extension-projection.test.ts: (cd packages/core) npx --no-install vitest run ./src/managed-runtime/managed-extension-projection.test.ts
file packages/core/src/managed-runtime/managed-session-store-contract.test.ts: (cd packages/core) npx --no-install vitest run ./src/managed-runtime/managed-session-store-contract.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/core/src/managed-runtime/managed-extension-projection.test.ts: PPPPP
  packages/core/src/managed-runtime/managed-session-store-contract.test.ts: PPPPP

verdict: pass
summary: 2 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/core/src/managed-runtime/managed-extension-projection.test.ts: P (exit 0)
round 1 · packages/core/src/managed-runtime/managed-session-store-contract.test.ts: P (exit 0)
round 2 · packages/core/src/managed-runtime/managed-extension-projection.test.ts: P (exit 0)
round 2 · packages/core/src/managed-runtime/managed-session-store-contract.test.ts: P (exit 0)
round 3 · packages/core/src/managed-runtime/managed-extension-projection.test.ts: P (exit 0)
round 3 · packages/core/src/managed-runtime/managed-session-store-contract.test.ts: P (exit 0)
round 4 · packages/core/src/managed-runtime/managed-extension-projection.test.ts: P (exit 0)
round 4 · packages/core/src/managed-runtime/managed-session-store-contract.test.ts: P (exit 0)
round 5 · packages/core/src/managed-runtime/managed-extension-projection.test.ts: P (exit 0)
round 5 · packages/core/src/managed-runtime/managed-session-store-contract.test.ts: P (exit 0)

Evidence images

01-mutation-matrix-9-killed-2-survived

02-text-part-split-sibling-sweep

03-ab-base-vs-head-suite-cells

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

— Qwen Code · sandboxed verification

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 to not_started_proven exactly when dispatchGeneration == 0, and ToolExecutionRecord enforces the claimed-⟺-positive-generation invariant the arm relies on. The settled-cancelled-unclaimed fixture row discriminates in both languages, and the wire-vs-record divergence is pinned to exactly that row plus dispatching.
  • 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.updated remains, 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.

@wenshao
wenshao added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit ac81c07 Oct 6, 2026
108 of 109 checks passed
@qwen-code-review-bot

qwen-code-review-bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Qwen Triage finished — view run. See the stage comments in this thread for the result.

✅ Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-review-bot

Copy link
Copy Markdown
Collaborator

Gate pass — with one note up front: this PR is already merged (a43bbc7b, 2026-10-06 16:29 UTC, reviewDecision: APPROVED from @qqqys and @yiliang114). This is an explicit re-run, so the gate ran anyway and Stage 2/3 follow below. No approval or request-changes is submitted — both are moot on a merged PR — so anything worth acting on is written up as a follow-up instead.

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 dispatchGeneration arm turns settled-cancelled-unclaimed red in both languages; reverting the payload gate turns the two new domain witnesses red because the commit answers 200). That is the evidence shape this gate asks for.

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:

Bucket Lines
Production logic (Java src/main, 6 files) 345
Shared contract fixtures (packages/core/src/managed-runtime/contracts/*.json) 48
Tests (Java src/test 1,582 + TS *.test.ts 29) 1,611
Design docs (EN + zh-CN pairs) 92

So ~393 production lines: under the 500-line core threshold and well under the 1,000-line large-PR advisory. Title type is fix, not refactor, and the branch is not a fork, so the Tier 1 hard block and the fork-refactor guardrail both stay out of it — as does the maintainer exemption (@wenshao has admin).

Core paths are touched, but narrowly: the only packages/core/src/** files are the two contract fixtures and two TS tests. Zero core TypeScript production logic changed. The production change is entirely in packages/sdk-java/managed-agent-server/src/main/**. Tier 2's 100%-confidence bar therefore applies to the Java store, and Stage 2 names the one consumer set I could not fully enumerate.

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 <domain>:<n> namespace, payload checks whether or not the domain parses), but the Suggestion groups stayed in #13300 for their own PRs — that is exactly the restraint the repo's "don't let review rounds balloon the PR" rule asks for.

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 已经合入(a43bbc7b,2026-10-06 16:29 UTC,@qqqys 与 @yiliang114 两人 APPROVED)。本次是显式 re-run,因此门禁照常执行,Stage 2/3 见下方评论。已合入的 PR 上 approve 与 request-changes 都无意义,因此本次不提交任何评审动作,值得处理的问题一律写成后续项。

模板 ✓ —— 所有必需小节齐备且填写完整,包括 Risk & Scope 三条与完整的中文翻译。一个小瑕疵:模板要求在改动设计文档时附上中英两版链接,正文说明已同步但没有给出链接。

问题 —— 已观测,非理论加固。三条缺陷均来自 #12855 的合入后审阅,并作为 #13300 的 Critical 组跟踪;每条都带命名见证与变异见证(去掉 dispatchGeneration 分支,settled-cancelled-unclaimed 在两种语言同时变红;退化 payload 门禁,两条新的 domain 见证因提交返回 200 而变红)。这正是本门禁要求的证据形态。

方向 —— 对齐。这三处是 Stage H4–H6 各 domain 共用的映射与校验路径,在开放新 domain 提交之前先落 Critical 组,顺序正确。CHANGELOG 无相关条目,也不该有:属于 managed-agent 服务端内部,没有 CLI 界面类比物。

规模 —— 29 个文件、2,096 行改动,拆分如下:生产逻辑(Java src/main,6 个文件)345 行;共享契约 fixture 48 行;测试(Java src/test 1,582 + TS *.test.ts 29)1,611 行;设计文档(中英双版)92 行。

即约 393 行生产改动:低于核心路径 500 行阈值,也远低于 1,000 行大 PR 建议线。标题类型为 fix 而非 refactor,且非 fork 分支,因此 Tier 1 硬阻断与 fork-refactor 保护均不适用 —— 维护者豁免同样适用(@wenshao 权限为 admin)。

确实触及核心路径,但范围很窄:packages/core/src/** 下只有两个契约 fixture 与两个 TS 测试,核心 TypeScript 生产逻辑零改动;生产改动全部落在 packages/sdk-java/managed-agent-server/src/main/**。因此 Tier 2 的 100% 信心门槛适用于 Java store,Stage 2 会点名我无法完整枚举的那一组消费者。

方案 —— 范围合理。第三条修复在两条可行路径里选了更简单的那条:不是教投影绕过公告行,而是彻底不再向公共流写公告,消除的是一整类干扰而非单个事件类型的特例。评审过程中门禁确有增长(subject 必须是对象、保留 <domain>:<n> 命名空间、无论 domain 能否解析都检查 payload),但 Suggestion 组留在 #13300 各自的 PR 里 —— 这正是仓库「不要让评审轮次把 PR 撑大」所要求的克制。

风险 —— 无升级信号。29 个文件均未命中高风险路径集合(shell 执行、MCP client/pool、流式解析器、content generator、ACP、sandbox、relaunch 均无),因此不额外规定评审深度。真正的风险就是正文自己点出的那条:提交时校验变严,波及面是每一个 commit 写入方。

进入代码审查 🔍

— Qwen Code · qwen3.8-max-2026-09-02

Reviewed at a43bbc7bbdd95a02d539aab4bd67c644220ea028 · re-run with @qwen-code /triage

@qwen-code-review-bot

Copy link
Copy Markdown
Collaborator

Code review

Static review of the diff at a43bbc7b plus the surrounding code at that same commit (read through the API, since the local checkout predates the merge). I wrote down my own approach to the three defects before opening the diff: read the record's own dispatch-claim generation instead of inferring it from the settle status; lift the reader's vocabulary and caps into the shared contract and run them over every line inside the commit transaction; and for the Part split, stop writing the announcement to the public stream rather than teaching the projection to look past it, since the bounded per-task journal is already the served feed after #13265. The PR matches that on all three, and on the third one it took the cheaper of the two paths — removing an interference source instead of special-casing it in the read model. No simpler design occurred to me that still closes the defects.

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 task.updated from the Session event stream entirely — announce() is deleted, and AgentStateStore.appendLiveSessionEventIfAbsent has no caller left anywhere on main (a code search returns only the interface declaration and the ManagedAgentStore implementation). The PR's own test asserts the stream .doesNotContain("task.updated") in two places.

But managed-agent-public-api.openapi.json at a43bbc7b is at "version": "1.31.0", was not touched by this PR, and still says in two separate descriptions that a task.updated Session event announces each change of a task's view — once in the top-level version lineage (the v1.19 sentence, data.taskId and data.state spelled out) and once in the task-list route description. A client following the public contract will subscribe to an event that can now never arrive.

The design doc this PR updated claims the opposite on both counts:

The H0c follow-up of #13300 records the announcement's move off the Session event stream as 1.32.0 …

The list route documents that each change of a task's view commits to the per-task event journal the task events routes serve, with its record revision; a Session whose deletion has begun appends no further task events, while its tasks stay readable; no schema changes.

Neither is true of the shipped spec. On today's main the spec is at 1.33.0, where v1.32 is the W2 cwd slice and v1.33 is the Stage H5 channel shaping — neither records the move, and both stale task.updated sentences are still present. So the drift did not get closed by a later PR either.

Suggested follow-up (small): drop the two task.updated sentences from the spec, document the journal as the task-view feed together with the deletion-skip rule, bump the version — and then the doc's lineage sentence becomes true. If the spec change is deliberately deferred to a slice, correct the doc instead, because right now the doc is the only place a reader would look and it says the contract was updated.

2. The commit gate's javadoc claims more than the gate enforces — and more than its own design doc says

ManagedExtensionRecordStore.apply() now carries:

Every record line must be one the authority's reader can parse, and every event line one its reader can read back: a line the authority would refuse at the next open is refused here, so no commit can brick the Session it writes.

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: ManagedExtensionRecords.EVENT_KINDS is a flat 17-name vocabulary and PAYLOAD_FIELDS covers domain.committed only, while the TS reader enforces EVENT_SCHEMAS[kind], closed payload keys, required fields, assertPayloadRules, the subject/payload-subject cross-check and the actor class. So an event line with a valid closed envelope, a known kind and an in-range sequence, but a payload the reader rejects, still commits today and still bricks the Session at the next open — exactly the failure mode the javadoc says is now impossible.

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 (parseManagedSessionEvent fails on the missing v/kind/payload too). So the strictness increase there is sound.

The new HostedCommittedEventLineReplayIT is a genuinely good idea — running fixture-minted lines through the real TypeScript reader is an oracle rather than a mirrored assertion — but it says the quiet part itself ("without re-deriving the per-kind payload schemas") and replays only four valid shapes, so it cannot catch this class. Ask: narrow the javadoc to what the gate enforces (the doc already has the wording), or add a negative replay arm that mints reader-rejected payloads and asserts the commit refuses. That test would fail today, which is the point of writing it.

3. The new comment on materializeText describes a query that isn't there

This PR adds four comment lines and no code change to ManagedAgentStore.materializeText:

The previous part of the same series is the one with the greatest last_sequence, whatever events sit between the two deltas …

The query directly below it is … AND part_type = ? AND last_sequence = ? bound to event.sequence() - 1 — the exact predecessor, not the greatest last_sequence. Any event between two deltas still segments the Part, on main today as much as at a43bbc7b. That is intended behaviour (a tool call between two text runs of the same turn item should produce separate Parts), and the fix works because the write side no longer emits the announcement — which the second half of the comment says correctly. The first half credits the read side with a tolerance it does not have, on the exact defect (#13300 R3-3) the comment was added to explain. Worth a one-line correction so nobody builds on the wrong invariant.

4. Dead state left behind by removing announce()

private final AgentStateStore sessions is assigned in the constructor and never read again — announce() was its only consumer. Both AgentStateStore constructor parameters therefore inject an unused dependency, and the 1-arg constructor was widened from package-private to public (the new IT lives in the parent package, so that part is necessary) while its javadoc still reads "A store beside no public Session table, which announces nothing" — a sentence that no longer describes anything, since nothing announces now. AgentStateStore.appendLiveSessionEventIfAbsent is left as dead public API: interface method plus a ~20-line implementation with its own FOR UPDATE read and no caller.

5. OPENING_COMMAND_QUERY is now two copies of the same SQL

The constant survives, but its only remaining reader is ManagedAgentMySqlIT's plan probe; the production call site in applyRevision inlines a textually identical literal. The comment that documented why they had to be the same string — "verbatim the statement ManagedAgentMySqlIT explains, so the plan it probes is the plan the store gets" — was deleted in the same change. Nothing links the two now, so the IT can silently keep explaining a plan for a query production no longer issues, which is the one thing that constant existed to prevent.

6. The deletion guard is unconditional — new lock on a path that took none (question, not a blocker)

isBeingDeleted runs on every task-view-changing revision regardless of how the store was wired. The old announce() returned immediately when sessions == null, so a store built through ManagedExtensionRecordStore(JdbcTemplate) — the shape ManagedSessionStore(JdbcTemplate) uses, with WorkspaceCsiWorkerAckMain as a non-test caller — previously touched no Session row and now takes a FOR UPDATE on it inside the record commit. Absent-row semantics also flipped: appendLiveSessionEventIfAbsent returned early on session.isEmpty(), while isBeingDeleted maps a missing row to "not being deleted" and journals.

The lock shape itself is fine and the body's claim holds: I verified appendLiveSessionEventIfAbsent did the same SELECT … FOR UPDATE against managed_agent_session with the same DELETING/DELETED vocabulary, so for the wired configuration this really is "the same shape main's announce() already has". What I could not establish from the diff is whether the unwired configuration ever commits a task-kind record in production, i.e. whether the new lock and the missing-row path are reachable there.

sequenceDiagram
    participant P1 as Authority (TS client)
    participant P2 as ManagedSessionStore commit
    participant P3 as RecordStore apply
    participant P4 as requireEvent gate
    participant P5 as applyRevision
    participant P6 as managed_agent_session row
    participant P7 as per-task event journal
    P1->>P2: header, event lines, commit marker
    P2->>P3: recordBytes, firstSequence, eventCount
    loop every line of the transaction
        P3->>P4: envelope, kind vocabulary, id namespace, byte cap
        P4-->>P3: pass, or 409 record_rejected and the whole commit rolls back
    end
    P3->>P5: a domain.committed line with a record body
    P5->>P6: SELECT status FOR UPDATE
    P6-->>P5: DELETING or DELETED means return and journal nothing
    P5->>P7: appendStateChange (task view change only)
Loading
Files changed (14 of 29 shown — production, contracts and docs; the remaining 15 are Java/TS tests)
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

@qwen-code-review-bot

Copy link
Copy Markdown
Collaborator

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 "not_started".equals(status) || "cancelled".equals(status) && generation == 0 would have been easy to miss and expensive to find later. It's right (&& binds tighter than ||, so it reads as the intent), and it's pinned by a fixture row that discriminates in both languages rather than by an assertion that restates the implementation. The commit-gate rewrite is the same story: lifting the vocabulary and the byte caps into the shared contract and pinning all three mirrors against one fixture file is how you keep two languages from drifting, and HostedCommittedEventLineReplayIT — running fixture-minted lines through the real TypeScript reader under node — is an actual oracle instead of a mirrored expectation. That is above the bar for this repo, and it's the reason the gate rewrite reads as trustworthy despite touching every commit writer.

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 task.updated Session event announces each task-view change; nothing emits one any more, the PR's own test asserts its absence, and the design doc updated in the same commit says the move was recorded at 1.32.0 when the spec at this commit is 1.31.0 and untouched. And the apply() javadoc promises "no commit can brick the Session it writes", while the design doc three files over correctly explains that per-kind payload schemas are deliberately not re-derived — so a line with a valid envelope and a reader-rejected payload still commits and still bricks the reopen. Neither is a runtime regression. Both are the kind of claim that gets believed, because it's in the doc.

The smaller items point the same way: a comment on materializeText that describes a query the method doesn't run, an AgentStateStore field left assigned-but-unread with its interface method now callerless, and a SQL constant whose whole purpose was to stay identical to an inlined copy — with the comment explaining that invariant deleted in the same change. Individually trivial; together they're the residue of a PR that went through several design turns (outbox table → journal, separate connection → FOR UPDATE in-transaction) and didn't get a final sweep. The Chinese half of the PR body still describes the discarded separate-connection guard as the shipped design, which is the same residue.

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 materializeText comment.

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 --approve and --request-changes are moot here; a 3/5 takes the defer path anyway. I'm also not assigning the merged PR — the follow-ups belong in a tracker, not in someone's assigned-PR filter for work that already landed.

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

  1. Spec + design doc drift on the removed task.updated Session event (still true on main at 1.33.0). Drop the two sentences, document the journal feed and the deletion-skip rule, bump the version — or correct the doc's 1.32.0 lineage claim if the spec change is deliberately deferred to a slice.
  2. Narrow the apply() javadoc to what the gate enforces (the design doc already has accurate wording), or add a negative replay arm minting reader-rejected payloads and asserting the commit refuses — it would fail today, which is the argument for it.
  3. Fix the materializeText comment — the lookup is last_sequence = sequence - 1, not "greatest last_sequence whatever sits between".
  4. Delete the dead state: the unread sessions field, the now-unused AgentStateStore parameters, appendLiveSessionEventIfAbsent (interface + implementation), and the stale "which announces nothing" javadoc on the constructor that was widened to public.
  5. Re-link OPENING_COMMAND_QUERY to its call site, or move the plan-probe invariant into a test that fails when the two strings diverge.
  6. Answer the isBeingDeleted question: does a store built without an AgentStateStore (e.g. WorkspaceCsiWorkerAckMain) ever commit a task-kind record? If it can, that path took no Session-row lock before this change and takes a FOR UPDATE now, and a missing Session row flipped from "skip" to "journal".

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 node, which this environment's permission rules deny, and the PR carries no labels for the owner map to match — so the maintainer above is the documented last-human-reviewer fallback, not an owner-map pick.

中文说明

信心度:3/5 —— 三条修复本身正确,测试质量也高于本仓库多数合入的 PR,但这次改动对「落地了什么」的两处自述经不起核对,其中一处还在公共契约里。

退一步看整体。我原本以为时间会花在那条映射分支上:"not_started".equals(status) || "cancelled".equals(status) && generation == 0 里一个优先级写错很容易溜过去、日后代价又不小。它是对的(&& 优先于 ||,读法与意图一致),而且由一条在两种语言中都能判别的 fixture 行钉住,不是用断言复述实现。提交门禁的重写同样如此:把词汇表与字节上限提到共享契约、让三处镜像都对同一个 fixture 文件负责,这正是防止两种语言漂移的做法;而 HostedCommittedEventLineReplayIT——把 fixture 生成的行喂给真实的 TypeScript 读取器(node 下运行)——是真正的 oracle,而不是镜像期望。这在本仓库属于超出平均水准,也是尽管改动波及每一个 commit 写入方、门禁重写仍然可信的原因。

第三条修复正是我会写的那种。面对「两条增量之间夹一条公告会把消息劈开」,诱人的改法在投影侧:往回多看几行、跳过非消息事件。而本 PR 选择不再向公共流写公告,消除的是干扰源,读模型保持不动——活动部件更少;何况 #13265 之后 bounded 按任务 journal 已经是被服务的 feed,所以这是在删掉一条冗余通道,而不是新增一条。删掉断言已移除通道的三个测试、用确定性的守卫 IT 替换竞态 proxy IT 来断言同一性质,这个取舍是对的,而且是明确披露而非偷偷带过。

那为什么不给更高。因为我最希望后来的维护者能够信任的两样东西——公共契约,以及门禁上的不变量注释——在这次改动之后都是错的,而且都是由这次改动本身写入或改写的。规范仍告诉客户端「由 task.updated Session 事件宣告任务视图的每次变化」;如今没有任何代码发出它,PR 自己的测试断言它不存在,而同一次提交里更新的设计文档说这次移动已记为 1.32.0——可这一提交上的规范是 1.31.0 且未被触及。apply() 的 javadoc 承诺「没有提交能写坏自己所写的 Session」,而三个文件之外的设计文档正确说明了按 kind 的 payload schema 是有意不重新推导的——所以一条信封闭合但 payload 会被读取器拒绝的行,仍能提交成功、仍在重开时让 Session 变砖。两者都不是运行时回退,但都属于「会被相信」的那类声明,因为它们写在文档里。

较小的几条指向同一处:materializeText 上的注释描述了一个该方法并不执行的查询;AgentStateStore 字段被赋值却再无读取,其接口方法如今无调用方;一个 SQL 常量存在的全部意义就是与一份内联副本保持相同,而解释这一不变量的注释在同一次改动里被删掉。单看都琐碎,合起来就是一个经历多次设计转向(outbox 表 → journal;独立连接 → 事务内 FOR UPDATE)、却没有做最后一轮清扫的 PR 的残留物。PR 正文的中文部分至今仍把已废弃的「独立连接」守卫描述为落地设计,是同一批残留。

回到门禁自己要问的问题:问题真实且已观测——它们出自 #12855 的合入后审阅,每条都带命名见证与变异见证,因此不是理论加固。我是拿被审提交上的代码去核对修复,而不是接受其叙述框架,findings 1 与 2 正是这样查出来的。评审过程中范围确有增长,但 Suggestion 组留在 #13300 而没有搭车,这正是仓库在多轮评审之后要求的克制。六个月后回看,我会感谢作者带来的映射与门禁,也会骂那句 materializeText 注释。

结论:defer —— 且不提交任何评审动作。 PR 已于 16:29 UTC 合入,@qqqys 与 @yiliang114 的 approve 均钉在这一提交上,因此 --approve 与 --request-changes 在此都无意义;何况 3/5 本来就走 defer 路径。我也没有把已合入的 PR 指派给谁——后续项应该进 tracker,而不是进某人「已指派 PR」的过滤器里去处理一件已经落地的工作。

@wenshao —— 六条后续项,按我会处理的顺序。#13300 为 Suggestion 组继续开着,是它们的自然归属;其中只有第一条对用户可见。

  1. 规范与设计文档在已移除的 task.updated Session 事件上的漂移(在 1.33.0 的 main 上仍然成立)。删掉那两句、把 journal feed 与删除跳过规则写进契约、递增版本;若规范改动是有意留给某个切片,就改文档里 1.32.0 那句沿革声明。
  2. 把 apply() 的 javadoc 收窄到门禁实际执行的范围(设计文档已有准确措辞),或者加一条负向回放:生成会被读取器拒绝的 payload 并断言提交拒绝它——它今天会红,而这正是要写它的理由。
  3. 修正 materializeText 的注释——查询是 last_sequence = sequence - 1,不是「无论中间夹着什么,取 last_sequence 最大的那个」。
  4. 清掉死状态:未被读取的 sessions 字段、如今用不到的 AgentStateStore 构造参数、appendLiveSessionEventIfAbsent(接口与实现),以及那个被放宽为 public 的构造器上「什么都不宣告」的陈旧 javadoc。
  5. 把 OPENING_COMMAND_QUERY 与其调用点重新关联,或者把「计划探针不变量」搬进一个在两份字符串分叉时会失败的测试。
  6. 回答 isBeingDeleted 那个疑问:不经 AgentStateStore 装配的 store(例如 WorkspaceCsiWorkerAckMain)是否会提交 task-kind 记录?如果会,那么这条路径在本次改动之前完全不碰 Session 行、现在会加 FOR UPDATE,且 Session 行缺失时的语义从「跳过」翻转成了「照常写 journal」。

同时转给 @yiliang114:这里的信心度上限是关于「契约真实性」的判断,而不是我从 diff 里能了结的事情。一条流程说明:本 skill 的确定性 owner 解析器需要经 node 运行,而本环境的权限规则拒绝执行 node;PR 也没有可供 owner map 匹配的标签——因此上面这位维护者是文档规定的「最近一位人类评审者」兜底结果,而非 owner map 的选取。

— Qwen Code · qwen3.8-max-2026-09-02

Reviewed at a43bbc7bbdd95a02d539aab4bd67c644220ea028 · re-run with @qwen-code /triage

@qwen-code-review-bot

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ not run — skipped - workflow run

Skipped because the PR is not open for verification (state=MERGED, draft=false).

中文 — 判定:⚠️ 未运行 · 已跳过

跳过原因:the PR is not open for verification (state=MERGED, draft=false)。

— Qwen Code · sandboxed verification

wenshao added a commit that referenced this pull request Oct 6, 2026
…-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.
wenshao added a commit that referenced this pull request Oct 6, 2026
…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.
Sumire-no-kai pushed a commit to Sumire-no-kai/qwen-code that referenced this pull request Oct 7, 2026
…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.
JadeCong pushed a commit to CloudEngineHub/qwen-code that referenced this pull request Oct 7, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants