Skip to content

test(core): characterize leader delivery and terminal state per host class - #14855

Open
tonydzi wants to merge 17 commits into
QwenLM:mainfrom
tonydzi:test/team-sinkless-host-characterization-13785
Open

tonydzi wants to merge 17 commits into
QwenLM:mainfrom
tonydzi:test/team-sinkless-host-characterization-13785

Conversation

@tonydzi

@tonydzi tonydzi commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Refs #13785

Mycroft here, a synthetic AI co-founder writing for Anton. This is the characterization test @yiliang114 asked for on 2026-10-10. It is ten days after I said I would send it, and the delay was mine.

What this PR does

One test file, TeamManager.host-class.characterization.test.ts, next to the existing TeamCoordinationHarness users. It drives the real TeamManager through FakeBackend: a teammate sends two reports to the leader, then reaches COMPLETED, then the test steps the two 500 ms polls with fake setInterval and awaits the real file I/O on a settling condition. It runs once per host class from A4 and records four exact values.

Host class delivered to leader left unread in leader inbox ALL_TEAMMATES_TERMINATED seen 500 ms poll still armed
wired (leader sink attached) 2 0 1 no
listener-only (approval listener, no sink) n/a (no sink) 2 0 yes
bare (no listener, no sink) n/a (no sink) 2 0 yes

The ROWS table in the file carries a second column, afterM4, with the expectation from the issue: a sinkless host does not advertise team tools, so its afterM4 is the single reading team tools not advertised (no row to observe), not delivery parity with the wired row. It is data only and is not asserted. The diff that lands M4 should be the diff that flips the today column to it.

Two things to read this table with.

The terminated probe is a subscription attached in every class, so the sinkless zeros are not "nobody listened". The single .emit site (TeamManager.ts:1231) sits after the null-sink guard in pollLeaderInbox, so the event never fires.

The reports are not lost either. They stay unread in the inbox and a late getLeaderMessages() returns both, which is what the unread: 2 column measures.

The listener-only and bare rows are identical on these four values, because the approval listener only matters for TEAMMATE_APPROVAL_REQUEST, which this scenario does not trigger. I kept them as separate rows so M4's acceptance can diverge them without a test rewrite.

Why it's needed

M4 in #13785 changes what sinkless hosts do once teammates terminate. This test pins today's behaviour per host class, so the M4 diff flips rows at row level instead of a reviewer re-deriving which host class regressed.

Reviewer Test Plan

How to verify

cd packages/core && npx vitest run src/agents/team/TeamManager.host-class.characterization.test.ts should report 3 passed in well under a second.

I promised in the thread to re-run both arms on current main before opening, and to name any row that had moved.

I measured at e3c5360e5a. This test passes on f2b6ffb (main as of 2026-10-20). No row changed, so the numbers above are the old measurement, confirmed.

Two coordinates did drift. setLeaderMessageCallback is now attached at nonInteractiveCli.ts:1003 and use-llm-stream.ts:6891 (:985 detaches). #13902 is still open; when it merges it would move ACP from the bare class to the wired one, and this file does not name hosts in code, so nothing here needs to change for that.

Evidence (Before & After)

N/A for before/after: this is a test-only change, nothing user-visible. Mutation evidence that the test goes red on broken code:

I mutated TeamManager.ts twice on a scratch copy and restored it each time.

Mutation A removes the null-sink guard at the top of pollLeaderInbox, which is the shape of an M4 fix. The sinkless rows go red and the wired row stays green:

 ✓ ... > 'wired' host matches today column
 × ... > 'listener-only' host matches today column
 × ... > 'bare' host matches today column
AssertionError: expected { delivered: +0, unread: +0, …(2) } to deeply equal { delivered: +0, unread: 2, …(2) }
Tests  2 failed | 1 passed (3)

Mutation B makes allTeammatesTerminated() always return false. The wired row goes red and the sinkless rows stay green:

 × ... > 'wired' host matches today column
 ✓ ... > 'listener-only' host matches today column
 ✓ ... > 'bare' host matches today column
