Repository navigation
Releases
v0.1.17 — 能力说真话
v0.1.17 — 能力说真话
四件互相独立的事,各自单一故障域:
- 面板的限制来自宿主:浏览器半删掉了自己的
MAX_AGENTS/FEED_CAP副本,改读快照里的limits。"未确认"是一等状态(null:不加行、不裁切、计数显示—),不再拿猜的数字当默认。 routeMeta逐字段深校验:未知字段、不存在的档位/额度、负数或非数字的价格、错类型,全部在装载期失败并指出是哪个路由的哪个字段;给尚未服务的路由预写覆盖仍然允许。顺带移除了一个从没人读过的死字段label。- 运行时能力契约 + 中立 registry:provider 自己注册能力解析器(知识留在知识的拥有者那里),hub 侧不出现任何 provider 名特判。
unknown与false严格不同:前者是"还没有证据",后者是"确认不能"。 - 面板展示真实能力:
op=capabilities独立于 board 的 SSE,按 10 秒轮询;全页共享一个 loader(按会话缓存 + 在途请求去重)。一次刷新失败不会把权限行从事实闪成"获取失败",而是保留上次结果并标记stale。
另外修掉一处文档漂移:README / CONTRACT 里"只读靠白名单"的说法与实际实现(精确拒绝)不符,并加了一条路障测试防止它再回来。
测试:191 项(宿主 141 / 跨两半 30 / 契约 20)。
升级后需要重启 dsh web:本次新增了一个 HTTP 操作与一个宿主服务;浏览器半刷新即可,但它调用的 op 在旧进程里不存在。
v0.1.15 - record which runtime took the child
v0.1.15 — record which runtime actually took the child
A small release that makes one fact visible, because it decides whether everything else
on the card means anything.
Why
A third report arrived from the same agent (9b90d379, codex-chatgpt/gpt-6-luna),
this time phrased precisely:
「我可以调用可用工具,但不能修改文件」
That is exactly the shape of an external Codex process: its tools are Codex's own
(apply_patch, exec_command), so it can call tools; its write access belongs to
Codex's own sandbox, which defaults to read-only, so it cannot modify files. It also
settles the honesty gap left open in v0.1.14: this child was dispatched with
可写 — and still could not write, because a power flag the hub enforces through a DSH
tool filter never reaches a runtime that does not use DSH tools.
What changed
subagent/start carries the subagent provider that was chosen, and the hub now records
it on the card as runtime, verbatim. hub_read's roster prints it (· 运行时 codex).
It is not a classification and nothing is gated on it yet — deliberately: the provider
string is the ground truth I did not have, and guessing a rule from one sample would
have been the same mistake as calling codex-chatgpt an in-process route.
The next step is now mechanical: read the recorded string, then stop rendering
可写/可执行 for rows whose runtime is not in-process, and say what such a row can
actually do.
Verified
- Host half 133 assertions (up from 132), cross-half 25. The new one asserts the
recorded runtime reaches both the card and the roster text.
第三次回报来自同一个智能体,这次说清了:能调工具、改不了文件。这正是外部 Codex 进程的形状
——工具是 Codex 自己的(所以能调用),写权限归 Codex 的沙箱(默认只读,所以改不了)。它还坐实了
v0.1.14 里留下的那处不诚实:这个子会话是我用「可写」派出去的,照样写不了——因为由 DSH 工具
白名单兑现的权限标志,根本到不了一个不用 DSH 工具的运行时。
现在把 subagent/start 里那个实际选中的 subagent provider 原样记在卡片上(runtime),
hub_read 的名册会打出来。刻意不做分类、也不据此 gate 任何东西:这个字符串正是我此前缺少的
事实,凭一个样本就推规则,会重蹈"把 codex-chatgpt 当进程内路由"的覆辙。
v0.1.14 - one agent, two cards
v0.1.14 — one agent, two cards; and what a codex row really is
Two findings from a controlled experiment, one of them a bug I introduced in v0.1.7.
The bug: every hub-launched agent got a second card
A launch printed its roster and five cards came back for two agents:
探针-codex · codex-chatgpt/gpt-6-luna · 可写/可执行 · id=9659b543…
探针-deepseek · deepseek-official/… · 可写/可执行 · id=976c2322…
子智能体 1 · codex-chatgpt/gpt-6-luna · 只读 · id=9b90d379…
子智能体 2 · deepseek-official/… · 只读 · id=976c2322… ← same id as 探针-deepseek
子智能体 3 · codex-chatgpt/gpt-6-luna · 只读 · id=9659b543… ← same id as 探针-codex
subagent/start is emitted inside startContinuable, before it returns the child
id — so at that moment the hub's own card still has id === null, the "is this child
already ours?" check misses, and adoption created a second card for the hub's own agent.
Every adopted card claims conservative powers, so each duplicate read 只读 beside a
可写 original: the phantom lane is exactly what "the codex agent is read-only" looked
like from the UI.
Adoption now stands down while a launch is in flight, a dedupe pass drops any adopted
card that collides with a launched id (repointing the child's event link at the real
card), and the post-launch sweep — already there for foreign children — catches anything
genuine that the stand-down window skipped.
What a codex row actually is (measured, not inferred)
Same launch, same powers, same task, two providers:
deepseek-official/deepseek-flash |
codex-chatgpt/gpt-6-luna |
|
|---|---|---|
| tools | 39 DSH tools — write, read, pwsh, hub_post, hub_read, … |
none of them: apply_patch, exec_command, image_gen, dozens of mcp__codex_apps__* |
hub_post |
worked | no such tool |
write |
created the file, no denial | no such tool (only Codex's apply_patch) |
| session policy | danger-full-access |
Codex's own sandbox (permissionMode default never → read-only) |
So a child dispatched on a codex route does not run inside DSH at all — it is an
external Codex process. The powers the hub ticks for it are inapplicable, it cannot post
to the board, and it cannot read the workspace. My earlier reading ("codex-chatgpt is
only an LLM route, so such children are in-process") holds for the conversation's own
agent and is wrong for a dispatched child — I over-extrapolated it, and this is the
correction.
The policy section and both READMEs now say so, in the place a model reads before
assigning work: send file-reading, board-reporting, or file-split work to an in-process
route; a codex row suits "here is material, give me a judgement". Making such a child
able to write is a deployment setting (subagent-codex's permissionMode: approve-for-me → sandbox: workspace-write), and even then it still has none of this
board's tools.
Verified
- Host half 132 assertions (up from 131), cross-half 25.
- The new test reproduces the real ordering — the mock fires
subagent/startfrom inside
startContinuable— and asserts the launch produces exactly one card, that the card
keeps the powers its row asked for, and that no adopted card claims that child id.
开台输出自己暴露了这个 bug:两个智能体,回来五张卡片——每个协作台派发的子会话都被多认领了
一张,而认领的卡片一律显示只读。真因是 subagent/start 在 startContinuable 内部触发、
返回值还没拿到,于是"这个子会话是不是已经有卡片"的判断落空。你在界面上看到的"codex 智能体
只读",很可能就是这张幽灵卡片。
同一批次的对照实验还证明了另一件事(我上一轮的判断错了):路由到 codex 的子会话根本不在
DSH 里跑——它是外部 Codex 进程,工具是 Codex 自己的,没有 hub_post/hub_read、没有 DSH
读写工具,写权限归 Codex 自己的沙箱(默认只读)。所以需要读文件或需要汇报进度的活,要派给
进程内路由。这条已写进策略段与两份 README。
v0.1.13 - a line the machine disagreed with
v0.1.13 — a line in the metadata that the machine disagreed with
A one-sentence correction, found by comparing what the shipped prompt says against what
the running deployment actually lists.
The codex-chatgpt/gpt-5.5 entry claimed:
app-server 已把它标为 hidden,不会再出现在路由清单里
(the app-server marks it hidden, so it will not appear in the route list)
After the restart, it is in the route list — 20 routes, gpt-5.5 among them. The two
surfaces disagree: codex debug models reports visibility: hide for it, while the
app-server's model/list (which the provider actually filters on, via hidden === true)
does not mark it. The entries that do get filtered are gpt-reserve and
codex-auto-review.
The line now says so, because a prompt that states something the machine contradicts is
worse than no line at all — the coordinator would have been reasoning about a model list
that does not exist.
Also confirmed by that restart
- 20 routes, with
codex-chatgpt/gpt-6.1-sol(中价)present: the build-selection fix
picked the installed 0.159.2 over the older 0.158.0-alpha, and the hub now recognises
the model it advertises. - The metadata written from the app-server's own descriptions is live:
gpt-6-sol(中价),gpt-6-luna(低价),gpt-5.6-luna(低价),
deepseek-official/deepseek-flash(低价).
Verified
Host half 131 assertions, cross-half 25, both unchanged and green — this release
changes one string.
一句话的更正:gpt-5.5 的边界写着"app-server 已把它标为 hidden,不会再出现在路由清单里",
而重启后的清单里它就是出现了(20 条路由)。两个面不一致:codex debug models 报
visibility: hide,而 provider 实际过滤的是 app-server model/list 的 hidden 标志,
后者没这么标。真正被滤掉的是 gpt-reserve 与 codex-auto-review。提示词里说一件机器会反驳的
话,比不说更糟——协调者会据此推理一个不存在的模型清单。
v0.1.12 - lanes show what is happening now
v0.1.12 — the lane area shows what is happening now
A finished agent used to sit in the lane list forever, next to the ones still working.
The lane area exists to answer "what is happening now", so it no longer pretends that a
finished agent is part of the answer.
What changed
The lanes are split into two surfaces, with counts on the switch:
- 进行中 / active — everything that is not terminal:
draft,queued,running,
idle. This is the default view, and the conversation's own agent leads it, because it
is never a finished agent. - 已完成 / finished —
done,error,stopped.
Finished agents keep every action. That is the point of the page, not a side effect:
the host re-marks a terminal card as running when a message is delivered to it, so waking
a finished agent to ask a follow-up works — and the one state that disables those three
controls is draft, which no finished agent is in. A test pins exactly that rule, so a
future edit adding a terminal-status check there fails loudly instead of quietly turning
the completed page read-only.
Order is preserved on both sides, so settling does not reshuffle the board.
Scope
Browser half only. Nothing in the host changed, so this needs a page refresh and not a
dsh web restart — unlike the host-side changes still waiting in v0.1.8–v0.1.11.
Verified
- Cross-half: 25 assertions (up from 23). The partition itself (which status lands on
which surface, with order preserved, and the empty case), and the guard that keeps
finished agents callable. - Host half: 131 assertions, unchanged.
已结束的智能体此前会永远留在泳道列表里,和还在干活的那些混在一起。泳道区要回答的是"现在
发生了什么",所以它不再把一个已结束的智能体算进答案里。现在分成两个面:进行中(默认,
draft/queued/running/idle,会话自己的智能体排第一)与已完成(done/error/stopped)。
关键点是已完成页保留全部动作——这不是副作用而是这页存在的理由:宿主在向终态卡片投递消息时会把
它重新标记为运行,所以"唤醒一个已完成的智能体追问一句"是可用的;而唯一会禁用那三个控件的状态是
draft,已完成的智能体不会处于该状态。有一条测试专门钉住这条规则,将来谁在那里加上终态判断,
会响亮地失败,而不是悄悄把完成页变成只读。
v0.1.11 - say what the codex routes actually are
v0.1.11 — the codex provider was running the wrong build
Reported as "recognise gpt-6.1-sol". The hub was not out of sync, and the model
metadata was not the problem: the provider had picked the wrong codex.exe, and
the older build simply does not have that model.
What the machine actually had
%LOCALAPPDATA%\OpenAI\Codex\bin\
faa963e871dd422c\codex.exe codex-cli 0.158.0-alpha.2.1 9 models, no gpt-6.1-sol
c6fe824d725f02d7\codex.exe codex-cli 0.159.2 10 models, gpt-6.1-sol (list)
56d1c54034a4a372\ no codex.exe
findCodexExecutable ranked those directories by name:
for (const build of builds.sort().reverse()) { … } // "highest build wins"The comment says the comparison is version-aware. The directory names are content
hashes, so "highest" is arbitrary — and on this machine it resolved to the older
build. The consequence was silent: the provider served a narrower catalogue, and
gpt-6.1-sol was simply absent from the hub with nothing anywhere saying why.
Recency now comes from the executable's own modification time, with the name kept
only as a deterministic tie-break (one app update writes several build directories in
the same second). Verified against the real installation: the resolver returns
c6fe824d725f02d7, the 0.159.2 build.
The reason it could go wrong quietly
findCodexExecutable had no tests at all — a function that decides which Codex
build the entire provider runs on, documented with an assumption that is false on the
machine it runs on. It now has a suite of its own covering newest-wins (with a name
order that would pick the other one), build directories without an executable, the
equal-timestamp tie-break, both override paths, and the nothing-installed case.
Route metadata, now read rather than assumed
The built-in catalogue's codex entries were my impressions. They are now taken from
the app-server's own descriptions, read with codex debug models on the installed
build — which is also where the news was:
| route | app-server says | tier |
|---|---|---|
gpt-6.1-sol |
Latest workhorse model for coding and everyday work | mid (new) |
gpt-6-astra |
Frontier intelligence for the most demanding work | high |
gpt-6-sol |
Previous generation workhorse model | high → mid |
gpt-6-luna |
Fast and affordable model for easier tasks | mid → cheap |
Unknown codex routes still fall back to the provider-level entry, so a model nobody
has described yet is treated conservatively instead of vanishing.
Verified
- Hub: 154 assertions (131 host + 23 cross-half), up from 153. A known model gets
its own line (gpt-6.1-sol→ 中价 / 最新主力), a renamed one says so (gpt-6-sol→
上一代主力), and an unknown one still falls back to the provider entry. - Codex provider: its resolver suite is new (5/5), alongside the existing provider
suite.
After the restart
Both halves are host-side, so one restart covers them. op=models should then list
19 routes, codex-chatgpt/gpt-6.1-sol among them — that is the end-to-end check,
and it is the one thing here I could not observe before the restart: what is verified
now is the resolver's return value against the real directory layout, and that the
build it now selects advertises the model.
报的是「同步识别 gpt-6.1-sol」。协作台并没有不同步,元数据也不是问题:provider 选错了
codex.exe——它跑的是 0.158.0-alpha(9 个模型,没有这个),而机器上装着 0.159.2(10 个模型,
gpt-6.1-sol 可见)。findCodexExecutable 按目录名排序取"最高",可那些名字是内容哈希,
与版本无关;注释里那条"版本感知"的前提在这台机器上就是假的,而且这个函数一行测试都没有。
现在改为按可执行文件自身的修改时间取最新(名字只做同秒时的确定性兜底),真机验证返回 c6fe824d…。
顺带把 codex 各路由的档位与擅长改为读 app-server 自己的描述,其中一条是新闻:gpt-6-sol
已被标为"上一代主力",最新的主力是 gpt-6.1-sol。
v0.1.10 - no more empty spinning lanes
v0.1.10 — a lane can no longer spin with nothing behind it
Reported as "empty-turn subagents are appearing": a lane on the board marked running
with no activity at all, next to a normal agent that produced twenty progress lines.
What was actually happening
subagent/start is emitted when a child is published, and publication happens
before the child's first prompt — the caller can still abort in between. Upstream's
own tests do exactly that:
ctx.on('subagent/start', () => { controller.abort('caller gave up') })The card read that event as proof of work and went to running. For a child that then
went on to run, the next events corrected it. For one that was published and never
prompted, nothing ever would: a lane sat on the board claiming to be working, with
toolCalls: 0, lastActivityAt: 0, and no terminal event coming — which is what an
empty, spinning lane on the board actually was.
The empty prompt was never the problem: validateSpec has always rejected a row
without a task or a route.
The fix
A card's status is now reconciled against the live agent whenever the board is
read — the same treatment the conversation's own card already got, for the same reason:
the runtime's status is the truth, and an event we happened to catch is not.
Three rules keep it honest:
- Terminal statuses are never overwritten.
done/error/stoppedare
evidence we observed, and a child session routinely outlives the run that produced it. - Only a status the runtime actually reports is used. Anything else leaves the value
the events folded in place, rather than guessingidleover a status the harness just
announced. hub_read's roster reconciles too, so an agent reading the board and a human
looking at the lanes are told the same thing about who is working.
phase is computed from the reconciled cards for the same reason: a stale running
card used to pin the board to running after everything had finished.
How to tell in one look whether this was the cause
After restarting, the lane that was spinning should read 空闲 instead of running
without ever having produced a line. If it still reads running, then that agent really
is running and the missing piece is its events — a different bug — but the board will
no longer be able to make either claim from a publication event alone.
Verified
- Host half: 130 assertions (up from 127). A published-but-never-prompted child
reportsidle; a genuinely running child still reportsrunning(the reconcile is
not a blanket downgrade); and the status the harness reports is the one both the
panel andhub_readshow. - Cross-half: 23 assertions, unchanged.
报的是「空转子智能体」:板上一行显示运行中,却一条动作都没有——旁边正常的智能体已经产出二十条。
真因是 subagent/start 在子会话被发布时触发,而发布发生在首次提示词之前,调用方可以在中间
弃掉(上游测试里就有 subagent/start 里 abort 的写法)。卡片把"发布"当成"在干活",于是对那种从未
被提示过的子会话,永远没有后续事件来纠正它——toolCalls: 0、lastActivityAt: 0、也不会有终态
事件。空提示词从来不是问题:validateSpec 一直要求非空 task 与路由。
现在卡片状态在每次读取时与活着的 Agent 对齐(会话自己那张卡早就这么做了,理由相同:运行时的
状态才是真的)。三条约束:终态绝不被覆盖(那是观察到的证据);只采纳运行时真的报出来的状态;
hub_read 的名册同样对齐。phase 也从对齐后的卡片算——否则一张陈旧的 running 卡会把整块台
永远钉在"运行中"。
v0.1.9 - route choice weighs cost, quota and capability
v0.1.9 — route choice now weighs cost, quota and capability
Until now the coordinator saw a flat list of provider/model strings. The only thing
it could rank them by was its own prior, and the classic failure follows: bulk
mechanical work goes to the flagship, the quota is gone, and the judgement-heavy roles
that actually needed it get the cheap model.
Every route now carries what a chooser needs, on the line itself:
deepseek-official/deepseek-flash|成本:低价,约 $0.1/$0.4 每百万 token(入/出)|额度:额度充裕|擅长:…|边界:…
codex-chatgpt/gpt-6-astra|成本:高价,订阅制(不按 token 计价)|额度:额度紧张|擅长:…|边界:…
src/catalog.js— the built-in metadata table (cost, cost tier, quota posture,
strengths, boundaries) for the three providers this deployment serves, plus the
operator escape hatch: unknown routes degrade to a bare route name and never block a
launch.- The coordinator's rules now say what to do with it: match capability rather than
prestige, respect quota posture (a thin route gets one or two roles, never the bulk
rows), never hand a role to a model whose boundaries exclude it, prefer the route
whose strengths name the work. The "a different model for the adversarial reviewer"
rule survives intact. routeMetaconfig lets an operator correct the estimates perprovider/model
(or provider) without touching code, and the standing policy lists the tiers so the
lead picks itsmodels:shortlist with the same awareness.- Validation is unchanged and still runs on the raw ids: the annotations are only
what the coordinator reads.
The bug this feature arrived with
The coordinator's annotated list honoured routeMeta; the policy line did not
(tierTagOf called routeMetaOf with no overrides). The same route would therefore be
"中价" to the lead and "低价" to the coordinator — two views of one fact, computed
separately, drifting. That is the shape of bug this codebase has already been bitten by
once, so it is now pinned by a test that asserts both views move together.
Two things to be clear about
- The prices are estimates, not verified. They are plausible round numbers, marked
约and described as estimates in both the prompt and the module doc, but nobody
checked them against a price list. If you have real numbers,routeMetais the fix —
and the prompt tells the coordinator to trust your values over the built-ins. routeMetafield types are not validated: the config layer only checks that the
entry is an object. A malformed value degrades gracefully (an unknown tier just drops
the tag) rather than failing the launch, but a typo will be silent.
Verified
- Host half: 127 assertions (up from 123). An echoed annotated line still resolves to
its route; an unknown route degrades to its bare name and stays overridable; the
operator override moves the coordinator's line and the policy line together; and a
subscription route renders cost, quota, strengths and boundaries. - Cross-half: 23 assertions, unchanged.
协调者此前只看到一串裸 provider/model,只能凭自己的先验排序,于是经典的失败模式:批量机械活
派给旗舰、额度用光,真正需要判断力的角色反而拿到便宜模型。现在每行都带上成本档位与估价、额度
态势、擅长与边界,协调者的规则里也写清了怎么用(能力匹配而非名气、额度紧张的路由最多给一两个
角色、不把角色派给边界不覆盖它的模型)。同时修掉了这个功能自带的一个缺陷:tierTagOf 没把
操作者的 routeMeta 传下去,于是同一路由对主智能体是"中价"、对协调者是"低价"——同一事实算了
两遍、各走各的。价格是估价不是核实过的价目(已在提示词与模块文档里标明,routeMeta 是更正
入口);routeMeta 只校验了"是个对象",字段类型未校验,坏值会静默降级。
v0.1.8 - no more read-only, tool-starved agents
v0.1.8 — the two reasons a dispatched agent arrived crippled
Reported as "codex lacks tools, and is read-only by default". Both halves were the
hub's fault, and neither had anything to do with the model or its provider.
"Read-only by default"
hub_launch's roster rows hardcoded the powers off:
powers: { write: row?.write === true, shell: row?.shell === true }So every row a model authored was read-only unless it wrote write: true on each
line — while the panel's rows took defaultWrite, which is true. One plugin with
two contradictory defaults, and the documented one (defaultWrite: true) was the one
the tool path ignored. Rows now fall back to the plugin settings exactly as the panel
does, so the config is the single source of truth and the tool description says so.
"Lacks tools"
A read-only row was expressed as an allow-list:
return { allow: existing(READ_ONLY_TOOLS) } // oldallow means "these and nothing else", so the agent lost every tool the list did not
name — job control, team tasks, and anything another plugin registers. That is not
what "read-only" means, and it is why dispatched agents reported missing tools.
Read-only is now a deny-list of the write and shell tools the live registry
actually has. The allow-list survives as the fallback for a registry where no write
tool is recognisable: there a deny-list would deny nothing and hand out write access,
which is the one outcome a read-only request must never produce. The mapping is now an
exported pure function so the policy is testable without a live registry.
What the plugin still cannot do
SubagentStartRequest is {agentOptions, maxDepth, toolFilter, persona, …} — there is
no sandbox or permission field. A child's file policy is fixed when its session is
created and the hub cannot set it. So if an agent can see the write tool and still gets
[sandbox: file access denied under read-only mode], that is the session's policy, not
this switch — and the policy section now says so, rather than letting an agent retry a
denied write forever.
Separately: codex-chatgpt is an LLM route (inject: ['llm'],
ctx.llm.registerAdapter), not a subagent runtime, so a codex-routed child runs
in-process with DSH's tools and this plugin's filter — it is not subject to Codex's own
sandbox. The permissionMode setting that does pin an external Codex process to
read-only belongs to the subagent-codex provider and only matters if you dispatch
through that provider instead.
Verified
- Host half: 123 assertions (up from 121). Read-only now yields
{deny: ['write','edit','pwsh']}on a realistic registry and carries no allow-list;
write-without-shell denies only the shell tools; both granted yields no filter; an
unrecognisable registry still falls back to the whitelist; and a tool row that omits
writelaunches unrestricted. - Cross-half: 23 assertions, unchanged.
报的是「codex 缺少工具、且默认只读」,两半都是协作台自己的问题,与模型和 provider 无关。
「默认只读」是 rosterRowOf 把 powers 硬编码成 false,导致模型写的每一行都只读,而面板写的行
按 defaultWrite(默认 true)——同一个插件两套相反的默认值。「缺少工具」是只读被实现成
白名单:allow 的含义是"只有这些",于是没被点名的工具(任务控制、团队任务、别的插件注册的)
全被砍掉。现在只读是按实际注册表精确拒绝写类与命令类工具,白名单只作为"一个写工具都认不出来"
时的兜底(那时 deny 会等于不设限,等于交出写权限)。另外查清了插件做不到的部分:
SubagentStartRequest 没有任何沙箱字段,子会话的文件策略在创建时即固定。
v0.1.7 - adopt the agents you start
v0.1.7 — the board adopts the agents you start, and stops advertising itself
Two changes, one of them the actual answer to "the subagents and the hub don't sync".
Why they did not sync
The board only ever knew the children it had launched. subagent/start fires for
every child in the conversation, but the hub's handler looked the child up in a map it
filled at launch time, found nothing, and returned — so an agent started with the
native subagent tool or as an Agent Teams teammate ran entirely off the board:
no lane, no progress, no terminal status. That is not a rendering bug; the host half
was dropping every one of those events.
Now subagent/start adopts the child. It appears as a row with its own name,
route, status, activity and streamed output, and its session/event traffic lands on
the board like any other agent's.
Three details this needed:
- The parent is not in the event.
SubagentRunInfocarries{runId, provider, id, local}— no parent — so the parent comes from the child's own session header. - The board is created on demand. A conversation that starts help before anyone
opens the hub would otherwise have nowhere to put it. Only a live parent session
qualifies, so a stale id cannot mint a phantom board. - Direct children only. A grandchild is a participant of the same board but not a
row: the parent must be the board's own session, not merely resolve to it, or the
board becomes a call tree.
The bug that adoption exposed
launch() spawned from board.agents. The moment adopted cards live in that array, a
launch would dispatch a second copy of every agent the conversation already had
running. It now spawns from the roster it just built, and there is a test that
asserts exactly one new child per launch row. The same array drove the plan editor in
the client, so adopted rows are filtered out of it too — both halves are pinned.
The composer dock is gone
The strip above the input box — "run it in parallel with the Agent Hub" plus an
"open the hub" button — has been removed, along with the component behind it. It made
a claim about every message the user typed whether or not that message was parallel
work, and the panel was already one click away in the sidebar and as a conversation
view tab. The hub is a tool, not a prompt.
Verified
- Host half: 121 assertions (up from 116). A natively spawned child is adopted with
its name and route; its progress arrives from its own session events; a re-launch
spawns exactly one new child and keeps the adopted lane; a grandchild is not adopted;
and a conversation with no board gets one when it spawns an agent. - Cross-half: 23 assertions, including that the client registers exactly three
slots and no composer dock.
子智能体和协作台"不同步"的真因不是界面:宿主半只在自己派发的那一刻登记子会话,而
subagent/start 对所有子会话都会触发——查不到登记就 return,于是原生 subagent 或 Agent Teams
成员派出来的智能体整条生命周期都在板外,事件被逐条丢弃。现在会认领它们(父会话从子会话
自己的 header 读,因为事件里没有;没有台就按需建台;只认领直接子会话)。认领也暴露了一个 bug:
launch 原本从 board.agents 派发,认领的卡片一旦留在那里,一次开台会把已经在跑的智能体再派发
一份——现在只派发本次名册,两半都有测试钉住。另外按你的要求删掉了输入框上方的协作台条与
"打开协作台"按钮:它对你输入的每一句话都在暗示"这可以并行处理",而面板本来就在侧边栏。