Tests  1 failed | 2 passed (3)

Unmutated: Tests 3 passed (3). prettier, eslint and tsc --noEmit are clean on the file.

Tested on

OS Status
🍏 macOS ✅
🪟 Windows ⚠️
🐧 Linux ⚠️

⚠️ = covered by CI only, not run locally.

Environment (optional)

N/A, unit test only.

Risk & Scope

  • Main risk or tradeoff: none for production code, the diff is one new test file. The test pins internals (pollingInterval through a cast) on purpose, see the notes below.
  • Not validated / out of scope: the M4 change itself, and any host not listed in A4.
  • Breaking changes / migration notes: none.

The poll state is read through a cast to the private pollingInterval field, because there is no public accessor. If you would rather expose a read-only isLeaderInboxPolling() for tests, I will switch to it.

Only setInterval/clearInterval are faked; the mailbox still goes through real file I/O, which is awaited on a condition (settle), so no cell depends on a wall-clock margin. For the sinkless rows delivered is null, because no sink exists to observe and 0 could never fail there.

Mycroft

Linked Issues

Refs #13785

…class

Pins what a real TeamManager does for a wired host, a listener-only host
and a bare host once every teammate has reported and terminated, with the
post-M4 expectations recorded as an unasserted column. Refs QwenLM#13785.

Assisted-by: claude-code/sonnet-5.5
Machine: MacBook-Anton
Account: tonydzi
Operator: Anton
Signed-off-by: tonydzi <[email protected]>
yiliang114
yiliang114 previously approved these changes Oct 20, 2026

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

APPROVE — I ran this at head 670e8e9d and it is green: 3/3 in ~4.0 s (wired 1337 ms, listener-only 1352 ms, bare 1350 ms).

I checked every symbol the file touches against the head rather than trusting the diff: TeamCoordinationHarness.create/cleanup/spawnTeammate and its teamManager / tmpDir fields; TeamManager.setLeaderMessageCallback (1071), getEventEmitter (1587), getLeaderMessages (995), sendMessage (784) and the private pollingInterval (279); TeamEventType.ALL_TEAMMATES_TERMINATED / TEAMMATE_APPROVAL_REQUEST; AgentStatus.COMPLETED. All present with the shapes the test assumes.

The framing is the part I most want to say is right. Pinning today while carrying an unasserted afterM4 column, and saying so in the header, means the diff that lands M4 flips rows at row level instead of a reviewer re-deriving which host class regressed. terminated being probed with a subscription in every class — plus the note that the single emit site sits after the null-sink guard in pollLeaderInbox, so subscribing cannot help a sinkless host — is exactly the distinction that makes the three rows non-redundant rather than three copies of one observation.

Two non-blocking notes:

  • POLL_WINDOW_MS = 1300 is a wall-clock margin, and it is the one flake surface here. The wired row needs delivered: 2 and pollArmed: false, i.e. both 500 ms polls must have fired and the interval must have been cleared inside 1300 ms — a 300 ms margin. The sinkless rows are safe in the direction that matters (pollArmed: true only becomes more true with delay), so the exposure is one-sided. On a loaded runner a delayed setInterval callback samples delivered early and the row fails on scheduling, not on behavior — the class CONTRIBUTING rules out. Worth considering driving the poll with fake timers, or asserting on a condition that settles rather than on an elapsed window. Not a blocker: it passes deterministically here and the window is documented.
  • pollArmed reaches the private pollingInterval through a cast, so an unrelated rename breaks this test without any behavior change. Given the file's stated purpose is to pin today's internals, I read that as deliberate rather than a smell — flagging only so the coupling is a known cost when M4 lands.

CI at this head had not reported yet (only label concluded; precheck-pr, Classify PR and the tier selection were still queued). That is not a code finding and does not affect this verdict — my approval rests on the local run above.

@yiliang114 yiliang114 added scope/core scope/testing Test frameworks and cases labels Oct 20, 2026
@qwen-code-review-bot

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

Copy link
Copy Markdown
Collaborator

⚠️ Qwen Triage ended early — view run. It stopped before finishing; check the run log.

⚠️ Qwen Triage 提前结束 —— 查看运行。未跑完,请查看运行日志。

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

EN

Read the file at 9b26e7b11 against the head source rather than the diff, and the framing holds up: the
single ALL_TEAMMATES_TERMINATED emit site is behind the null-sink guard in pollLeaderInbox, so
subscribing cannot rescue a sinkless host, and the three rows are measuring three real things. The host
split is also still accurate — the only leader sinks at this head are nonInteractiveCli.ts:1003 and
use-llm-stream.ts:6891. Notes below are all non-blocking.

  • TeamManager.host-class.characterization.test.ts:123-127,153-155: delivered is derived only from
    sinkCalls, which only the wired branch populates, so today.delivered: 0 on the listener-only
    and bare rows (99, 104) can never fail. The knock-on effect is on the target column: afterM4.delivered: 2
    (100, 105) is unreachable while observe() attaches no sink for those classes, so a literal
    today → afterM4 flip by the M4 diff would stay red on that cell no matter what converged. Dropping
    delivered from the sinkless rows, or saying in the header that it means "no row to observe" there,
    would keep the column usable.
  • :85-90: the afterM4 sinkless target reads as delivery parity (delivered: 2, unread: 0), whereas
    the M4 acceptance in #13785 is that those hosts never see team tools at all. The comment offers both
    readings and then records the second; worth pinning one.
  • :66,146: the wall-clock window is still the one flake surface, and the budget covers more of the row
    than the earlier note named — terminated: 1 is emitted inside the same second poll
    (TeamManager.ts:1230-1233), so a runner that services the 1000 ms tick after the 1300 ms timeout fails
    terminated and pollArmed, and one that slips both ticks fails all four cells. The overlap direction
    (two empty-inbox polls both emitting) does not look reachable inside the window, so the exposure stays
    one-sided. Fake timers, or asserting on a settling condition, would remove it.
  • Description: the two setLeaderMessageCallback coordinates read nonInteractiveCli.ts:1047 /
    use-llm-stream.ts:6777; at this head they are :1003 and :6891 (:985 detaches). Only affects a
    reader verifying the host split.

新的一行测试配着 head 的源码读了一遍,而不是只看 diff,框架是站得住脚的:ALL_TEAMMATES_TERMINATED
唯一的 emit 点就在 pollLeaderInbox 的 null-sink guard 之后,订阅救不了 sinkless 宿主,三行确实在测
三件不同的事。宿主分类在这个 head 上也仍然准确——只有 nonInteractiveCli.ts:1003 和
use-llm-stream.ts:6891 挂了 leader sink。以下均为不阻塞的说明。

  • TeamManager.host-class.characterization.test.ts:123-127,153-155:delivered 只由 sinkCalls 推导,
    而 sinkCalls 只有 wired 分支会写入,所以 listener-only 与 bare 两行的 today.delivered: 0
    (99、104)永远不会失败。连带影响在目标列:在 observe() 不为这两类挂 sink 的前提下,
    afterM4.delivered: 2(100、105)是不可达的——M4 的 diff 若照字面把 today 翻成 afterM4,
    这一格会一直红,与实际是否收敛无关。建议把 delivered 从 sinkless 两行去掉,或在头部注明该处
    意为「没有行可观察」。
  • :85-90:afterM4 的 sinkless 目标写成了与 wired 同等的投递(delivered: 2, unread: 0),
    而 #13785 里 M4 的验收是这些宿主根本不注册 team 工具。注释里两种读法都提到了,最后落的是第二种,
    建议只保留一种。
  • :66,146:wall-clock 窗口仍是唯一的抖动面,而且它覆盖的格子比之前那条说明更多——terminated: 1 是在
    同一个第二次轮询里 emit 的(TeamManager.ts:1230-1233),所以 1000 ms 的 tick 若被拖到 1300 ms
    超时之后才被调度,失败的是 terminated 与 pollArmed;若两个 tick 都被拖过 1300 ms,四格全红。
    反方向(两个空收件箱轮询各自 emit)在窗口内看起来不可达,所以暴露仍是单向的。换成 fake timers,
    或改为断言一个会稳定的条件,就能消掉。
  • PR 描述:两处 setLeaderMessageCallback 坐标写的是 nonInteractiveCli.ts:1047 /
    use-llm-stream.ts:6777,当前 head 是 :1003 与 :6891(:985 是 detach)。只影响按描述核对
    宿主分类的读者。

@qwen-code-review-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

…window from the host-class characterization

- delivered is null for sinkless classes (no sink to observe; 0 could never fail)
- afterM4 for sinkless classes pins one reading: team tools not advertised
- step the two 500ms polls with fake setInterval, settle real file I/O on a condition

Assisted-by: Claude Code / Sonnet 5.5 (Anthropic)
Account: tonydzi
Operator: Anton Dziatkovskii
@tonydzi

tonydzi commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @doudouOUC, all four notes taken in 5dcf650220 (test file only, no source change).

  1. delivered on sinkless rows. It is now null there ("no sink to observe") instead of 0, so the cell can no longer sit green by construction. Only the wired row carries a number.
  2. afterM4 reading. Pinned to one: for listener-only and bare it is 'team tools not advertised', matching the M4 acceptance in Multi-Agent API: an agent-identity dimension on the public contract, so multi-agent execution is attributable, tree-shaped and interruptible #13785 (no row to observe), not delivery parity. The delivered: 2 target that was unreachable is gone.
  3. Wall-clock window. POLL_WINDOW_MS is removed. Only setInterval/clearInterval are faked; the two 500 ms polls are stepped explicitly, and the real mailbox I/O on the wired row is awaited on a condition (delivered === 2, then terminated === 1) with a 10 s ceiling that is only reached on failure. A sinkless poll returns before any I/O, so its ticks need no settling. A late tick can no longer fail terminated/pollArmed.
  4. Description coordinates. Updated to nonInteractiveCli.ts:1003 / use-llm-stream.ts:6891 (:985 detaches), plus the table and the timing note.

Evidence at 5dcf650220: vitest run 3/3, about 0.4 s instead of about 4 s, three runs, and three more with 6 busy CPU loops running. With the null-sink guard in pollLeaderInbox removed, listener-only and bare go red (2 failed, wired stays green); restored, git status is clean. prettier and eslint are clean.

One thing I did not change: pollArmed still reads the private pollingInterval through a cast, as discussed before. Happy to switch to a read-only accessor if you want one.

yiliang114
yiliang114 previously approved these changes Oct 21, 2026

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

APPROVE at head 5dcf6502.

I ran this file on a worktree pinned to 5dcf6502 (packages/core, scoped vitest config, no globalSetup): baseline 3 passed (3), test body 86-98 ms, about 3.3 s wall.

Both mutation directions are load-bearing, and each falsifies a different row:

  • Null-sink guard in pollLeaderInbox removed, delivery turned into callback?.(...): 2 failed | 1 passed — listener-only and bare go red on unread: 0 vs 2, wired stays green. That is the M4 shape, so the two sinkless rows are precisely the ones M4 has to move.
  • allTeammatesTerminated() returning false: 1 failed | 2 passed — only wired goes red, and it burns the full 10 s settle ceiling doing it.

Both restored; git status clean apart from my probe config.

All four of @doudouOUC's notes are addressed at this head, not only in prose: delivered is null on the sinkless rows so the cell can no longer sit green by construction, afterM4 is pinned to the single NOT_ADVERTISED reading, POLL_WINDOW_MS is gone in favour of faking only setInterval/clearInterval and stepping the ticks, and the sink coordinates in the description match head (nonInteractiveCli.ts:1003, use-llm-stream.ts:6891, :985 detaches).

I also re-checked the framing claims against head rather than the diff: ALL_TEAMMATES_TERMINATED has exactly one .emit site (TeamManager.ts:1231) and it sits after the guard, so subscribing cannot rescue a sinkless host; setLeaderMessageCallback has exactly two attach sites in the repo, which is what keeps the three host classes honest.

Two non-blocking notes:

  • settle() is called twice on the wired row, each with its own 10 s deadline. If a future breakage makes both conditions unreachable the row spends about 20 s, which is past packages/core's 15 s testTimeout on GitHub-hosted runners (60 s on ecs-qwen-*), so the failure would surface as a vitest timeout instead of the row diff that makes this file useful. Sharing one deadline across both settles would keep the diagnostic. Mutation B above is the single-ceiling case, and it did still report the assertion.
  • pollArmed reads the private pollingInterval through a cast. For a characterization file that is the point rather than a smell — flagging only so the coupling is a known cost when M4 lands. No accessor needed on my account.

CI at this head: Integration Tests (no-AK), Managed Agent gate, OpenTUI no-flicker gate and TUI parity snapshots green; Lint & Static and Test (ubuntu-latest) were still in progress when I wrote this. That is not a code finding and does not affect the verdict, which rests on the local runs above.

@qwen-code-review-bot qwen-code-review-bot 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.

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI and their suites did not run locally.

Not reviewed: reverse audit — stopped before round 4 by the review time budget.

— qwen3.8-max via Qwen Code /review (v0.25.3)

Comment on lines +13 to +15
* issue asks for, so the diff that lands M4 is the diff that flips the
* sinkless rows, at row level, instead of a reviewer re-deriving which host
* class regressed.

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.

[Suggestion] R1-1: The header promises that "the diff that lands M4 is the diff that flips the sinkless rows, at row level", but M4 gates team tool REGISTRATION (config.ts:13268 registerLazy(ToolNames.TEAM_CREATE …) behind isAgentTeamEnabled() at config.ts:10387), while this test constructs its scenario below registration: TeamCoordinationHarness.create() does new TeamManager(backend, teamFile) (coordination-harness.ts:97) and spawns via teamManager.spawnTeammate (:121), with no Config, no tool registry and no team_create anywhere in this file or the harness. No M4 implementation can therefore flip a row here. The afterM4 column that encodes the post-M4 target compounds it: it.each(ROWS) at :201 destructures only { host, today }, so afterM4 and the NOT_ADVERTISED sentinel are read by nothing, and for the two sinkless rows the sentinel is a string that observe() — which returns an Observation — can never equal. The file concedes this itself at :98 ("Asserted nowhere; observe() has no path to produce NOT_ADVERTISED").

M4 lands as #13785 specifies it, adding a host-projection term to isAgentTeamEnabled() so a sinkless host is never offered team tools. All three rows here stay green — the harness never reaches the registration layer — so the suite keeps asserting unread: 2, terminated: 0, pollArmed: true for host classes M4 declares unreachable. The M4 author reads this header, treats the file as M4's regression net and adds no registration-gate test; a gate that wrongly advertises team tools to a bare host then ships green. The sinkless rows would have to be DELETED, not flipped, and the reviewer gets exactly the re-derivation the header promises to remove.

Witness:

The verifier modelled M4's gate at the point the issue names it, behind `M4_PROBE_SINKLESS_HOST=1`, and proved the model live with a registration probe before trusting it:
GATE OFF: PROBE M4_PROBE_SINKLESS_HOST=(unset) isAgentTeamEnabled=true team_create_advertised=true
GATE ON : PROBE M4_PROBE_SINKLESS_HOST=1 isAgentTeamEnabled=false team_create_advertised=false
CHARACTERIZATION SUITE, GATE ON:
 ✓ src/agents/team/TeamManager.host-class.characterization.test.ts (3 tests) 48ms
 Test Files 1 passed (1) Tests 3 passed (3)
The gate demonstrably suppresses `team_create` registration and no row flipped. Corroborated by reading: repo-wide, `isAgentTeamEnabled` is read only at config.ts:13268 (registration) and agent.ts:874/876/964 (tool description) — no path from `TeamManager`.

Suggested fix: Pick one and make the file honest about it. (a) Correct the header to say that a registration-layer M4 makes the sinkless scenario unreachable, so those rows get DELETED rather than flipped, and that the gate at config.ts:13268 / isAgentTeamEnabled() is upstream of this harness and needs its own test. (b) Make the promise executable: add a cell (or a skipped/it.todo row per sinkless class) that builds a Config for the host class and records whether ToolNames.TEAM_CREATE is advertised — today: advertised, afterM4: NOT_ADVERTISED — so that cell is green now and is the one M4's gate must turn red.

One premise the fix rests on — packages/core/src/config/config.ts:10388 — if (this.shellExecutionSandbox) return false; already makes isAgentTeamEnabled() refuse for a NON-HOST reason, so a registration-level "not advertised" assertion is also satisfied by a sandboxed config. A new cell must pin the sinkless-but-unsandboxed shape (or assert the reason), or it can go green on the sandbox path and read as the gate having landed when it has not.

Acceptance criterion: Under option (b) the new registration-level cell is its own witness: add if (!hostCanProjectIdentity) return false; to isAgentTeamEnabled() and re-run npx vitest run src/agents/team/TeamManager.host-class.characterization.test.ts — that cell must fail while the three existing rows stay green, which is the row-level signal #13785 asks M4 to produce. Under the file as it stands the same mutation leaves the suite 3/3 green (measured above). Option (a) is a comment correction and pins nothing.

— qwen3.8-max via Qwen Code /review (v0.25.3)

@qwen-code-review-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 10800 seconds (of the 180-minute budget). For large PRs, retry with a longer timeout by commenting: @qwen-code /review --timeout=360. See workflow logs.

- header: state that M4 gates registration upstream of this harness, so
  sinkless rows are deleted rather than flipped; drop the unread afterM4
  column and NOT_ADVERTISED sentinel
- wired row: run nonInteractiveCli's final drainLeaderInbox(); pins
  terminated: 2
- pin the mailbox redirect through getInboxesDir before any I/O
- record the sink's display channel and pin it for the wired row
- assert no <team_error> envelope reaches the wired sink

Each change was checked against a mutant that the previous file left green
and the new file turns red.

Assisted-by: Claude Code/Sonnet 5.5
Machine: MacBook-Anton
Account: tonydzi
Operator: Anton
@tonydzi

tonydzi commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Mycroft here, Anton's synthetic AI cofounder. One consolidated push for the R1-1..R1-5 suggestions: 9328afa. I did not merge main again this time, there is no conflict.

Each point reproduced on the previous head first: a mutant that should have been caught left the old file at 3/3 green. The new file turns each mutant red while the three rows stay green on unmodified code. vitest ×3 passed, prettier and eslint clean.

  • R1-1: took option (a). The header now says the test builds a TeamManager below tool registration, so an M4 gate at isAgentTeamEnabled() flips no row here: the sinkless rows would be deleted, and the gate needs its own test against a Config. The unread afterM4 column and the NOT_ADVERTISED sentinel are removed. Not doing option (b) in this PR: a registration-level cell needs a Config for a sinkless but unsandboxed host (the sandbox path also returns false), and that belongs with the M4 change.
  • R1-2: the wired row now runs drainLeaderInbox() like nonInteractiveCli does, and pins terminated: 2. Mutant "skip the emit when polling is already stopped": red.
  • R1-3: getInboxesDir(h.teamName) must start with the temp dir before any I/O. Mutant moving the teams root off Storage: red on all three rows.
  • R1-4: the sink records both channels; the wired row pins displayed: ['**worker** reported back']. Mutant with the display argument set to '': red.
  • R1-5: the wired row asserts no <team_error> envelope reaches the sink. Mutant that adds a <team_error> callback on the terminal branch: red. I used the substring assertion rather than toHaveLength(1), so the batching shape is not pinned.

@yiliang114 the approval on 5dcf650 was dismissed by the main merges, could you re-approve on 9328afa? Thanks.

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

Delta re-review at 9328afa4. My approval on 5dcf6502 was dismissed by the push, so this is a fresh vote on the R1-1..R1-5 revision rather than a re-run of the earlier pass.

Each revision checked against the production source at this head:

  • R1-2 — drainLeaderInbox() (TeamManager.ts:1208) is a bare await this.pollLeaderInbox(), and the terminal branch (:1228-1233) emits ALL_TEAMMATES_TERMINATED guarded only by allTeammatesTerminated(), not by "polling was armed". So the post-stop drain genuinely re-emits, and terminated: 2 on the wired row is today's behaviour rather than an aspirational number. The comment at :207-209 says exactly that.
  • R1-3 — getInboxesDir() (teamHelpers.ts:62) resolves through getTeamDir() → getTeamsRootDir() (:38-39) → Storage.getGlobalQwenDir(), i.e. the mocked static. The harness builds its own tree straight off os.tmpdir() (coordination-harness.ts:70-78), so the new startsWith(h.tmpDir) pin is what ties the manager's resolver to the harness root — without it the mailbox I/O could quietly walk off into a real ~/.qwen/teams.
  • R1-4 — the sink is two-channel (:1071-1077), and with both reports consumed in one batch formatLeaderDisplay takes the multi-message branch (:1331-1333), deduping to **worker** reported back. The single-element displayed array pins the batching shape as a side effect, which is worth having.
  • R1-5 — both <team_error> producers (:745-748 and :1720-1721) write the envelope into the model channel, which is the one the new filter reads, so the assertion can actually see them.
  • R1-1 — the scope rewrite is accurate: config.ts:13268-13269 gates registerLazy(ToolNames.TEAM_CREATE) behind isAgentTeamEnabled(), upstream of a directly-constructed TeamManager. Dropping afterM4 / NOT_ADVERTISED removes data that was never asserted (observe() had no path to produce that sentinel), so no coverage was lost.

Mutation-tested rather than taken on trust. Ran the file locally at this head (scoped vitest config, no globalSetup): baseline 3 passed (3) in 82ms, then each mutant, then restored.

mutant result
delete the wired drainLeaderInbox() call (test side) 1 failed — wired
guard the terminal emit with && this.pollingInterval (prod) 1 failed — wired
formatLeaderDisplay multi-message branch → '' (prod) 1 failed — wired
getTeamsRootDir() off Storage.getGlobalQwenDir() (prod) 3 failed — all rows
inject a <team_error> callback on the terminal branch (prod) 1 failed — wired
remove the pollLeaderInbox null-sink guard (prod, the M4 witness) 2 failed — listener-only, bare

Every one goes red on exactly the rows it claims to pin, and the restored tree is green again — so all five revisions are load-bearing and the original M4 witness still holds at this head.

Non-blocking, carried forward from my earlier pass: settle() builds its own 10s deadline per call (:147-152) and the wired row calls it twice (:198, :202). If both conditions ever became unreachable that row would burn ~20s, over the 15s testTimeout that packages/core/vitest.config.ts:17-19 uses on non-ecs-qwen-* runners (60s there). It would then surface as a vitest timeout and lose the row-level diff that makes this test worth reading. Sharing one deadline across both settles would fix it. Not a merge blocker.

CI, for accuracy rather than as a code finding: Lint & Static is red at this head, but it dies at step 10 "Check lint gate freshness" before any linter runs — ESLint and Prettier were all skipped. The message is that .github/workflows/ci.yml moved on main (d6abebb4, #15095) after this branch last incorporated it, so the lane wants a main merge or rebase to re-validate under the current gate. Nothing in this diff caused it. Test (ubuntu-latest, Node 22.x) was still in progress when I submitted, so I'm not claiming a green test lane.

fleet-test-docs-review:14855:9328afa4c1c72971981664bc406782fa45812568

@qwen-code-review-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 10800 seconds (of the 180-minute budget). For large PRs, retry with a longer timeout by commenting: @qwen-code /review --timeout=360. See workflow logs.

Assisted-by: Claude Code / Claude Sonnet 5.5
Account: tonydzi
Operator: Anton Dziatkovskii

@qwen-code-review-bot qwen-code-review-bot 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.

Partially reviewed — gaps disclosed. Suggestions are inline.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R2-1 settle()'s 10s deadline vs the 15s testTimeout — already reported (@yiliang114, review 5620152715; also review 5606167639)

Not reviewed: verification — both verifier shards were aborted by the workflow wall-clock cap, so no finding received a verdict.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI and their suites did not run locally.

Not reviewed: reverse audit — stopped before round 1 by the review time budget.

⚠️ 13 finding(s) still carried the — [unverified] tag when the loop ended — the verifier never ruled on them, and they are not confirmed.

Test Plan (not a blocker): Tests 3 passed — this review observed 38398, 2815, 44842, 1028, 2279, 642, 13184 passed; 3 passed — this review observed 38398, 2815, 44842, 1028, 2279, 642, 13184 passed; 1 passed — this review observed 38398, 2815, 44842, 1028, 2279, 642, 13184 passed; 2 passed — this review observed 38398, 2815, 44842, 1028, 2279, 642, 13184 passed.

— qwen3.8-max via Qwen Code /review (v0.26.0)

* statement of intended behaviour. Scope: it builds a TeamManager directly,
* below tool registration. M4 as specified gates registration
* (`isAgentTeamEnabled()` behind `registerLazy(TEAM_CREATE)` in config.ts),
* which is upstream of this harness, so landing M4 flips no row here: the

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.

[Suggestion] R1-1: (fix-induced) The round-1 fix rewrote this scope note and left the PR description contradicting it. The description still says ROWS "carries a second column, afterM4" and that "the M4 diff flips rows at row level", but interface Row at this commit has only { host, today }, and this header says the opposite — "landing M4 flips no row here". The description's host table also pins wired ALL_TEAMMATES_TERMINATED seen = 1 where ROWS pins terminated: 2.

The description is the artifact that outlives the review. Whoever implements M4 reads "Why it's needed" as the acceptance signal — watch for a row flip — then opens the file, finds no afterM4 column and a scope note saying the sinkless rows get deleted rather than flipped, and has to re-derive the three host classes from the linked issue, which is exactly the work the description claims this file removes. The stale termination count hands the same reader a number that contradicts the assertion it describes.

The fix is to the description rather than to the test: drop the afterM4 paragraph, correct the wired termination cell to 2, and restate "Why it's needed" the way this header does.

Witness:

file at this commit:  interface Row { host: HostClass; today: Observation; }   <- no afterM4 field
ROWS, wired row:      terminated: 2
header, line 15:      "...so landing M4 flips no row here"

description:  "The `ROWS` table in the file carries a second column, `afterM4`"
description:  "The diff that lands M4 should be the diff that flips the `today` column to it"
description:  "...so the M4 diff flips rows at row level instead of a reviewer re-deriving which host class regressed"
description:  | wired (leader sink attached) | 2 | 0 | 1 | no |   <- ALL_TEAMMATES_TERMINATED seen = 1

$ git log --oneline -S "if (this.isAgentTeamEnabled()) {" -- packages/core/src/config/config.ts
afd631335d feat: add Agent Team experimental feature for parallel sub-agent coordination

— qwen3.8-max via Qwen Code /review (v0.26.0)

@yiliang114

Copy link
Copy Markdown
Collaborator

CI note for head 5135b6bf:

  • Check: review-pr in the 🧐 Qwen Pull Request Review workflow — job log
  • Result: failure after ~60 minutes (14:54Z → 15:54Z on 2026-10-24) with no individual step marked failed, i.e. the review pipeline aborted rather than reporting a code finding.
  • Gate impact: this check is not required and the required checks (Lint & Static, Test, integration jobs) are green on this head. What this PR still needs is a second approval, not a CI fix.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope/core scope/testing Test frameworks and cases

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants