diff --git a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
index c09041769f..16591b4f3e 100644
--- a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
-2026-07-12-agent-scope-runtime-design.md: cd916ab1bf9f4bb7a02cb5c6a8458630b757e09d
-2026-07-12-agent-scope-runtime-design.zh.md: a0db51d7129d744b96946782a920e99b0cd7b518
+2026-07-12-agent-scope-runtime-design.md: b9b2c837f560c2b8b98da37e3656caf298b705e2
+2026-07-12-agent-scope-runtime-design.zh.md: 4b94d1b8449bd619c47f6bee0712ec8ed0b39593
diff --git a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
index cd916ab1bf..b9b2c837f5 100644
--- a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
+++ b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
@@ -119,30 +119,28 @@ Publication admits and announces resources in the order required by observers:
1. Enter the session.
2. Enter the agent.
3. Announce `session/created`.
-4. Announce `agent/created`.
-5. Enable public driving.
-6. Emit `agent/session-start`.
-7. Start the driver.
+4. Await serial `agent/created` listeners.
+5. Release queued input to the driver.
-The agent never drives before both registries and creation notifications agree. A synchronous listener may veto or dispose an owner; the transaction records publication in progress and waits for that callback stack to unwind before teardown continues. Every creation announcement that begins has a matching disposal announcement during rollback.
+The agent never drives before both registries and creation listeners finish. A listener may reject or dispose an owner; the transaction retains the scope and session until dispatch settles before teardown continues. Every creation announcement that begins has a matching disposal announcement during rollback. The [awaited creation decision](2026-09-09-awaited-agent-creation.md) owns asynchronous initializer timing.
-The sequence diagram isolates the non-obvious race: a synchronous creation listener can request disposal while the publication call stack still owns both registry entries. Teardown must deactivate immediately but wait for that stack to unwind before stopping and detaching anything.
+A creation listener can request disposal while publication still owns both registry entries. Teardown deactivates immediately and waits for the awaited dispatch before stopping and detaching anything.
```mermaid
sequenceDiagram
participant Tx as AgentCreationTransaction
participant Registries
- participant Listener as Synchronous listener
+ participant Listener as Creation listener
participant Driver
Tx->>Tx: mark publication in progress
Tx->>Registries: announce agent/created
- Registries->>Listener: invoke inside the same call stack
+ Registries->>Listener: await listener
Listener->>Tx: dispose reentrantly
Tx->>Tx: deactivate, teardown waits for publication
Tx-->>Listener: disposal request accepted
- Listener-->>Registries: return
- Registries-->>Tx: announcement unwound
+ Listener-->>Registries: settle
+ Registries-->>Tx: dispatch settled
Tx->>Tx: resolve publication settlement
Tx->>Driver: stop and drain
Tx->>Registries: detach agent, then session
@@ -153,7 +151,7 @@ sequenceDiagram
Every teardown request joins one memoized path. The order is:
-1. Deactivate creation or driving and let synchronous publication finish.
+1. Deactivate creation or driving and await creation dispatch.
2. Stop and drain the driver, discarding any injection that remains pending.
3. Detach the agent.
4. Detach the session.
diff --git a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md
index a0db51d712..4b94d1b844 100644
--- a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md
@@ -119,30 +119,28 @@ Setup 接收完整的子上下文和确切的未发布 Agent,可以等待插
1. 将会话写入注册表。
2. 将 agent 写入注册表。
3. 宣告 `session/created`。
-4. 宣告 `agent/created`。
-5. 启用公开驱动。
-6. 发射 `agent/session-start`。
-7. 启动 driver。
+4. 等待串行 `agent/created` 监听器。
+5. 向驱动器释放已排队输入。
-Agent 在两个注册表和创建通知都达成一致之前绝不驱动。同步监听器可以否决或 dispose 一个所有者;事务记录发布进行中,并等待该回调栈展开后再继续拆除。每个已开始的创建宣告在回滚期间都有匹配的销毁宣告。
+Agent 在两个注册表与创建监听器都完成前绝不驱动。监听器可以拒绝或 dispose 一个所有者;事务保留作用域与会话,等待分发结算后再继续拆除。每个已开始的创建宣告在回滚期间都有匹配的销毁宣告。[可等待创建决策](2026-09-09-awaited-agent-creation.zh.md)拥有异步初始化器时序。
-以下序列图隔离了非显而易见的竞态:同步创建监听器可以在发布调用栈仍拥有两个注册表条目时请求 dispose。拆除必须立即停用,但要等待该栈展开后才停止和分离任何东西。
+创建监听器可以在发布仍拥有两个注册表条目时请求 dispose。Teardown 会立即停用,并等待所调用的异步分发完成后才停止和分离资源。
```mermaid
sequenceDiagram
participant Tx as AgentCreationTransaction
participant Registries
- participant Listener as Synchronous listener
+ participant Listener as Creation listener
participant Driver
Tx->>Tx: mark publication in progress
Tx->>Registries: announce agent/created
- Registries->>Listener: invoke inside the same call stack
+ Registries->>Listener: await listener
Listener->>Tx: dispose reentrantly
Tx->>Tx: deactivate, teardown waits for publication
Tx-->>Listener: disposal request accepted
- Listener-->>Registries: return
- Registries-->>Tx: announcement unwound
+ Listener-->>Registries: settle
+ Registries-->>Tx: dispatch settled
Tx->>Tx: resolve publication settlement
Tx->>Driver: stop and drain
Tx->>Registries: detach agent, then session
@@ -153,7 +151,7 @@ sequenceDiagram
每个拆除请求加入一条记忆化路径。顺序为:
-1. 停用创建或驱动,让同步发布完成。
+1. 停用创建或驱动,并等待创建分发。
2. 停止并排空 driver,丢弃仍处于待处理状态的注入。
3. 分离 agent。
4. 分离会话。
diff --git a/.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.i18n.yaml
index 822fd5f9f2..01690045bd 100644
--- a/.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md
-2026-07-30-session-end-seed-log-boundary.md: 76f8904f75f6c4b5a1a6acab8d69ef2290be718e
-2026-07-30-session-end-seed-log-boundary.zh.md: 4310654c841305a7f0de2f46434d0f839085c482
+2026-07-30-session-end-seed-log-boundary.md: 9cf7c7f20701688208e8b1c343904f2a9f2cab9e
+2026-07-30-session-end-seed-log-boundary.zh.md: fdf4ec9de610f17e792de98334dd5d37f5e4c355
diff --git a/.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md b/.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md
index 76f8904f75..9cf7c7f207 100644
--- a/.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md
+++ b/.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.md
@@ -8,7 +8,7 @@ English | [中文](2026-07-30-session-end-seed-log-boundary.zh.md)
A plugin that owns a standalone open/close bracket in the session log cannot tell a dead marker from a live one. `compaction/start` … `compaction/end` is the shipped case: on picking up a log whose last compaction event is an unmatched `compaction/start`, "the previous writer died mid-compaction" and "a compaction is running right now" are byte-identical stored history. The owner must either refuse to compact a log that is actually free (wedging the session) or proceed over one that is genuinely busy.
-Nothing in the log marked where inherited history ended. `session/created`, `session/disposed`, and `session/flush` are cordis runtime signals, not log events; `agent/session-start` is emit-only. `Session.firstLiveSeq` already held the answer exactly — the seq of this lifecycle's first own write — but only in memory, so a consumer reading stored bytes could not see it.
+Nothing in the log marked where inherited history ended. `session/created`, `session/disposed`, and `session/flush` are cordis runtime signals, not log events; `agent/created` is also a non-durable runtime event. `Session.firstLiveSeq` already held the answer exactly — the seq of this lifecycle's first own write — but only in memory, so a consumer reading stored bytes could not see it.
Crash repair does not close the gap and must not: `interruptedTurnClosers` synthesizes turn, step, and tool boundaries because core owns that vocabulary, and `compaction/*` belongs to the compaction seam. A core repair pass that closed plugin brackets would put every plugin's bracket semantics in core.
diff --git a/.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.zh.md b/.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.zh.md
index 4310654c84..fdf4ec9de6 100644
--- a/.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-30-session-end-seed-log-boundary.zh.md
@@ -8,7 +8,7 @@ Status: implemented
在会话日志中拥有独立开/闭括号的插件无法区分一个已死的标记和一个存活的标记。`compaction/start` … `compaction/end` 就是已发布的实例:当接手一份日志、而它最后的压缩(compaction)事件是一个未配对的 `compaction/start` 时,「上一个写入方在压缩中途死掉了」与「此刻正有一次压缩在运行」在存储历史中是逐字节相同的。该括号所有方只能二选一:拒绝压缩一份其实空闲的日志(把会话卡死),或者在一份确实繁忙的日志上继续压缩。
-日志中没有任何东西标出继承历史在哪里结束。`session/created`、`session/disposed` 与 `session/flush` 是 Cordis 运行时信号,不是日志事件;`agent/session-start` 只发射不落盘。`Session.firstLiveSeq` 本来就精确地持有这个答案——本生命周期第一次自有写入的 seq——但只存在于内存中,因此读取存储字节的消费方看不到它。
+日志中没有任何东西标出继承历史在哪里结束。`session/created`、`session/disposed` 与 `session/flush` 是 Cordis 运行时信号,不是日志事件;`agent/created` 同样是不落盘的运行时事件。`Session.firstLiveSeq` 本来就精确地持有这个答案——本生命周期第一次自有写入的 seq——但只存在于内存中,因此读取存储字节的消费方看不到它。
崩溃修复既没有填上这个缺口,也不应该去填:`interruptedTurnClosers` 合成轮次、步骤与工具边界,是因为核心拥有那套词汇表,而 `compaction/*` 属于压缩 seam。一个会关闭插件括号的核心修复流程,等于把每个插件的括号语义都搬进核心。
diff --git a/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.i18n.yaml
new file mode 100644
index 0000000000..50f7a2eb8b
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.md
+2026-09-09-awaited-agent-creation.md: 27353225a8d99d038d5c61b1eea0fa67d9120a55
+2026-09-09-awaited-agent-creation.zh.md: f664e89da0794acd1d2a6c0c8a4820a7056dcac6
diff --git a/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.md b/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.md
new file mode 100644
index 0000000000..27353225a8
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.md
@@ -0,0 +1,31 @@
+# Agent Note: Awaited per-agent initialization
+
+Status: implemented
+
+English | [中文](2026-09-09-awaited-agent-creation.zh.md)
+
+## Problem
+
+Shared presets install tools and prompt sections separately for each Agent. That installation can await plugin activation, and external SessionStart hooks can produce context asynchronously. A creator must know that these contributions have finished before the Agent's first model request.
+
+## Decision
+
+`agent/created` is the serial initialization event after factory setup and registry entry. Each listener finishes before the next starts; a throw or rejection fails creation and skips later listeners. The payload retains `SessionStartSource` and accepts the factory's cancellation signal. `register()` and `announce()` are awaited by their callers. Lifecycle source selection belongs to factory publication through `announce()`; `register()` announces fresh startup.
+
+AgentLoop holds its existing maintenance activity through setup and creation dispatch. Input may enter the inbox during initialization, but the driver starts only after successful completion. Failure cancels that activity without waking queued input; ordered teardown owns inbox cleanup. Keeping these operations separate preserves the initialization error when another teardown has already removed the inbox projection.
+
+Creation dispatch retains the scope and Session while listeners await. Disposal cancels initialization and joins the dispatch before releasing those resources. A listener must not await its own Agent's idle state or its owner's disposal, because each waits for that listener to finish. Background work on another Agent follows that Agent's own initialization lifecycle.
+
+This decision owns asynchronous creation timing. The [scope runtime decision](2026-07-12-agent-scope-runtime-design.md) retains registry identity and teardown ownership, while the [interception decision](../feature/2026-06-30-interception-extension-points.md) retains policy and tool-event semantics.
+
+## Alternatives considered
+
+**A separate setup event.** Existing creation listeners already install per-agent contributions. A second initialization event splits that responsibility without a distinct consumer need.
+
+**Detached initialization.** Returning before plugin activation or hook context settles lets the first request omit required tools or context and disconnects initialization failure from the creator.
+
+## Consequences
+
+Creation latency includes asynchronous plugin and SessionStart work. Initializer failures become caller-visible creation failures, and cancellation relies on listeners settling cooperatively. Notifications already delivered cannot be undone; rollback pairs them with disposal notifications.
+
+Scoped installer tests verify rollback, lifecycle tests verify ordered completion and cancellation, and the SDK serial-created scenario verifies that asynchronous prompt context reaches the first request. Hook tests cover awaited context injection and subprocess disposal.
diff --git a/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.zh.md b/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.zh.md
new file mode 100644
index 0000000000..f664e89da0
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-09-awaited-agent-creation.zh.md
@@ -0,0 +1,31 @@
+# Agent Note: 可等待的单个 agent 初始化
+
+Status: implemented
+
+[English](2026-09-09-awaited-agent-creation.md) | 中文
+
+## 问题
+
+共享 preset 为每个 Agent(智能体)分别安装工具与提示词段。安装可能要等待插件激活,外部 SessionStart 钩子也可能异步产生上下文。创建方必须知道这些贡献已完成,才能允许 Agent 发起首次模型请求。
+
+## 决策
+
+`agent/created` 是工厂 setup 完成且进入注册表后的串行初始化事件。每个监听器完成后才启动下一个;抛出或拒绝会使创建失败,并跳过后续监听器。载荷保留 `SessionStartSource`,并接受工厂的取消信号。调用方等待 `register()` 与 `announce()` 完成。生命周期来源由工厂发布时通过 `announce()` 指定;`register()` 宣告全新启动。
+
+AgentLoop 在 setup 与创建分发期间保留现有的维护活动。初始化期间输入可以进入收件箱,但只有成功完成后驱动器才会启动。失败会取消该活动而不唤醒已排队输入;有序 teardown 拥有收件箱清理。区分这两项操作,能够在另一条 teardown 已移除收件箱投影时保留初始化错误。
+
+创建分发在监听器等待期间保留作用域与 Session。Dispose(资源释放)会取消初始化,并在释放这些资源前等待分发结束。监听器不得等待自身 Agent 的空闲状态或自身所有者的 dispose,因为两者都要等待该监听器完成。另一个 Agent 上的后台工作遵循该 Agent 自己的初始化生命周期。
+
+本决策拥有异步创建时序。[作用域运行时决策](2026-07-12-agent-scope-runtime-design.zh.md)继续拥有注册表身份与 teardown 归属,[拦截决策](../feature/2026-06-30-interception-extension-points.zh.md)继续拥有策略与工具事件语义。
+
+## 考虑过的替代方案
+
+**单独的 setup 事件。** 现有创建监听器已经安装针对单个 agent 的贡献。第二个初始化事件会拆分这一职责,却没有独立的消费方需求。
+
+**分离运行的初始化。** 在插件激活或钩子上下文结算前返回,会让首次请求缺少所需工具或上下文,也会使初始化失败脱离创建方。
+
+## 后果
+
+创建延迟包括异步插件与 SessionStart 工作。初始化器失败成为调用方可见的创建失败,取消依赖监听器协作结算。已经送达的通知无法撤回;回滚会为其配对销毁通知。
+
+作用域安装器测试验证回滚,生命周期测试验证有序完成与取消,SDK serial-created 场景验证异步提示词上下文进入首次请求。钩子测试覆盖需等待的上下文注入与子进程 dispose。
diff --git a/.agents/notes/implemented/feature/2026-06-30-interception-extension-points.i18n.yaml b/.agents/notes/implemented/feature/2026-06-30-interception-extension-points.i18n.yaml
index d5628fae1e..8ed643d29b 100644
--- a/.agents/notes/implemented/feature/2026-06-30-interception-extension-points.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-06-30-interception-extension-points.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-30-interception-extension-points.md
-2026-06-30-interception-extension-points.md: d628cdce0bc6c13a90a25eeebcfd977602dd76e3
-2026-06-30-interception-extension-points.zh.md: 689d7f6ee6d5a552c2eaf950ce5930be559eb5bb
+2026-06-30-interception-extension-points.md: 5adffd1679c97cc69cf18991913113b84d88ba3c
+2026-06-30-interception-extension-points.zh.md: c3aa374a22d58c5185a3bd8e79b4040a04bd43e3
diff --git a/.agents/notes/implemented/feature/2026-06-30-interception-extension-points.md b/.agents/notes/implemented/feature/2026-06-30-interception-extension-points.md
index d628cdce0b..5adffd1679 100644
--- a/.agents/notes/implemented/feature/2026-06-30-interception-extension-points.md
+++ b/.agents/notes/implemented/feature/2026-06-30-interception-extension-points.md
@@ -15,7 +15,7 @@ The surface needs distinct contracts for per-prompt policy (CC's `UserPromptSubm
The canonical surface separates transformable policy, around-dispatch control, and observe-only notification. Policy waterfalls return small extension-point-specific **typed Decision unions**; wrappers return normalized results; notifications receive immutable snapshots and cannot affect the outcome. The set covers the hook points in scope (`session-start`, `prompt-submit`, `pre-tool`, `post-tool`, `stop`-via-continuation) while leaving non-hook execution policy independently composable.
**Agent events** (`dsh-agent`):
-- `agent/session-start({ agent, source })` — emit, once before turn 1, carrying a `SessionStartSource` (`startup` for a fresh/forked create, `resume` for a reloaded persisted session; `clear`/`compact` reserved). A pure notification — it CANNOT block startup (a deliberate gap: a bridge logs/injects, it does not gate startup). A listener seeds context via `agent.inject()`.
+- `agent/created({ agent, source, signal? })` — serial per-agent initialization before the first turn, carrying `SessionStartSource`. Listeners may install tools and seed context through `agent.inject()`; creation awaits them and rejects on failure. The [awaited creation decision](../architecture/2026-09-09-awaited-agent-creation.md) owns this timing and supersedes the observe-only startup policy.
- `agent/pre-step({ agent, messages, turn, step, signal }, next) → PreStepDecision` — waterfall, fired before every proposed step after the loop has atomically removed its exclusive inbox batch. The payload carries the request's `turn`, `step`, and cancellation `signal` (the retired `PreStepContext` fields live in the payload; see the [payload-object events decision](../../archived/architecture/2026-08-06-agent-event-payload-objects.md)); `messages` is empty for a tool continuation with no intervening input. `enter` returns the complete message batch, including any current-request context a listener contributes; `reject` opens no step and leaves the claimed messages removed.
**`agent/turn-stopping`** is an awaited notification at the natural stop boundary. A listener that needs another step calls `agent.steer()` with explicitly sourced model-facing content; the loop then re-reads the outbox and either continues or closes the turn.
diff --git a/.agents/notes/implemented/feature/2026-06-30-interception-extension-points.zh.md b/.agents/notes/implemented/feature/2026-06-30-interception-extension-points.zh.md
index 689d7f6ee6..c3aa374a22 100644
--- a/.agents/notes/implemented/feature/2026-06-30-interception-extension-points.zh.md
+++ b/.agents/notes/implemented/feature/2026-06-30-interception-extension-points.zh.md
@@ -15,7 +15,7 @@ harness 需要一套钩子子系统:用户像 Claude Code(CC)和 Codex 那
规范接口将可变换策略、环绕调度控制与仅观测通知分离。策略 waterfall(瀑布式事件)返回小型的、扩展点专属的**类型化 Decision 联合类型**;包装层返回规范化结果;通知接收不可变快照,无法影响结果。覆盖的钩子点包括 `session-start`、`prompt-submit`、`pre-tool`、`post-tool`、通过 continuation 实现的 `stop`,同时将非钩子的执行策略留作独立可组合。
**Agent 事件**(`dsh-agent`):
-- `agent/session-start({ agent, source })` ——emit,在第 1 轮次之前触发一次,携带 `SessionStartSource`(`startup` 表示全新/fork 创建,`resume` 表示重新加载的持久化会话;`clear`/`compact` 保留)。纯通知,不能阻塞启动(这是有意的空白:桥接可以记录/注入,但不管控启动)。监听器通过 `agent.inject()` 注入上下文。
+- `agent/created({ agent, source, signal? })` ——首个轮次前针对单个 agent 的串行初始化,携带 `SessionStartSource`。监听器可以安装工具,并通过 `agent.inject()` 注入上下文;创建等待监听器完成,失败时拒绝。[可等待创建决策](../architecture/2026-09-09-awaited-agent-creation.zh.md)拥有这一时序,并取代只观察启动的策略。
- `agent/pre-step({ agent, messages, turn, step, signal }, next) → PreStepDecision` ——waterfall,在每个拟议步骤之前、循环原子移除其独占 inbox 批次后触发。payload 携带该请求的 `turn`、`step` 与取消 `signal`(已退役的 `PreStepContext` 字段位于 payload 中;参见 [payload-object 事件决策](../../archived/architecture/2026-08-06-agent-event-payload-objects.md));没有中途输入的工具续步会收到空批次。`enter` 返回完整消息批次,其中包括监听器为当前请求贡献的上下文;`reject` 不打开步骤,并让已领取消息保持已删除。
**`agent/turn-stopping`** 是自然停止边界上的一次 awaited 通知。需要再执行一步的监听器调用 `agent.steer()`,传入来源显式的 steering(中途引导)内容供模型使用;循环随后重新读取 outbox,继续执行或关闭轮次。
diff --git a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.i18n.yaml b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.i18n.yaml
index 859c0b8e7f..62b98a691a 100644
--- a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md
-2026-07-19-persisted-same-session-goal-domain.md: bc6c9ec14c16b20bc8434bac0f1eb6070ea8920d
-2026-07-19-persisted-same-session-goal-domain.zh.md: 30b9d22633f37dda971104f8d2d5be0d3c8c3163
+2026-07-19-persisted-same-session-goal-domain.md: 4ddc02c3bd3b7bbf04b3c6867a76673431309460
+2026-07-19-persisted-same-session-goal-domain.zh.md: 2bb937b0f637e54ab6d9a6e10ebe237849e08171
diff --git a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md
index bc6c9ec14c..4ddc02c3bd 100644
--- a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md
+++ b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md
@@ -28,7 +28,7 @@ Incremental replay advances its cursor after each valid event and remains positi
At most one goal is current. Create requires no current non-complete goal and always generates a revision-one id not used earlier in the session; a completed goal may be replaced. Every other mutation carries the expected `GoalRef`, and stale ids or revisions reject. Resume accepts a paused or blocked phase, or a disarmed active goal, only when the round cap has remaining capacity. The domain validates blocker reason shape but deliberately leaves reason codes and the decision to block to policy consumers.
-A cache built from any seed starts disarmed, and every `agent/session-start` edge disarms it again. `GoalService.disarm(agent)` also lets a lifecycle owner remove process-local authority without a session event, revision change, or `goal/changed` notification. Resume, fork, and continuation-driver replacement therefore preserve the durable objective and history but never initiate work on their own. A later human prompt can be interpreted by the model, whose policy API may explicitly call resume and arm the goal.
+A cache built from any seed starts disarmed, and every `agent/created` edge disarms it again. `GoalService.disarm(agent)` also lets a lifecycle owner remove process-local authority without a session event, revision change, or `goal/changed` notification. Resume, fork, and continuation-driver replacement therefore preserve the durable objective and history but never initiate work on their own. A later human prompt can be interpreted by the model, whose policy API may explicitly call resume and arm the goal.
### Service boundary
diff --git a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md
index 30b9d22633..2bb937b0f6 100644
--- a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md
@@ -28,7 +28,7 @@ Status: implemented
最多只有一个当前目标。创建要求不存在未完成的当前目标,并始终生成该会话此前未使用过、修订号为一的 id;已完成目标可以被替换。其他每次变更都携带预期的 `GoalRef`,陈旧的 id 或修订号会被拒绝。仅当 Goal Round 上限仍有余量时,暂停或阻塞阶段以及已解除激活的活跃目标才能恢复。领域层校验阻塞原因的形状,但会把原因代码和是否阻塞的决策留给策略消费方。
-从任何种子构建的缓存都以未激活状态开始,每次 `agent/session-start` 边沿也会再次解除激活。`GoalService.disarm(agent)` 还允许生命周期所有者移除进程内权限,而不写入会话事件、不改变修订号,也不发出 `goal/changed` 通知。因此,会话恢复、fork 和继续执行驱动器替换都会保留持久化目标与历史,但绝不会自行启动工作。后续人类提示词可由模型解释,其策略 API 可以显式调用恢复操作并激活目标。
+从任何种子构建的缓存都以未激活状态开始,每次 `agent/created` 边沿也会再次解除激活。`GoalService.disarm(agent)` 还允许生命周期所有者移除进程内权限,而不写入会话事件、不改变修订号,也不发出 `goal/changed` 通知。因此,会话恢复、fork 和继续执行驱动器替换都会保留持久化目标与历史,但绝不会自行启动工作。后续人类提示词可由模型解释,其策略 API 可以显式调用恢复操作并激活目标。
### 服务边界
diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml
index c3956d0da9..23fb232ab3 100644
--- a/docs/architecture.i18n.yaml
+++ b/docs/architecture.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/architecture.md
-architecture.md: 3addf70ab997590d97a2581ea0a459e6d4e86911
-architecture.zh.md: 078d99dc06cf49233d0ca4c036bab89a3f664d57
+architecture.md: efd726f9fcccc53c5bf927f4eadf20b6f5b36be8
+architecture.zh.md: 043eeecd1299e06ad7d728a09036f07dddca6825
diff --git a/docs/architecture.md b/docs/architecture.md
index 3addf70ab9..efd726f9fc 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -75,6 +75,8 @@ Events are the extension points, and picking the right domain is the first decis
- **Agent events** (`agent/*`) carry a live `Agent`: inbox, step, status, request, validation, continuation. Use one to observe or intercept work in flight.
- **Capability events** attach policy and adapters to a seam (`fs/*`, `tools/*`, `telemetry/*`) without importing the loop.
+AgentLoop awaits serial `agent/created` initialization before starting queued work. Initialization failure rolls back creation; [agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) defines teardown ordering.
+
The [event map](event-producer-consumer.md) lists every event's producers and consumers.
## Turn flow
diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md
index 078d99dc06..043eeecd12 100644
--- a/docs/architecture.zh.md
+++ b/docs/architecture.zh.md
@@ -77,6 +77,8 @@ Electron 通过内置的上游 Node.js 进程启动私有 Desktop Host 包;该
- **Agent 事件**(`agent/*`)携带活跃 `Agent`:inbox、步骤、状态、请求、验证、续跑。要观察或拦截进行中的工作时,使用它。
- **能力事件**无需导入循环即可向某个 seam(`fs/*`、`tools/*`、`telemetry/*`)附加策略和适配器。
+AgentLoop 在启动已排队工作前等待串行 `agent/created` 初始化。初始化失败会回滚创建;[agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation)定义 teardown 顺序。
+
[事件映射](event-producer-consumer.zh.md)列出每个事件的生产方与消费方。
diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index 3cde1b9e33..bc61bcd940 100644
--- a/docs/config-catalog.i18n.yaml
+++ b/docs/config-catalog.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: ad1d7d977e0cea23c76256fca626988d51429fb6
-config-catalog.zh.md: 4a3d1692355c0a6ff7306a929fd87050f61ed06b
+config-catalog.md: d13611e7fd2131f8d6fda4fe79ed09397e1c8fe3
+config-catalog.zh.md: ccd14d9b486568188f3e5985c0f8d19a60a612b0
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index ad1d7d977e..d13611e7fd 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -111,7 +111,7 @@ export interface Config {
Depends on: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/core.md)
-Source: [`packages/core/agent-loop/src/index.ts:318`](../packages/core/agent-loop/src/index.ts)
+Source: [`packages/core/agent-loop/src/index.ts:317`](../packages/core/agent-loop/src/index.ts)
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index 4a3d169235..ccd14d9b48 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -113,7 +113,7 @@ export interface Config {
依赖:[`AgentOptions`](subsystems/core.zh.md) · [`SessionId`](subsystems/core.zh.md)
-来源:[`packages/core/agent-loop/src/index.ts:318`](../packages/core/agent-loop/src/index.ts)
+来源:[`packages/core/agent-loop/src/index.ts:317`](../packages/core/agent-loop/src/index.ts)
diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml
index 5969c35226..676aefa070 100644
--- a/docs/cookbook/extension-cookbook.i18n.yaml
+++ b/docs/cookbook/extension-cookbook.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/cookbook/extension-cookbook.md
-extension-cookbook.md: 40d91dbe698a2f2801503a5278c885d6b7a39ed5
-extension-cookbook.zh.md: 149817dec36c5a625e417e9cc1e2879a012682d6
+extension-cookbook.md: b54f0191b48c109b21e111cac27020efcb37174a
+extension-cookbook.zh.md: c03e92b21e156a82d51d2afd742f1dd880e32a47
diff --git a/docs/cookbook/extension-cookbook.md b/docs/cookbook/extension-cookbook.md
index 40d91dbe69..b54f0191b4 100644
--- a/docs/cookbook/extension-cookbook.md
+++ b/docs/cookbook/extension-cookbook.md
@@ -103,7 +103,7 @@ Every product feature maps to a listener on a documented extension point — the
| Product feature | Plugin mechanism |
|---|---|
-| Hook system (user + project level) | listeners on `agent/session-start`, `agent/pre-step`, `agent/request`, `tools/pre-execute`, `tools/post-execute`, and `agent/turn-stopping`; the waterfalls return typed decisions, while `agent/turn-stopping` may steer another step; the `dsh-hooks-claude-code` / `dsh-hooks-codex` bridges map hook config files onto these extension points |
+| Hook system (user + project level) | listeners on `agent/created`, `agent/pre-step`, `agent/request`, `tools/pre-execute`, `tools/post-execute`, and `agent/turn-stopping`; the waterfalls return typed decisions, while `agent/turn-stopping` may steer another step; the `dsh-hooks-claude-code` / `dsh-hooks-codex` bridges map hook config files onto these extension points |
| `/goal` | `ctx.goals` owns durable state, `dsh-goal-round-driver` schedules same-session rounds through the public `Agent`, and separate command/tool producers expose human/model control |
| `/loop` | on the `turn/end` session event, `followup()` the next iteration; or force-continue |
| Dynamic workflow | `ctx.workflowEngine` + the PTC workflow engine + the `workflow` tool; structured in-process children enforce output with scoped prompt/tool registrations, a monotonic tool guard, final `tools/result` commit (including enclosing `run_code`), and the structured-output execution's monotonic `concludeTurn()` marker |
diff --git a/docs/cookbook/extension-cookbook.zh.md b/docs/cookbook/extension-cookbook.zh.md
index 149817dec3..c03e92b21e 100644
--- a/docs/cookbook/extension-cookbook.zh.md
+++ b/docs/cookbook/extension-cookbook.zh.md
@@ -107,7 +107,7 @@ export function apply(ctx: Context) {
| 产品功能 | 插件机制 |
|---|---|
-| 钩子系统(用户级 + 项目级) | `agent/session-start`、`agent/pre-step`、`agent/request`、`tools/pre-execute`、`tools/post-execute` 和 `agent/turn-stopping` 上的监听器;waterfall 返回类型化决策,`agent/turn-stopping` 则可通过 steering(中途引导)触发下一步;`dsh-hooks-claude-code` / `dsh-hooks-codex` 桥接器将钩子配置文件映射到这些扩展点上 |
+| 钩子系统(用户级 + 项目级) | `agent/created`、`agent/pre-step`、`agent/request`、`tools/pre-execute`、`tools/post-execute` 和 `agent/turn-stopping` 上的监听器;waterfall 返回类型化决策,`agent/turn-stopping` 则可通过 steering(中途引导)触发下一步;`dsh-hooks-claude-code` / `dsh-hooks-codex` 桥接器将钩子配置文件映射到这些扩展点上 |
| `/goal` | `ctx.goals` 管理持久状态,`dsh-goal-round-driver` 通过公共 `Agent` 调度同会话 Round,独立的命令/工具生产方分别提供人类/模型控制 |
| `/loop` | 在 `turn/end` 会话事件上 `followup()` 下一次迭代;或强制继续 |
| 动态工作流 | `ctx.workflowEngine` + PTC 工作流引擎 + `workflow` 工具;结构化的进程内子任务通过作用域化的提示词/工具注册、单调工具守卫、最终 `tools/result` 提交(包括外层 `run_code`)和结构化输出执行的单调 `concludeTurn()` 标记来强制输出 |
diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml
index 5a3ebe74e6..b4aef2f914 100644
--- a/docs/event-producer-consumer.i18n.yaml
+++ b/docs/event-producer-consumer.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
-event-producer-consumer.md: b727570dff92aba1bfa0cbb1fba86f9ffe9d9bff
-event-producer-consumer.zh.md: 8fc9c6263e133b4f1fd8703167d44ee530f8b079
+event-producer-consumer.md: f2f4795cf9187adc72ce98d35a452930cba57f35
+event-producer-consumer.zh.md: ed8766bfb45088a7160f51d07f0d6a4fbe2e85a9
diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md
index b727570dff..f2f4795cf9 100644
--- a/docs/event-producer-consumer.md
+++ b/docs/event-producer-consumer.md
@@ -7,21 +7,20 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
-| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:246`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
+| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:245`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:82`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` |
-| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:373`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:258`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:267`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:403`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
-| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:296`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
-| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:304`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
-| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
-| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:330`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:347`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
-| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
-| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:316`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:277`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
-| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:391`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
+| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
+| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | [`agent-presets`](../packages/preset/agent-presets), `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
+| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
+| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
+| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
+| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
+| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
+| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
+| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:601`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:581`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:608`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md
index 8fc9c6263e..ed8766bfb4 100644
--- a/docs/event-producer-consumer.zh.md
+++ b/docs/event-producer-consumer.zh.md
@@ -7,23 +7,22 @@
本矩阵展示哪些包会派发各个 harness 自有事件,以及哪些包会监听这些事件。事件之间存在多对多关系,因此密集的关系数据以表格而非一张大型关系图呈现。接收方和事件名称类型还涵盖有意绕过 `ctx.emit` 的内含派发位置,例如 subagent 生命周期封装。
-| 事件 | 模式 | 声明位置 | 派发方 | 监听方 |
+| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
-| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:246`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
+| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:245`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:82`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` |
-| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:373`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
-| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:258`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:267`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:403`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
-| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:296`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
-| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:304`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
-| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:285`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
-| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:330`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:347`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
-| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
-| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:316`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
-| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:277`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
-| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:391`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
+| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
+| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | [`agent-presets`](../packages/preset/agent-presets), `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
+| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
+| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
+| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
+| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
+| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
+| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
+| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:601`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:581`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:608`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
diff --git a/docs/glossary.i18n.yaml b/docs/glossary.i18n.yaml
index 840997c1e3..a9beea5d35 100644
--- a/docs/glossary.i18n.yaml
+++ b/docs/glossary.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/glossary.md
-glossary.md: 891234c487eea37e6f6beb4779c47054dd011168
-glossary.zh.md: d8fef23873d4c0098b774cf846c8fec156c2d269
+glossary.md: 1a9ac39a73526833be3cdc191d67f519bdf8fc90
+glossary.zh.md: cd41876d41148082ddc18a08c49cf2abba882c79
diff --git a/docs/glossary.md b/docs/glossary.md
index 891234c487..1a9ac39a73 100644
--- a/docs/glossary.md
+++ b/docs/glossary.md
@@ -17,7 +17,7 @@ Domain vocabulary for DeepSeek Harness uses one canonical term per concept. Term
- **scoped dispatch** — the rule: an event about one agent's activity dispatches with that agent's carrier. Events about a registry itself (a tool was added) are *registry-subject* and stay unfiltered.
- **shadowing** — most-specific-wins name resolution: a scoped tool/section/variable replaces its same-named global twin for that scope alone. The per-agent persona and per-agent tool-variant mechanism.
- **restriction / scope-local registration** — a restriction (`tools.restrict`) filters the GLOBAL tool set for one scope (compose by intersection); scope-local registrations are merged after that filter. A filtered-away global tool is absent from the prompt AND refuses execution, indistinguishably from a nonexistent one.
-- **setup window** — the creation slot where a creator composes an agent's scoped world (`CreateAgentOptions.setup`): after the scope and agent object exist but before the agent or session is published, `agent/session-start` fires, or the first prompt is assembled. Setup registers; it never drives the agent.
+- **setup window** — the creation slot where a creator composes an agent's scoped world (`CreateAgentOptions.setup`): after the scope and agent object exist but before the agent or session is published, `agent/created` fires, or the first prompt is assembled. Setup registers; it never drives the agent.
- **lineage** — parent/child facts carried as data (`parentSession`, durable `delegationDepth`, runtime `subagentDepth`); never affects visibility.
## goal
diff --git a/docs/glossary.zh.md b/docs/glossary.zh.md
index d8fef23873..cd41876d41 100644
--- a/docs/glossary.zh.md
+++ b/docs/glossary.zh.md
@@ -17,7 +17,7 @@ DeepSeek Harness 的领域词汇为每个概念规定一个规范术语。各术
- **scoped dispatch**:规则是:关于某个 agent 的活动的事件以该 agent 的 carrier 进行分发。关于注册表本身的事件(如「一个工具被添加了」)属于*注册表主体*事件,保持不过滤。
- **shadowing**:最具体者胜出的名称解析:一个带作用域的工具/片段/变量仅在该 scope 内替换同名的全局对应项。这是按 agent 定制 persona 和按 agent 定制工具变体的机制。
- **restriction / scope-local 注册**:restriction(`tools.restrict`)为单个 scope 过滤全局工具集合(多个 restriction 取交集组合);scope-local 注册在过滤之后合并。被过滤掉的全局工具既不出现在提示词中,也拒绝执行,与不存在的工具无法区分。
-- **setup window**:创建者组装 agent 作用域环境的创建时隙(`CreateAgentOptions.setup`):此时 scope 和 agent 对象已存在,但 agent 或会话尚未发布,`agent/session-start` 尚未触发,首次提示词尚未组装。setup 只做注册,从不驱动 agent。
+- **setup window**:创建者组装 agent 作用域环境的创建时隙(`CreateAgentOptions.setup`):此时 scope 和 agent 对象已存在,但 agent 或会话尚未发布,`agent/created` 尚未触发,首次提示词尚未组装。setup 只做注册,从不驱动 agent。
- **lineage**:以数据形式携带的父子关系事实(`parentSession`、持久的 `delegationDepth`、运行时 `subagentDepth`);从不影响可见性。
## 目标
diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml
index caf832356e..a9958ba12e 100644
--- a/docs/subsystems/core.i18n.yaml
+++ b/docs/subsystems/core.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/core.md
-core.md: deb4743df146983857555016b5bdbe8d2ce29b42
-core.zh.md: f18a00eb02aac23ec07e80b4a9e3ea2234d7e576
+core.md: e5a155854a0a5ecf7b663087bf45f78aeb79297d
+core.zh.md: 24aebd77f54e2b2c0c94e2a044d51b2b14e83f6f
diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md
index deb4743df1..e5a155854a 100644
--- a/docs/subsystems/core.md
+++ b/docs/subsystems/core.md
@@ -336,7 +336,7 @@ type RequestErrorAction = { kind: 'retry' } | undefined
`agent/pre-step` is the only waterfall listener chain before request derivation. `agent/turn-stopping` runs when a turn has no tool or steering continuation, before one final steering drain.
-`agent/session-start` carries a `SessionStartSource` (why the session lifecycle began; a bridge keys its SessionStart matcher on it):
+`agent/created` carries a `SessionStartSource` (why the session lifecycle began; a bridge keys its SessionStart matcher on it):
```ts type-equiv
/** Why a session lifecycle began; seeded creates are `startup`, while persisted loads are `resume`. */
@@ -818,16 +818,16 @@ async create(options: CreateAgentOptions): Promise
async resume(options: ResumeAgentOptions): Promise
/**
- * Register a live agent. Throws if an agent with the same id is already
- * registered. Emits `agent/created` on registration and `agent/disposed`
+ * Register a live agent with source `startup`. Rejects if the id is already registered or a
+ * serial `agent/created` listener fails. Emits `agent/disposed`
* when the calling fiber is disposed — both with the agent's scope carrier
* (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the
* emits are scope-filtered regardless of which context invoked `register`
* (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
* requires passing the carrier). The entry is a runtime root; factory-backed
- * creation uses `options.parentAgent` for child ownership. Returns the disposer.
+ * creation uses `options.parentAgent` for child ownership. Await the registration before using the agent.
* @param agent - the already-constructed agent to record in the store.
- * @returns the EXACT Cordis effect disposer (single-shot; a repeat call
+ * @returns the awaitable Cordis effect disposer (single-shot; a repeat call
* returns undefined without awaiting an in-flight teardown). Exact
* identity is load-bearing: a composite (generator) effect that owns a
* teardown ORDER — the agent factory's lifecycle chain — must yield THIS
@@ -836,7 +836,7 @@ async resume(options: ResumeAgentOptions): Promise
* owner unload, unregistering the agent (and emitting `agent/disposed`)
* while its final turn is still draining.
*/
-register(agent: Agent): () => void
+register(agent: Agent): ReturnType
/**
* Insert an already-constructed agent without announcing it. This is the
@@ -850,19 +850,22 @@ register(agent: Agent): () => void
* the resumed session's durable parent lineage.
* @returns an idempotent closure that removes this exact entry and emits
* `agent/disposed` with listener failures contained. When called from a
- * synchronous `agent/created` listener, removal and disposal wait until
- * that creation dispatch unwinds.
+ * `agent/created` listener, removal and disposal wait until the serial
+ * creation dispatch settles.
*/
enter(agent: Agent, owner: Agent | undefined): () => void
/**
* Announce an agent previously inserted with {@link enter}.
* @param agent - the live inserted agent to announce.
+ * @param source - fresh creation, resume, clear, or compaction source.
+ * @param signal - optional factory initialization cancellation signal passed to listeners.
+ * @returns completion of the serial creation listeners; a listener failure rejects.
* @throws if `agent` is not the exact live registry entry for its id, or its
* creation announcement already began (including a reentrant call from a
* creation listener).
*/
-announce(agent: Agent): void
+async announce(agent: Agent, source: SessionStartSource, signal?: AbortSignal): Promise
/**
* Look up a live agent.
@@ -925,24 +928,27 @@ Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
-
+
-#### `agent/created` — emit
+#### `agent/created` — serial
-A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving extension point. Synchronous listener failure vetoes publication, while returned-promise rejection is reported. Detach requested during dispatch waits until every creation listener has observed the stable entry.
+An entered agent is ready for per-agent initialization after factory setup. Listeners run in order and are awaited before creation resolves. AgentLoop holds queued input until all listeners finish. A throw or rejection fails creation and skips later listeners. Disposal retains the scope and session until dispatch settles; listeners must not await agent.whenIdle() or their own owner's disposal.
```ts cordis-catalog
/**
- * A fully configured agent and live session were published. Setup is
- * composition-only; `agent/session-start` is the first startup-driving extension point.
- * Synchronous listener failure vetoes publication, while returned-promise
- * rejection is reported. Detach requested during dispatch waits until every
- * creation listener has observed the stable entry.
+ * An entered agent is ready for per-agent initialization after factory setup.
+ * Listeners run in order and are awaited before creation resolves. AgentLoop
+ * holds queued input until all listeners finish. A throw or rejection fails
+ * creation and skips later listeners. Disposal retains the scope and session
+ * until dispatch settles; listeners must not await agent.whenIdle() or their
+ * own owner's disposal.
* @param payload.agent - the newly registered agent with its live session and completed setup.
+ * @param payload.source - fresh creation, resume, clear, or compaction source.
+ * @param payload.signal - factory initialization cancellation signal, when provided.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
+ * @mode serial
*/
-'agent/created'(this: Scoped, payload: { agent: Agent }): void
+'agent/created'(this: Scoped, payload: { agent: Agent; source: SessionStartSource; signal?: AbortSignal }): undefined | Promise
```
Types: [Scoped](scope.md)
@@ -1145,30 +1151,6 @@ Types: [LlmFailure](llm-streaming.md) · [ResolvedRetryPolicy](llm-streaming.md)
Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
-
-
-#### `agent/session-start` — emit
-
-The session lifecycle began, once before the first turn. Use `agent.inject()` to seed model-facing context. This is a notification, not a veto; disposal requested by a lifecycle owner is rechecked before the driver starts.
-
-```ts cordis-catalog
-/**
- * The session lifecycle began, once before the first turn. Use
- * `agent.inject()` to seed model-facing context. This is a notification, not
- * a veto; disposal requested by a lifecycle owner is rechecked before the
- * driver starts.
- * @param payload.agent - the agent whose session lifecycle began.
- * @param payload.source - why the session started (fresh startup, resume, …).
- * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
- */
-'agent/session-start'(this: Scoped, payload: { agent: Agent; source: SessionStartSource }): void
-```
-
-Types: [Scoped](scope.md)
-
-Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
-
#### `agent/status` — emit
diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md
index f18a00eb02..24aebd77f5 100644
--- a/docs/subsystems/core.zh.md
+++ b/docs/subsystems/core.zh.md
@@ -344,7 +344,7 @@ type RequestErrorAction = { kind: 'retry' } | undefined
`agent/pre-step` 是请求推导前唯一的 waterfall(瀑布式)监听器链。`agent/turn-stopping` 在轮次没有工具或 steering(中途引导)后续时运行,先于最后一次 steering 排空。
-`agent/session-start` 携带 `SessionStartSource`(会话生命周期为何开始;桥接层据此匹配其 SessionStart):
+`agent/created` 携带 `SessionStartSource`(会话生命周期为何开始;桥接层据此匹配其 SessionStart):
```ts type-equiv
/** Why a session lifecycle began; seeded creates are `startup`, while persisted loads are `resume`. */
@@ -828,16 +828,16 @@ async create(options: CreateAgentOptions): Promise
async resume(options: ResumeAgentOptions): Promise
/**
- * Register a live agent. Throws if an agent with the same id is already
- * registered. Emits `agent/created` on registration and `agent/disposed`
+ * Register a live agent with source `startup`. Rejects if the id is already registered or a
+ * serial `agent/created` listener fails. Emits `agent/disposed`
* when the calling fiber is disposed — both with the agent's scope carrier
* (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the
* emits are scope-filtered regardless of which context invoked `register`
* (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
* requires passing the carrier). The entry is a runtime root; factory-backed
- * creation uses `options.parentAgent` for child ownership. Returns the disposer.
+ * creation uses `options.parentAgent` for child ownership. Await the registration before using the agent.
* @param agent - the already-constructed agent to record in the store.
- * @returns the EXACT Cordis effect disposer (single-shot; a repeat call
+ * @returns the awaitable Cordis effect disposer (single-shot; a repeat call
* returns undefined without awaiting an in-flight teardown). Exact
* identity is load-bearing: a composite (generator) effect that owns a
* teardown ORDER — the agent factory's lifecycle chain — must yield THIS
@@ -846,7 +846,7 @@ async resume(options: ResumeAgentOptions): Promise
* owner unload, unregistering the agent (and emitting `agent/disposed`)
* while its final turn is still draining.
*/
-register(agent: Agent): () => void
+register(agent: Agent): ReturnType
/**
* Insert an already-constructed agent without announcing it. This is the
@@ -860,19 +860,22 @@ register(agent: Agent): () => void
* the resumed session's durable parent lineage.
* @returns an idempotent closure that removes this exact entry and emits
* `agent/disposed` with listener failures contained. When called from a
- * synchronous `agent/created` listener, removal and disposal wait until
- * that creation dispatch unwinds.
+ * `agent/created` listener, removal and disposal wait until the serial
+ * creation dispatch settles.
*/
enter(agent: Agent, owner: Agent | undefined): () => void
/**
* Announce an agent previously inserted with {@link enter}.
* @param agent - the live inserted agent to announce.
+ * @param source - fresh creation, resume, clear, or compaction source.
+ * @param signal - optional factory initialization cancellation signal passed to listeners.
+ * @returns completion of the serial creation listeners; a listener failure rejects.
* @throws if `agent` is not the exact live registry entry for its id, or its
* creation announcement already began (including a reentrant call from a
* creation listener).
*/
-announce(agent: Agent): void
+async announce(agent: Agent, source: SessionStartSource, signal?: AbortSignal): Promise
/**
* Look up a live agent.
@@ -935,24 +938,27 @@ Types: [Scoped](scope.zh.md)
Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
-
+
-#### `agent/created` — emit
+#### `agent/created` — serial
-A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving extension point. Synchronous listener failure vetoes publication, while returned-promise rejection is reported. Detach requested during dispatch waits until every creation listener has observed the stable entry.
+An entered agent is ready for per-agent initialization after factory setup. Listeners run in order and are awaited before creation resolves. AgentLoop holds queued input until all listeners finish. A throw or rejection fails creation and skips later listeners. Disposal retains the scope and session until dispatch settles; listeners must not await agent.whenIdle() or their own owner's disposal.
```ts cordis-catalog
/**
- * A fully configured agent and live session were published. Setup is
- * composition-only; `agent/session-start` is the first startup-driving extension point.
- * Synchronous listener failure vetoes publication, while returned-promise
- * rejection is reported. Detach requested during dispatch waits until every
- * creation listener has observed the stable entry.
+ * An entered agent is ready for per-agent initialization after factory setup.
+ * Listeners run in order and are awaited before creation resolves. AgentLoop
+ * holds queued input until all listeners finish. A throw or rejection fails
+ * creation and skips later listeners. Disposal retains the scope and session
+ * until dispatch settles; listeners must not await agent.whenIdle() or their
+ * own owner's disposal.
* @param payload.agent - the newly registered agent with its live session and completed setup.
+ * @param payload.source - fresh creation, resume, clear, or compaction source.
+ * @param payload.signal - factory initialization cancellation signal, when provided.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
+ * @mode serial
*/
-'agent/created'(this: Scoped, payload: { agent: Agent }): void
+'agent/created'(this: Scoped, payload: { agent: Agent; source: SessionStartSource; signal?: AbortSignal }): undefined | Promise
```
Types: [Scoped](scope.zh.md)
@@ -1155,30 +1161,6 @@ Types: [LlmFailure](llm-streaming.zh.md) · [ResolvedRetryPolicy](llm-streaming.
Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
-
-
-#### `agent/session-start` — emit
-
-The session lifecycle began, once before the first turn. Use `agent.inject()` to seed model-facing context. This is a notification, not a veto; disposal requested by a lifecycle owner is rechecked before the driver starts.
-
-```ts cordis-catalog
-/**
- * The session lifecycle began, once before the first turn. Use
- * `agent.inject()` to seed model-facing context. This is a notification, not
- * a veto; disposal requested by a lifecycle owner is rechecked before the
- * driver starts.
- * @param payload.agent - the agent whose session lifecycle began.
- * @param payload.source - why the session started (fresh startup, resume, …).
- * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
- */
-'agent/session-start'(this: Scoped, payload: { agent: Agent; source: SessionStartSource }): void
-```
-
-Types: [Scoped](scope.zh.md)
-
-Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
-
#### `agent/status` — emit
diff --git a/packages/api/session-controller/tests/agent.host.spec.ts b/packages/api/session-controller/tests/agent.host.spec.ts
index 586d7b4e7b..2f8ae62c9a 100644
--- a/packages/api/session-controller/tests/agent.host.spec.ts
+++ b/packages/api/session-controller/tests/agent.host.spec.ts
@@ -172,7 +172,7 @@ describe('ApiSession Agent lookup and recovery', () => {
it('projects live Agent contexts and maps missing cold identities through Typert lookup failures', async () => {
const { ctx } = await harness()
const live = agent(ctx, header('live'))
- ctx.agents.register(live)
+ await ctx.agents.register(live)
providePersistence(ctx, {
list: () => Promise.resolve([]),
inspect: vi.fn(),
@@ -193,7 +193,7 @@ describe('ApiSession Agent lookup and recovery', () => {
})
const winner = agent(ordinary.ctx, ordinaryMeta)
vi.spyOn(ordinary.ctx.agents, 'resume').mockImplementation(async () => {
- ordinary.ctx.agents.register(winner)
+ await ordinary.ctx.agents.register(winner)
throw new Error('raced publication')
})
await expect(ordinary.agents.resolveAgent(ordinaryMeta.id)).resolves.toEqual({ agent: winner })
@@ -326,7 +326,7 @@ describe('ApiSession create or adoption', () => {
const ordinaryMeta = header('create-race', cwd)
const winner = agent(ordinary.ctx, ordinaryMeta)
vi.spyOn(ordinary.ctx.agents, 'create').mockImplementation(async () => {
- ordinary.ctx.agents.register(winner)
+ await ordinary.ctx.agents.register(winner)
throw new Error('raced creation')
})
await expect(ordinary.agents.ensureSession(ordinaryMeta.id, cwd, false))
diff --git a/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts b/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts
index d7559bc047..79838ef7f0 100644
--- a/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts
+++ b/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts
@@ -82,7 +82,7 @@ async function commandHarness(
followup: vi.fn(),
cancel,
} as unknown as Agent
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
ctx.provide('workspaceRegistry', { get: () => undefined, list: () => [] } as never)
ctx.provide('agentDefaultModel', {
currentSelection: () => ({ provider: 'fixture', model: 'fixture-model' }),
diff --git a/packages/api/session-controller/tests/commands-upload-file.host.spec.ts b/packages/api/session-controller/tests/commands-upload-file.host.spec.ts
index a6073c5f2a..c5749285ad 100644
--- a/packages/api/session-controller/tests/commands-upload-file.host.spec.ts
+++ b/packages/api/session-controller/tests/commands-upload-file.host.spec.ts
@@ -29,7 +29,7 @@ async function uploadHarness(origin?: 'subagent'): Promise<{
saveFile: ReturnType
saveFileStream: ReturnType
saveImages: ReturnType
- disposeAgent: () => void
+ disposeAgent: () => Promise
uploadRoute: (request: Request) => Promise
}> {
const ctx = new Context()
@@ -52,7 +52,7 @@ async function uploadHarness(origin?: 'subagent'): Promise<{
cancel: vi.fn(),
} as unknown as Agent
;(agent as { ctx: Context }).ctx = createScope(ctx, agent).ctx
- const disposeAgent = ctx.agents.register(agent)
+ const disposeAgent = await ctx.agents.register(agent)
const saveFile = vi.fn((input: SaveFileAttachment): Promise => Promise.resolve({
attachmentId: AttachmentId(`sha256:${'cd'.repeat(32)}`),
name: input.name ?? 'file',
@@ -226,7 +226,7 @@ describe('Session file uploads', () => {
saveFile.mockReturnValueOnce(saved.promise)
const uploading = uploads.upload(agent, { data: 'AAAA', name: 'late.bin' }, new AbortController().signal)
await vi.waitFor(() => { expect(saveFile).toHaveBeenCalledOnce() })
- disposeAgent()
+ await disposeAgent()
saved.resolve({
attachmentId: AttachmentId(`sha256:${'ab'.repeat(32)}`), name: 'late.bin', bytes: 3,
})
@@ -235,9 +235,9 @@ describe('Session file uploads', () => {
it('resolves a cold ordinary Agent and releases the resolver registration', async () => {
const { ctx, uploads, agent, disposeAgent } = await uploadHarness()
- disposeAgent()
+ await disposeAgent()
const resolveAgent = vi.fn(async () => {
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
return agent
})
const disposeResolver = uploads.registerAgentResolver(resolveAgent)
@@ -258,7 +258,7 @@ describe('Session file uploads', () => {
it('rejects a cold upload when no Agent resolver is registered', async () => {
const { uploads, disposeAgent } = await uploadHarness()
- disposeAgent()
+ await disposeAgent()
await expect(uploads.uploadStream({
sessionId: SESSION,
data: (async function* (): AsyncIterable {})(),
@@ -366,7 +366,7 @@ describe('Session file uploads', () => {
{ type: 'image', mediaType: 'image/png', data: 'AAAA' },
]))
await vi.waitFor(() => { expect(saveImages).toHaveBeenCalledOnce() })
- disposeAgent()
+ await disposeAgent()
admitted.resolve([{
attachmentId: AttachmentId('admitted-image'), mediaType: 'image/png', bytes: 3, width: 1, height: 1,
}])
diff --git a/packages/api/session-controller/tests/control-jobs.host.spec.ts b/packages/api/session-controller/tests/control-jobs.host.spec.ts
index 4c88ba21df..46b3dff031 100644
--- a/packages/api/session-controller/tests/control-jobs.host.spec.ts
+++ b/packages/api/session-controller/tests/control-jobs.host.spec.ts
@@ -59,7 +59,7 @@ async function harness(withJobs: boolean): Promise<{
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
const control = new SessionControlController(ctx)
await new Promise(resolve => setTimeout(resolve, 0))
return { ctx, session, agent, control }
diff --git a/packages/api/session-controller/tests/controller.host.spec.ts b/packages/api/session-controller/tests/controller.host.spec.ts
index 8f303fa6d0..43d1394851 100644
--- a/packages/api/session-controller/tests/controller.host.spec.ts
+++ b/packages/api/session-controller/tests/controller.host.spec.ts
@@ -74,7 +74,7 @@ describe('SessionController facade', () => {
status: 'idle',
ctx,
} as Agent
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
const resolveUploadAgent = (id: SessionId): Promise => {
if (uploadResolver === undefined) throw new Error('file upload resolver was not registered')
return uploadResolver(id)
diff --git a/packages/api/session-controller/tests/session-cold.host.spec.ts b/packages/api/session-controller/tests/session-cold.host.spec.ts
index eaca25062f..ee9c8a9662 100644
--- a/packages/api/session-controller/tests/session-cold.host.spec.ts
+++ b/packages/api/session-controller/tests/session-cold.host.spec.ts
@@ -202,7 +202,7 @@ describe('attached updatedAt tracks human prompts', () => {
],
meta: { cwd: '/proj', createdAt: 500 },
})
- ctx.agents.register({ id: resumed.id, session: resumed, status: 'idle', ctx } as Agent)
+ await ctx.agents.register({ id: resumed.id, session: resumed, status: 'idle', ctx } as Agent)
const boundary = resumed.snapshotEvents().at(-1)
expect(boundary?.type).toBe('session/end-seed')
expect(boundary?.time).toBeGreaterThan(worked)
@@ -330,7 +330,7 @@ describe('Remote Agent and Session lookup policy', () => {
meta: { cwd: '/proj', parentSession: sid('session-parent'), origin: 'subagent' },
})
const liveAgent = { id: liveSession.id, session: liveSession, status: 'idle', ctx } as Agent
- ctx.agents.register(liveAgent)
+ await ctx.agents.register(liveAgent)
const resume = vi.spyOn(ctx.agents, 'resume')
const defaultAgentLookup = ctx.typert.lookups.get('agent')
const defaultSessionLookup = ctx.typert.lookups.get('session')
@@ -478,7 +478,7 @@ describe('subagent ownership fence', () => {
await ctx.plugin(AgentRegistry)
const parentSession = ctx.sessions.create(sid('session-parent'), { meta: { cwd: '/proj' } })
const parent = { id: parentSession.id, session: parentSession, status: 'idle', ctx } as Agent
- ctx.agents.register(parent)
+ await ctx.agents.register(parent)
const originSession = ctx.sessions.create(sid('session-origin-child'), {
meta: { cwd: '/proj', parentSession: parent.id, origin: 'subagent' },
@@ -493,7 +493,7 @@ describe('subagent ownership fence', () => {
cancel,
updateInbox,
} as unknown as Agent
- ctx.agents.register(originChild)
+ await ctx.agents.register(originChild)
const startingSession = ctx.sessions.create(sid('session-starting-child'), {
meta: { cwd: '/proj', parentSession: parent.id },
@@ -549,7 +549,7 @@ describe('subagent ownership fence', () => {
const agent = {
id: session.id, session, inbox: inboxFor(), status: 'idle', ctx, followup,
} as unknown as Agent
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
const response = await remote.prompt(promptRequest({
@@ -570,7 +570,7 @@ describe('subagent ownership fence', () => {
const agent = {
id: session.id, session, inbox: inboxFor(), status: 'idle', ctx, followup,
} as unknown as Agent
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
cwd: '/tmp',
@@ -686,7 +686,7 @@ describe('sessions.prompt synchronous rejection', () => {
const session = ctx.sessions.create(sid('session-empty-prompt'))
const followup = vi.fn()
const steer = vi.fn()
- ctx.agents.register({
+ await ctx.agents.register({
id: session.id,
session,
inbox: inboxFor(),
@@ -781,7 +781,7 @@ describe('sessions.prompt synchronous rejection', () => {
const session = ctx.sessions.create(sid('session-throwing'))
// A live structural stub whose delivery verbs throw synchronously, the
// shape a disposed loop presents at this gateway boundary.
- ctx.agents.register({
+ await ctx.agents.register({
id: session.id,
session,
inbox: inboxFor(),
@@ -820,7 +820,7 @@ describe('sessions.prompt synchronous rejection', () => {
// while the generic cold resume is in flight, so the resume collides.
const parentSession = ctx.sessions.create(sid('race-parent'), { meta: { cwd: '/proj' } })
const parent = { id: parentSession.id, session: parentSession, status: 'idle', ctx } as Agent
- ctx.agents.register(parent)
+ await ctx.agents.register(parent)
const childSession = ctx.sessions.create(sessionId, {
meta: { cwd: '/proj', parentSession: parent.id, origin: 'subagent' },
})
@@ -828,7 +828,7 @@ describe('sessions.prompt synchronous rejection', () => {
vi.spyOn(ctx.agents, 'resume').mockImplementationOnce(async () => {
// The parent's `enter()` wins the identity between the pre-resume
// re-check and publication; the generic resume then collides.
- ctx.agents.register(child)
+ await ctx.agents.register(child)
throw new Error('session id already published')
})
const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
diff --git a/packages/api/session-controller/tests/session-fork.host.spec.ts b/packages/api/session-controller/tests/session-fork.host.spec.ts
index bc076f2868..346ec758a1 100644
--- a/packages/api/session-controller/tests/session-fork.host.spec.ts
+++ b/packages/api/session-controller/tests/session-fork.host.spec.ts
@@ -42,7 +42,7 @@ async function composed(workspaces: readonly Workspace[] = []): Promise
const agentCtx = ownerCtx
Object.assign(agent, { id: session.id, session, status: 'idle', ctx: agentCtx })
await options.setup?.(agentCtx, agent)
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
return { agent, dispose: () => Promise.resolve() }
},
resume: () => Promise.reject(new Error('fork test sources are live')),
@@ -53,13 +53,13 @@ async function composed(workspaces: readonly Workspace[] = []): Promise
/** Tail turn appended after the completed ones: left open, or closed as aborted (a stopped turn). */
type Tail = 'none' | 'open' | 'aborted'
-function liveAgent(
+async function liveAgent(
ctx: Context,
id: string,
turns: number,
tail: Tail = 'none',
lineage: { parentSession?: SessionId; origin?: 'subagent' } = {},
-): Session {
+): Promise {
const session = ctx.sessions.create(sid(id), { meta: { cwd: '/proj', ...lineage } })
for (let turn = 1; turn <= turns; turn++) {
session.append('turn/start', { turn })
@@ -80,7 +80,7 @@ function liveAgent(
reason: { kind: 'aborted', reason: { kind: 'user' } },
})
}
- ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
+ await ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
return session
}
@@ -139,7 +139,7 @@ describe('sessions.fork', () => {
it('cuts at the anchored completed turn and records lineage and cwd', async () => {
const ctx = await composed()
- const source = liveAgent(ctx, 'session-source', 2)
+ const source = await liveAgent(ctx, 'session-source', 2)
const response = await remote(ctx).fork(request({ sessionId: source.id, atSeq: 1 }))
expect(response.ok ? null : response.error).toBeNull()
if (!response.ok) return
@@ -161,13 +161,13 @@ describe('sessions.fork', () => {
attachSession,
} as unknown as Workspace
const ctx = await composed([workspace])
- const owner = liveAgent(ctx, 'session-owner', 1)
+ const owner = await liveAgent(ctx, 'session-owner', 1)
accounted.push(owner.id)
- const child = liveAgent(ctx, 'session-child', 1, 'none', {
+ const child = await liveAgent(ctx, 'session-child', 1, 'none', {
parentSession: owner.id,
origin: 'subagent',
})
- const grandchild = liveAgent(ctx, 'session-grandchild', 1, 'none', {
+ const grandchild = await liveAgent(ctx, 'session-grandchild', 1, 'none', {
parentSession: child.id,
origin: 'subagent',
})
@@ -253,7 +253,7 @@ describe('sessions.fork', () => {
it('uses the last completed turn only for omitted and past-end anchors', async () => {
const ctx = await composed()
- const source = liveAgent(ctx, 'session-tail', 2, 'open')
+ const source = await liveAgent(ctx, 'session-tail', 2, 'open')
const proxy = remote(ctx)
const expectedTypes = [
'turn/start', 'user/message', 'turn/end',
@@ -289,7 +289,7 @@ describe('sessions.fork', () => {
it('cuts through an aborted turn: stopped is closed, not open', async () => {
const ctx = await composed()
- const source = liveAgent(ctx, 'session-aborted', 1, 'aborted')
+ const source = await liveAgent(ctx, 'session-aborted', 1, 'aborted')
// What a stopped message's fork button anchors on: the frozen node sits
// one event before its turn/end, floored client-side to that event's seq.
const anchor = (source.snapshotEvents().at(-1)?.seq ?? 0) - 1
@@ -306,7 +306,7 @@ describe('sessions.fork', () => {
it('rejects an in-log anchor whose turn is still open', async () => {
const ctx = await composed()
- const source = liveAgent(ctx, 'session-open', 1, 'open')
+ const source = await liveAgent(ctx, 'session-open', 1, 'open')
const anchor = source.snapshotEvents().at(-1)?.seq ?? 0
const response = await remote(ctx).fork(request({ sessionId: source.id, atSeq: anchor }))
expect(response).toMatchObject({
@@ -319,7 +319,7 @@ describe('sessions.fork', () => {
it('inherits model selection through the completed turn and excludes later changes', async () => {
const ctx = await composed()
- const source = liveAgent(ctx, 'session-routed', 0)
+ const source = await liveAgent(ctx, 'session-routed', 0)
source.append('turn/start', { turn: 1 })
source.append('request/header', {
header: {
diff --git a/packages/api/session-controller/tests/session-list-blank.host.spec.ts b/packages/api/session-controller/tests/session-list-blank.host.spec.ts
index cdfbbda39b..fce236d3e3 100644
--- a/packages/api/session-controller/tests/session-list-blank.host.spec.ts
+++ b/packages/api/session-controller/tests/session-list-blank.host.spec.ts
@@ -19,15 +19,15 @@ import type {} from '@deepseek-ai/dsh-permission-presets'
import type {} from '@deepseek-ai/dsh-sandbox-policy'
import { createSessionTestRemote, type TestSessionRemote } from './test-remote.ts'
-async function harness(): Promise<{ ctx: Context; remote: TestSessionRemote; attach: (session: Session) => void }> {
+async function harness(): Promise<{ ctx: Context; remote: TestSessionRemote; attach: (session: Session) => Promise }> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
return {
ctx,
remote: createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }),
- attach: (session) => {
- ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
+ attach: async (session) => {
+ await ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
},
}
}
@@ -57,7 +57,7 @@ describe('summary blank = conversation not started', () => {
it('standalone events (command lifecycle, plan/mode, title) keep the session blank', async () => {
const { ctx, remote, attach } = await harness()
const session = ctx.sessions.create()
- attach(session)
+ await attach(session)
expect(await listBlank(remote, session.id)).toBe(true)
appendStandalone(session)
expect(await listBlank(remote, session.id)).toBe(true)
@@ -66,7 +66,7 @@ describe('summary blank = conversation not started', () => {
it('the first turn clears blank', async () => {
const { ctx, remote, attach } = await harness()
const session = ctx.sessions.create()
- attach(session)
+ await attach(session)
appendStandalone(session)
session.append('turn/start', { turn: 0 })
expect(await listBlank(remote, session.id)).toBe(false)
diff --git a/packages/api/session-controller/tests/session-models.host.spec.ts b/packages/api/session-controller/tests/session-models.host.spec.ts
index fe3ef628b5..bb9e77b4e2 100644
--- a/packages/api/session-controller/tests/session-models.host.spec.ts
+++ b/packages/api/session-controller/tests/session-models.host.spec.ts
@@ -132,7 +132,7 @@ async function harness(logged?: {
ctx,
inbox: { nextTurn: [], nextStep: [] },
} as unknown as Agent
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
return { ctx, agent, sessionId: session.id }
}
diff --git a/packages/api/session-controller/tests/session-presets.host.spec.ts b/packages/api/session-controller/tests/session-presets.host.spec.ts
index d917ccb777..4fc3fd4f25 100644
--- a/packages/api/session-controller/tests/session-presets.host.spec.ts
+++ b/packages/api/session-controller/tests/session-presets.host.spec.ts
@@ -68,8 +68,8 @@ async function harness(presets?: readonly string[]) {
const agent = stubAgent(session)
;(agent as { ctx?: Context }).ctx = ctx
await options.setup?.(ctx, agent)
- const unregister = ctx.agents.register(agent)
- return { agent, dispose: () => { unregister(); return Promise.resolve() } }
+ const unregister = await ctx.agents.register(agent)
+ return { agent, dispose: async () => { await unregister() } }
},
async resume() {
throw new Error('test harness has no persisted sessions')
diff --git a/packages/api/session-controller/tests/session-rename.host.spec.ts b/packages/api/session-controller/tests/session-rename.host.spec.ts
index 60987608b9..0e4754d8b0 100644
--- a/packages/api/session-controller/tests/session-rename.host.spec.ts
+++ b/packages/api/session-controller/tests/session-rename.host.spec.ts
@@ -36,13 +36,13 @@ async function composed(withTitles = true): Promise {
// forwarded seed/meta (the store validates the balanced prefix) and
// registers an idle agent stub over it.
ctx.agents.setFactory({
- createAgent: (ownerCtx: Context, options: CreateAgentOptions): Promise => {
+ createAgent: async (ownerCtx: Context, options: CreateAgentOptions): Promise => {
const session = ctx.sessions.create(options.sessionId, {
...options.seed === undefined ? {} : { seed: [...options.seed] },
...options.meta === undefined ? {} : { meta: options.meta },
})
const agent = { id: session.id, session, status: 'idle', ctx: ownerCtx } as Agent
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
return Promise.resolve({ agent, dispose: () => Promise.resolve() })
},
resume: () => Promise.reject(new Error('resume must not run: every source is attached')),
@@ -51,7 +51,7 @@ async function composed(withTitles = true): Promise {
}
/** Register one live agent whose log holds `turns` completed turns. */
-function liveAgent(ctx: Context, id: string, turns: number): Session {
+async function liveAgent(ctx: Context, id: string, turns: number): Promise {
const session = ctx.sessions.create(sid(id), { meta: { cwd: '/proj' } })
for (let turn = 1; turn <= turns; turn++) {
session.append('turn/start', { turn })
@@ -61,7 +61,7 @@ function liveAgent(ctx: Context, id: string, turns: number): Session {
}), { surfaceOp: 'append' })
session.append('turn/end', { turn, reason: { kind: 'completed' } })
}
- ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
+ await ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
return session
}
@@ -70,7 +70,7 @@ const remote = (ctx: Context) => createSessionTestRemote(ctx, { defaultModelSele
describe('sessions.rename', () => {
it('accepts through the composed title service: normalized user-source event, echoed seq', async () => {
const ctx = await composed()
- const source = liveAgent(ctx, 'session-rename', 1)
+ const source = await liveAgent(ctx, 'session-rename', 1)
const renamed = await remote(ctx).rename(request({ sessionId: source.id, title: ' new name ' }))
expect(renamed.ok).toBe(true)
@@ -83,7 +83,7 @@ describe('sessions.rename', () => {
it('maps only an empty-normalizing title to title-invalid, with a presentable message', async () => {
const ctx = await composed()
- const source = liveAgent(ctx, 'session-rename-bad', 1)
+ const source = await liveAgent(ctx, 'session-rename-bad', 1)
// U+200B passes a client-side trim gate but normalizes to empty host-side.
const response = await remote(ctx).rename(request({ sessionId: source.id, title: ' ' }))
@@ -104,8 +104,8 @@ describe('sessions.rename', () => {
// title service's liveness check throws a plain Error, which must not
// read as the user's fault.
const foreign = await composed(false)
- const stale = liveAgent(foreign, 'session-rename-stale', 1)
- ctx.agents.register({ id: stale.id, session: stale, status: 'idle', ctx } as Agent)
+ const stale = await liveAgent(foreign, 'session-rename-stale', 1)
+ await ctx.agents.register({ id: stale.id, session: stale, status: 'idle', ctx } as Agent)
const response = await remote(ctx).rename(request({ sessionId: stale.id, title: 'name' }))
expect(response.ok).toBe(false)
@@ -114,7 +114,7 @@ describe('sessions.rename', () => {
it('answers internal when the composition mounts no session-title service', async () => {
const ctx = await composed(false)
- const source = liveAgent(ctx, 'session-no-titles', 1)
+ const source = await liveAgent(ctx, 'session-no-titles', 1)
const response = await remote(ctx).rename(request({ sessionId: source.id, title: 'name' }))
expect(response.ok).toBe(false)
diff --git a/packages/api/session-controller/tests/session-skills.host.spec.ts b/packages/api/session-controller/tests/session-skills.host.spec.ts
index c09e08ac1b..a7124c22c3 100644
--- a/packages/api/session-controller/tests/session-skills.host.spec.ts
+++ b/packages/api/session-controller/tests/session-skills.host.spec.ts
@@ -90,7 +90,7 @@ describe('SessionSkillCatalog', () => {
const sessionId = SessionId('live-skills')
const session = ctx.sessions.create(sessionId, { meta: { cwd: '/live/project' } })
const agent = { id: sessionId, session, status: 'idle', ctx } as Agent
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
ctx.provide('sessionQuery', {
observeSession: () => Promise.resolve(observation(sessionId, { cwd: '/live/project' })),
} as never)
diff --git a/packages/bundle/headless/tests/headless.spec.ts b/packages/bundle/headless/tests/headless.spec.ts
index 46496285ed..96f646d264 100644
--- a/packages/bundle/headless/tests/headless.spec.ts
+++ b/packages/bundle/headless/tests/headless.spec.ts
@@ -165,7 +165,7 @@ async function bench(script: Script, options: BenchOptions = {}): Promise<{
},
}
await createOptions.setup?.(ownerCtx, agent)
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
return agent
}
diff --git a/packages/context/agent-instructions/tests/agent-instructions.spec.ts b/packages/context/agent-instructions/tests/agent-instructions.spec.ts
index 877c4f4058..e40b6f317b 100644
--- a/packages/context/agent-instructions/tests/agent-instructions.spec.ts
+++ b/packages/context/agent-instructions/tests/agent-instructions.spec.ts
@@ -1416,7 +1416,7 @@ describe('workspace context request injection', () => {
await fiber.dispose()
await mountAgentInstructionsPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const resumed = await stubAgent(root, original.session.snapshotEvents())
- agentEvents(ctx, resumed).emit('agent/session-start', { source: 'resume' })
+ await agentEvents(ctx, resumed).serial('agent/created', { source: 'resume' })
const claimed = claimInbox(resumed, 'next-step')
const decision = await agentEvents(ctx, resumed).waterfall(
'agent/pre-step',
@@ -1462,7 +1462,7 @@ describe('workspace context request injection', () => {
await fiber.dispose()
await mountAgentInstructionsPlugin(ctx, { dshHome: home, maxBytes: 65536 })
const resumed = await stubAgent(root, original.session.snapshotEvents())
- agentEvents(ctx, resumed).emit('agent/session-start', { source: 'resume' })
+ await agentEvents(ctx, resumed).serial('agent/created', { source: 'resume' })
const staleClaim = claimInbox(resumed, 'next-step')
const staleDecision = await agentEvents(ctx, resumed).waterfall(
'agent/pre-step',
@@ -1515,7 +1515,7 @@ describe('workspace context request injection', () => {
if (provideFs) await resumedCtx.plugin(LocalFileSystem, { cwd: '/' })
await mountAgentInstructionsPlugin(resumedCtx, { dshHome: home, maxBytes })
const resumed = await stubAgent(root, original.session.snapshotEvents())
- agentEvents(resumedCtx, resumed).emit('agent/session-start', { source: 'resume' })
+ await agentEvents(resumedCtx, resumed).serial('agent/created', { source: 'resume' })
const claimed = claimInbox(resumed, 'next-step')
const decision = await agentEvents(resumedCtx, resumed).waterfall(
'agent/pre-step',
@@ -1824,7 +1824,7 @@ describe('workspace context request injection', () => {
const resumed = await stubAgent(root, original.session.snapshotEvents())
// Resume announces its lifecycle start before the first step.
- agentEvents(ctx, resumed).emit('agent/session-start', { source: 'resume' })
+ await agentEvents(ctx, resumed).serial('agent/created', { source: 'resume' })
await composeBaselinePrefix(ctx, resumed)
const baselines = baselineEvents(resumed)
diff --git a/packages/context/file-reference-local/README.i18n.yaml b/packages/context/file-reference-local/README.i18n.yaml
index bb57f2018c..1395353c93 100644
--- a/packages/context/file-reference-local/README.i18n.yaml
+++ b/packages/context/file-reference-local/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/context/file-reference-local/README.md
-README.md: ec1a681f6fa1c5fb33a7705d2c1faf5232690b23
-README.zh.md: c80a5546d8bb9dce1b0d3862f3169deffa7d4fdf
+README.md: efeebdbd3e113597cc93dfe4d6c379e16b324d93
+README.zh.md: f286f92d6e6ec76afc9919db9f81c73e4ace8d04
diff --git a/packages/context/file-reference-local/README.md b/packages/context/file-reference-local/README.md
index ec1a681f6f..efeebdbd3e 100644
--- a/packages/context/file-reference-local/README.md
+++ b/packages/context/file-reference-local/README.md
@@ -63,7 +63,7 @@ This section explains the design of the provider; the observable behavior is cov
### Design concept
-The provider maintains one reusable `WorkspaceFileSearch` per agent, rooted at that session's `cwd`. Directory-scoped queries (`a/b/...`) list live directory state, while bare fuzzy queries share one bounded recursive traversal. Only a workspace's first bare query waits for that traversal; a `tool/result` event marks the settled entries stale, and the next bare query serves them while the replacement builds. The model guidance is a per-agent prompt section contributed only while the addressed agent has a `read` tool; agent disposal releases both the index and the prompt fiber.
+The provider maintains one reusable `WorkspaceFileSearch` per agent, rooted at that session's `cwd`. Directory-scoped queries (`a/b/...`) list live directory state, while bare fuzzy queries share one bounded recursive traversal. Only a workspace's first bare query waits for that traversal; a `tool/result` event marks the settled entries stale, and the next bare query serves them while the replacement builds. The model guidance is a per-agent prompt section contributed only while the addressed agent has a `read` tool; creation awaits prompt installation and rolls back on failure; agent disposal releases both the index and the prompt fiber.
### Source map
diff --git a/packages/context/file-reference-local/README.zh.md b/packages/context/file-reference-local/README.zh.md
index c80a5546d8..f286f92d6e 100644
--- a/packages/context/file-reference-local/README.zh.md
+++ b/packages/context/file-reference-local/README.zh.md
@@ -63,7 +63,7 @@ agent(智能体)及宿主 UI 可以用各 agent 本地工作区中经过排
### 设计理念
-提供方为每个 agent 维护一个可复用的 `WorkspaceFileSearch`,以该会话的 `cwd` 为根。目录范围查询(`a/b/...`)列出实时目录状态,裸模糊查询共享一次有界递归遍历。每个工作区仅首次裸查询会等待该遍历;`tool/result` 事件把已完成的条目标记为陈旧,下一次裸查询在替代品构建期间继续由它作答。模型指引是按 agent 的提示词段,仅在指定 agent 拥有 `read` 工具时贡献;agent dispose(资源释放)时会同时释放索引与提示词 fiber。
+提供方为每个 agent 维护一个可复用的 `WorkspaceFileSearch`,以该会话的 `cwd` 为根。目录范围查询(`a/b/...`)列出实时目录状态,裸模糊查询共享一次有界递归遍历。每个工作区仅首次裸查询会等待该遍历;`tool/result` 事件把已完成的条目标记为陈旧,下一次裸查询在替代品构建期间继续由它作答。模型指引是按 agent 的提示词段,仅在指定 agent 拥有 `read` 工具时贡献;创建会等待提示词安装,安装失败会回滚 agent;agent dispose(资源释放)时会同时释放索引与提示词 fiber。
### 源码地图
diff --git a/packages/context/file-reference-local/src/index.ts b/packages/context/file-reference-local/src/index.ts
index 34e2b75674..0eca490ef8 100644
--- a/packages/context/file-reference-local/src/index.ts
+++ b/packages/context/file-reference-local/src/index.ts
@@ -63,8 +63,9 @@ export class LocalFileReferenceService extends FileReferenceService {
}
validateConfig(this.config)
- const installPrompt = (agent: Agent): void => {
- if (this.promptFibers.has(agent)) return
+ const installPrompt = (agent: Agent): ReturnType => {
+ const existing = this.promptFibers.get(agent)
+ if (existing !== undefined) return existing
const fiber = agent.ctx.inject(['systemPrompt', 'tools'], (scope) => {
scope.systemPrompt.section({
name: 'context:file-reference',
@@ -73,6 +74,7 @@ export class LocalFileReferenceService extends FileReferenceService {
})
})
this.promptFibers.set(agent, fiber)
+ return fiber
}
const disposePrompt = (agent: Agent): void => {
const fiber = this.promptFibers.get(agent)
@@ -87,7 +89,7 @@ export class LocalFileReferenceService extends FileReferenceService {
})
}
for (const agent of ctx.agents.list()) installPrompt(agent)
- ctx.on('agent/created', ({ agent }) => { installPrompt(agent) })
+ ctx.on('agent/created', async ({ agent }) => { await installPrompt(agent) })
ctx.on('agent/disposed', ({ agent }) => {
this.searches.get(agent)?.dispose()
this.searches.delete(agent)
diff --git a/packages/context/file-reference-local/tests/service.spec.ts b/packages/context/file-reference-local/tests/service.spec.ts
index 72af52d8b2..d41e8a8253 100644
--- a/packages/context/file-reference-local/tests/service.spec.ts
+++ b/packages/context/file-reference-local/tests/service.spec.ts
@@ -31,7 +31,7 @@ async function stubAgent(
ctx: Context,
id = 'file-reference-agent',
includeCwd = true,
-): Promise<{ agent: Agent; dispose: () => void }> {
+): Promise<{ agent: Agent; dispose: () => Promise }> {
const root = await mkdtemp(join(tmpdir(), 'dsh-file-reference-service-'))
roots.push(root)
await writeFile(join(root, 'README.md'), 'readme')
@@ -51,10 +51,29 @@ async function stubAgent(
cancel() {},
whenIdle: () => Promise.resolve(),
} as unknown as Agent
- return { agent, dispose: ctx.agents.register(agent) }
+ return { agent, dispose: await ctx.agents.register(agent) }
}
describe('LocalFileReferenceService', () => {
+ it('defers guidance until optional prompt and tool services become available', async () => {
+ const ctx = new Context()
+ await ctx.plugin(SessionStore)
+ await ctx.plugin(AgentRegistry)
+ await ctx.plugin(LocalFileReferenceService)
+ try {
+ const { agent } = await stubAgent(ctx, 'deferred-prompt')
+ expect(ctx.agents.get(agent.id)).toBe(agent)
+ await ctx.plugin(SystemPrompt, { personaPrefix: '' })
+ await ctx.plugin(ToolRegistry)
+ ctx.tools.register(defineContentToolFixture({
+ name: 'read', description: 'read a file', parameters: {}, execute: () => Promise.resolve([]),
+ }))
+ expect(renderPrompt(await ctx.systemPrompt.assemble())).toContain(FILE_REFERENCE_PROMPT)
+ } finally {
+ await ctx.fiber.dispose()
+ }
+ })
+
it('serves the addressed workspace and installs read-tool guidance for existing agents', async () => {
const ctx = await harness()
const { agent } = await stubAgent(ctx)
@@ -95,7 +114,7 @@ describe('LocalFileReferenceService', () => {
ctx.emit('session/event', orphan, { type: 'tool/result' } as never)
expect(invalidate).toHaveBeenCalledOnce()
- dispose()
+ await dispose()
expect(close).toHaveBeenCalledOnce()
ctx.emit('agent/disposed', { agent })
})
@@ -131,7 +150,7 @@ describe('LocalFileReferenceService', () => {
const fiber = ctx.plugin(LocalFileReferenceService)
await fiber
const { agent } = await stubAgent(ctx, 'cwd-fallback', false)
- ctx.emit('agent/created', { agent })
+ await ctx.serial('agent/created', { agent, source: 'startup' })
const list = vi.spyOn(WorkspaceFileSearch.prototype, 'list').mockResolvedValue([])
await expect(ctx.fileReferences.list(agent, '', new AbortController().signal)).resolves.toEqual([])
await expect(ctx.fileReferences.list(agent, 'src', new AbortController().signal)).resolves.toEqual([])
@@ -151,8 +170,8 @@ describe('LocalFileReferenceService', () => {
const first = await stubAgent(ctx, 'cleanup-one')
const second = await stubAgent(ctx, 'cleanup-two')
expect(inject).toHaveBeenCalledTimes(2)
- first.dispose()
- second.dispose()
+ await first.dispose()
+ await second.dispose()
await vi.waitFor(() => {
expect(warn).toHaveBeenCalledWith('file-reference-local: prompt cleanup failed: error cleanup')
expect(warn).toHaveBeenCalledWith('file-reference-local: prompt cleanup failed: string cleanup')
diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml
index f4879011cc..a130d19558 100644
--- a/packages/core/agent-loop/README.i18n.yaml
+++ b/packages/core/agent-loop/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/agent-loop/README.md
-README.md: d494f3f6f7c68440d59ec2c1aa8fad2eae0bcda1
-README.zh.md: bb3ca764a688cc01f61aa9b90667312f7d765fb5
+README.md: e53ccb4658ee857f7798ee18343752416b93a890
+README.zh.md: 3974bc56960d9f5c39082affc52937fc9f8b1946
diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md
index d494f3f6f7..e53ccb4658 100644
--- a/packages/core/agent-loop/README.md
+++ b/packages/core/agent-loop/README.md
@@ -108,7 +108,7 @@ The loop deep-freezes each derived message identity on its first request and reu
### Creation and teardown
-Creation is one rollback-covered transaction: construct a private session, concrete agent, and scoped context; await optional setup with the context and Agent passed separately; enter both registries; announce `session/created` then `agent/created`; emit `agent/session-start`; only then start the driver. A caller creating a runtime child sets `options.parentAgent`; the caller Context separately owns the transaction and live handle. A setup throw, commit failure, or owner disposal rolls the transaction back without publishing either id. Teardown runs stop-and-drain, closes the session's write path, unwinds the scope, detaches the agent, then detaches the session, and every detach is bound to the exact entered object so a stale disposer cannot remove a later same-id replacement.
+Creation is one rollback-covered transaction: construct a private session, concrete agent, and scoped context; await optional setup; enter both registries; announce `session/created`; await serial `agent/created` listeners; then release queued input. A caller creating a runtime child sets `options.parentAgent`; the caller Context separately owns the transaction and live handle. Setup, commit, or listener failure and owner disposal roll back the prepared resources. Announcements already delivered remain observable and receive matching disposal notifications. Teardown stops and drains the driver, unwinds the scope, closes the session's write path, detaches the agent, then detaches the session. Every detach binds the exact entered object, so a stale disposer cannot remove a later same-id replacement.
### Persistence integration
diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md
index bb3ca764a6..3974bc5696 100644
--- a/packages/core/agent-loop/README.zh.md
+++ b/packages/core/agent-loop/README.zh.md
@@ -108,7 +108,7 @@ const handle = await ctx.agents.create({
### 创建与拆除
-创建是同一个受回滚保护的事务:构造私有会话、具象 agent 与带作用域上下文;等待分别传入上下文与 Agent 的可选 setup;进入两个注册表;依次宣告 `session/created` 与 `agent/created`;发出 `agent/session-start`;此后才启动驱动器。创建运行时子 Agent 的调用方设置 `options.parentAgent`;调用方 Context 则单独拥有事务和存活句柄。Setup 抛出、commit 失败或所有者 dispose 都会回滚事务而不发布任一 id。Teardown 顺序是停止并排空、关闭会话的写路径、撤销作用域、detach agent、再 detach 会话,且每次 detach 都绑定到确切进入的对象,因此陈旧 disposer 无法移除之后出现的同 id 替代项。
+创建是同一个受回滚保护的事务:构造私有会话、具象 agent 与带作用域上下文;等待可选 setup;进入两个注册表;宣告 `session/created`;等待串行 `agent/created` 监听器;随后释放已排队输入。创建运行时子 Agent 的调用方设置 `options.parentAgent`;调用方 Context 则单独拥有事务和存活句柄。Setup、commit、监听器失败或所有者 dispose 都会回滚已准备的资源。已送达的宣告仍可被观察,并有配对的销毁通知。Teardown 停止并排空驱动器、撤销作用域、关闭会话写路径、detach agent,再 detach 会话。每次 detach 都绑定到确切进入的对象,因此陈旧 disposer 无法移除之后出现的同 id 替代项。
### 持久化集成
diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts
index e79fcd5e40..4ea7593551 100644
--- a/packages/core/agent-loop/src/agent.ts
+++ b/packages/core/agent-loop/src/agent.ts
@@ -170,7 +170,8 @@ export class ReactLoopAgent implements Agent {
return await job(maintenance.abort.signal)
} finally {
this.setPhase({ kind: 'idle', lastTurn: maintenance.lastTurn })
- if (maintenance.wakeRequested && this.inbox.hasPending) this.wakeDriver()
+ const cause = maintenance.abort.signal.reason as AgentCancelCause | undefined
+ if (cause?.kind !== 'disposed' && maintenance.wakeRequested && this.inbox.hasPending) this.wakeDriver()
done.resolve()
}
})()
diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts
index 6c30f04135..aa70ca2a34 100644
--- a/packages/core/agent-loop/src/index.ts
+++ b/packages/core/agent-loop/src/index.ts
@@ -10,7 +10,6 @@ import { randomUUID } from 'node:crypto'
import z from '@deepseek-ai/schemastery'
import { z as zod } from 'zod'
import { brandString } from '@deepseek-ai/dsh-brand'
-import { emitAgentEvent } from '@deepseek-ai/dsh-agent'
import type {
Agent,
AgentFactory,
@@ -214,8 +213,8 @@ interface PreparedAgent {
agent: ReactLoopAgent
/** Aborts when the factory unloads, the caller cancels, or teardown begins — ends any setup await. */
signal: AbortSignal
- /** Enter registries, announce, notify session-start, and start the machine. */
- publish(source: SessionStartSource): AgentHandle
+ /** Enter both registries and await creation listeners. */
+ publish(source: SessionStartSource): Promise
/** Reverse teardown: stop the machine, unregister, unwind the scope. Memoized. */
dispose(): Promise
}
@@ -569,6 +568,7 @@ export class AgentLoop extends Service implements AgentFactory {
let detachSession: (() => void) | undefined
let detachAgent: (() => void) | undefined
let disposing: Promise | undefined
+ let publication: ReturnType> | undefined
const machineReady = Promise.withResolvers()
// Reverse teardown, memoized so every racing owner awaits one quiescence:
// stop the machine, drain and close the session's write path, leave the
@@ -582,6 +582,8 @@ export class AgentLoop extends Service implements AgentFactory {
// disposal rejects with what failed so every racing owner observes it.
const failures: unknown[] = []
try {
+ // Creation listeners retain the session and scope through their awaits.
+ if (publication !== undefined) await publication.promise
// Disposal IS a disposed-cause cancel followed by quiescence. New work
// sent after this point is the sender's bug — the registries are about
// to drop the agent, so nothing should still hold it.
@@ -659,22 +661,23 @@ export class AgentLoop extends Service implements AgentFactory {
return {
agent,
signal: abort.signal,
- publish: (source) => {
- assertLive()
- detachSession = agent.ctx.sessions.enter(session)
- // The mounted backend routes announced live events into the active
- // write handle by session id; the loop only owns the handle itself.
- detachAgent = loopCtx.agents.enter(agent, parentAgent)
- agent.ctx.sessions.announce(session)
- assertLive()
- loopCtx.agents.announce(agent)
- assertLive()
- // A synchronous announce/session-start listener may have started
- // teardown; the machine is already live (delivery works from the
- // session-start extension point), so only the liveness recheck is owed.
- emitAgentEvent(loopCtx, agent, 'agent/session-start', { source })
- assertLive()
- return { agent, dispose }
+ publish: async (source) => {
+ publication = Promise.withResolvers()
+ try {
+ assertLive()
+ detachSession = agent.ctx.sessions.enter(session)
+ // The mounted backend routes announced live events into the active
+ // write handle by session id; the loop only owns the handle itself.
+ detachAgent = loopCtx.agents.enter(agent, parentAgent)
+ agent.ctx.sessions.announce(session)
+ assertLive()
+ await loopCtx.agents.announce(agent, source, abort.signal)
+ assertLive()
+ return { agent, dispose }
+ } finally {
+ publication.resolve()
+ publication = undefined
+ }
},
dispose,
}
@@ -706,14 +709,10 @@ export class AgentLoop extends Service implements AgentFactory {
await stored?.handle.close().catch(() => {})
throw error
}
- try {
+ return (await this.initializeAgent(prepared, async () => {
await this.appendUnstoredSuffix(stored, preparation.session)
- return prepared.publish('startup').agent
- } catch (error: unknown) {
- // Rollback swallows a disposal rejection: the setup failure is primary.
- void prepared.dispose().catch(() => {})
- throw error
- }
+ return await prepared.publish('startup')
+ })).agent
}
/**
@@ -822,11 +821,25 @@ export class AgentLoop extends Service implements AgentFactory {
await stored?.handle.close().catch(() => {})
throw error
}
- try {
+ return await this.initializeAgent(prepared, async () => {
const setupCommit = await raceAbort(setup?.(prepared.agent.ctx, prepared.agent), prepared.signal, id)
setupCommit?.commit()
await this.appendUnstoredSuffix(stored, session)
- return prepared.publish(source)
+ return await prepared.publish(source)
+ })
+ }
+
+ private async initializeAgent(prepared: PreparedAgent, initialize: () => Promise): Promise {
+ try {
+ return await prepared.agent.runMaintenance(async () => {
+ try {
+ return await initialize()
+ } catch (error: unknown) {
+ // Teardown owns inbox cleanup and may already have removed its projection.
+ prepared.agent.cancel({ kind: 'disposed' }, { keepInbox: true })
+ throw error
+ }
+ })
} catch (error: unknown) {
// Rollback swallows a disposal rejection (a failing final handle close):
// the setup failure is the primary error the caller must see.
diff --git a/packages/core/agent-loop/tests/fixtures/serial-created.mjs b/packages/core/agent-loop/tests/fixtures/serial-created.mjs
new file mode 100644
index 0000000000..3ddc41b52b
--- /dev/null
+++ b/packages/core/agent-loop/tests/fixtures/serial-created.mjs
@@ -0,0 +1,24 @@
+/** Awaited creation context shared by TypeScript and Python SDK snapshots. */
+import { randomUUID } from 'node:crypto'
+import { setImmediate } from 'node:timers/promises'
+
+export const name = 'serial-created-fixture'
+export const inject = ['agents']
+
+/** Install ordered creation listeners whose context must precede the first turn. */
+export function apply(ctx) {
+ const ready = new WeakSet()
+ ctx.on('agent/created', async ({ agent }) => {
+ await setImmediate()
+ agent.inject({
+ id: randomUUID(),
+ role: 'user',
+ content: [{ type: 'text', text: 'Serial agent creation completed.' }],
+ source: { kind: 'plugin', plugin: name },
+ })
+ ready.add(agent)
+ })
+ ctx.on('agent/created', ({ agent }) => {
+ if (!ready.has(agent)) throw new Error('creation listeners ran out of order')
+ })
+}
diff --git a/packages/core/agent-loop/tests/interception.spec.ts b/packages/core/agent-loop/tests/interception.spec.ts
index b50fb45942..27302c53d8 100644
--- a/packages/core/agent-loop/tests/interception.spec.ts
+++ b/packages/core/agent-loop/tests/interception.spec.ts
@@ -21,7 +21,7 @@ import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
/**
* The interception points introduced by the hooks taxonomy: `agent/pre-step`,
- * `agent/session-start`, `agent/turn-stopping`, and the
+ * `agent/created`, `agent/turn-stopping`, and the
* `tools/pre-execute` / `tools/post-execute`
* split with `additionalContexts` buffering. These verify the canonical event
* API a hook bridge (or a native plugin) programs against, WITHOUT any
@@ -557,30 +557,28 @@ describe('agent/pre-step', () => {
})
})
-describe('agent/session-start', () => {
+describe('agent/created', () => {
it('fires once with source "startup" for a fresh create, before the first turn', async () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(adapter)
const sources: SessionStartSource[] = []
- ctx.on('agent/session-start', ({ source }) => void sources.push(source))
+ ctx.on('agent/created', ({ source }) => void sources.push(source))
const agent = await ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
- // fires synchronously at create, before any turn
expect(sources).toEqual(['startup'])
expect(events(agent).some(e => e.type === 'turn/start')).toBe(false)
send(agent, 'go')
await waitForIdle(ctx, agent)
- // still only one session-start
expect(sources).toEqual(['startup'])
})
- it('a session-start listener can inject context the first request sees', async () => {
+ it('a creation listener can inject context the first request sees', async () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(adapter)
- ctx.on('agent/session-start', ({ agent }) => {
+ ctx.on('agent/created', ({ agent }) => {
agent.inject(createUserMessage({ content: [{ type: 'text', text: 'session preamble' }], source: { kind: 'plugin', plugin: 'test' } }))
})
@@ -595,20 +593,17 @@ describe('agent/session-start', () => {
expect(ctxMsg?.type === 'user/message' && ctxMsg.data.source).toEqual({ kind: 'plugin', plugin: 'test' })
})
- it('a throwing session-start listener does not abort agent construction', async () => {
+ it('a throwing creation listener rolls back the agent before its first turn', async () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(adapter)
- ctx.on('agent/session-start', () => { throw new Error('session-start hook broke') })
+ const reason = new Error('creation hook broke')
+ ctx.on('agent/created', () => { throw reason })
- // create must not throw — the listener error is contained/logged
- const agent = await ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
- expect(agent.id).toBe(SessionId('a1'))
-
- // and the agent still runs
- send(agent, 'go')
- await waitForIdle(ctx, agent)
- expect(adapter.requests).toHaveLength(1)
+ await expect(ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })).rejects.toBe(reason)
+ expect(ctx.agents.list()).toEqual([])
+ expect(ctx.sessions.list()).toEqual([])
+ expect(adapter.requests).toHaveLength(0)
})
})
@@ -728,7 +723,7 @@ describe('worked example: a native hook plugin is just a cordis plugin on the se
name: 'native-guard',
apply(ctx: Context) {
// 1. SessionStart: seed a standing instruction.
- ctx.on('agent/session-start', ({ agent, source }) => {
+ ctx.on('agent/created', ({ agent, source }) => {
agent.inject(createUserMessage({ content: [{ type: 'text', text: `policy active (started: ${source})` }], source: { kind: 'plugin', plugin: 'native-guard' } }))
})
// 2. PreStep: reject a forbidden prompt, annotate the rest.
diff --git a/packages/core/agent-loop/tests/resume.spec.ts b/packages/core/agent-loop/tests/resume.spec.ts
index 271849eba3..9a9d80c91b 100644
--- a/packages/core/agent-loop/tests/resume.spec.ts
+++ b/packages/core/agent-loop/tests/resume.spec.ts
@@ -589,12 +589,12 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
await ctx2.fiber.dispose()
})
- it('agent/session-start fires "startup" for createAgent and "resume" for resume()', async () => {
- // Lifecycle 1: a fresh createAgent emits session-start with source 'startup'.
+ it('agent/created fires "startup" for createAgent and "resume" for resume()', async () => {
+ // Lifecycle 1: a fresh createAgent announces creation with source 'startup'.
const adapter1 = new MockAdapter([textResponse('a')])
const { ctx: ctx1, root } = await persistentHarness(adapter1)
const sources1: string[] = []
- ctx1.on('agent/session-start', ({ source }) => void sources1.push(source))
+ ctx1.on('agent/created', ({ source }) => void sources1.push(source))
const h1 = await ctx1.agents.create({ sessionId: SessionId('start-sess') })
expect(sources1).toEqual(['startup'])
h1.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }))
@@ -602,10 +602,10 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
await h1.dispose()
await ctx1.fiber.dispose()
- // Lifecycle 2: resuming the persisted session emits session-start 'resume'.
+ // Lifecycle 2: resuming the persisted session announces creation 'resume'.
const ctx2 = await mountPersistentHarness(root, new MockAdapter([textResponse('b')]))
const sources2: string[] = []
- ctx2.on('agent/session-start', ({ source }) => void sources2.push(source))
+ ctx2.on('agent/created', ({ source }) => void sources2.push(source))
await ctx2.agents.resume({ resumeSessionId: SessionId('start-sess') })
expect(sources2).toEqual(['resume'])
await ctx2.fiber.dispose()
@@ -626,11 +626,8 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
})
ctx.on('agent/created', ({ agent }) => {
expect(agent.status).toBe('idle')
- order.push('agent/created')
- })
- ctx.on('agent/session-start', ({ agent }) => {
expect(() => { agent.cancel({ kind: 'user' }) }).not.toThrow()
- order.push('agent/session-start')
+ order.push('agent/created')
})
const resuming = ctx.agents.resume({
@@ -671,7 +668,6 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
'setup-listener:session/created',
'agent/created',
'setup-listener:agent/created',
- 'agent/session-start',
])
await handle.dispose()
await ctx.fiber.dispose()
@@ -722,7 +718,6 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const published: string[] = []
ctx.on('session/created', () => void published.push('session/created'))
ctx.on('agent/created', () => void published.push('agent/created'))
- ctx.on('agent/session-start', () => void published.push('agent/session-start'))
await expect(ctx.agents.resume({
resumeSessionId: sessionId,
@@ -827,7 +822,6 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const published: string[] = []
ctx.on('session/created', () => void published.push('session/created'))
ctx.on('agent/created', () => void published.push('agent/created'))
- ctx.on('agent/session-start', () => void published.push('agent/session-start'))
let resuming!: ReturnType
const owner = await ctx.plugin(Object.assign((inner: Context) => {
@@ -846,7 +840,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const retry = await promptly(ctx.agents.resume({ resumeSessionId: sessionId, agentOptions: { provider: 'mock', model: 'mock' } }))
await rejection
expect(opens).toBe(2)
- expect(published).toEqual(['session/created', 'agent/created', 'agent/session-start'])
+ expect(published).toEqual(['session/created', 'agent/created'])
// Settlement of the abandoned backend open cannot resume the old
// transaction: the late handle is closed, and no second publication lands
@@ -856,7 +850,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
await expect.poll(() => abandoned.close.mock.calls.length).toBe(1)
expect(ctx.agents.get(sessionId)).toBe(retry.agent)
expect(ctx.sessions.get(sessionId)).toBe(retry.agent.session)
- expect(published).toEqual(['session/created', 'agent/created', 'agent/session-start'])
+ expect(published).toEqual(['session/created', 'agent/created'])
await retry.dispose()
await ctx.fiber.dispose()
diff --git a/packages/core/agent-loop/tests/scope-lifecycle.spec.ts b/packages/core/agent-loop/tests/scope-lifecycle.spec.ts
index 81bfc64abd..12c9c6a09a 100644
--- a/packages/core/agent-loop/tests/scope-lifecycle.spec.ts
+++ b/packages/core/agent-loop/tests/scope-lifecycle.spec.ts
@@ -61,6 +61,97 @@ function disposeCurrentLifecycle(ownerCtx: Context): void {
}
describe('agent scope lifecycle', () => {
+ it('awaits serial creation before caller readiness', async () => {
+ const ctx = await harness()
+ const entered = Promise.withResolvers()
+ const release = Promise.withResolvers()
+ const order: string[] = []
+ ctx.on('agent/created', async () => {
+ order.push('created:start')
+ entered.resolve(undefined)
+ await release.promise
+ order.push('created:end')
+ })
+ ctx.on('agent/created', () => { order.push('created:second') })
+ const creating = ctx.agentLoop.create(SessionId('serial-start')).then((agent) => {
+ order.push('ready')
+ return agent
+ })
+ try {
+ await entered.promise
+ expect(order).toEqual(['created:start'])
+ release.resolve(undefined)
+ await creating
+ expect(order).toEqual(['created:start', 'created:end', 'created:second', 'ready'])
+ } finally {
+ release.resolve(undefined)
+ await ctx.fiber.dispose()
+ }
+ })
+
+ it('retains both entries and scoped effects until an asynchronous creation listener settles on owner unload', async () => {
+ const ctx = await harness()
+ const entered = Promise.withResolvers()
+ const release = Promise.withResolvers()
+ let ownerCtx!: Context
+ const owner = await ctx.plugin(Object.assign((inner: Context) => { ownerCtx = inner }, { inject: ['agents'] }))
+ const order: string[] = []
+ const id = SessionId('serial-owner-dispose')
+ ctx.on('agent/created', async ({ agent }) => {
+ entered.resolve(undefined)
+ await release.promise
+ expect(ctx.agents.get(id)).toBe(agent)
+ expect(ctx.sessions.get(id)).toBe(agent.session)
+ expect(order).toEqual([])
+ order.push('created:end')
+ })
+ ctx.on('agent/disposed', () => { order.push('agent-disposed') })
+ ctx.on('session/disposed', () => { order.push('session-disposed') })
+ const creating = ownerCtx.agents.create({
+ sessionId: id,
+ setup(agentCtx) { agentCtx.effect(() => () => { order.push('scope-disposed') }) },
+ })
+ const rejected = expect(creating).rejects.toThrow('owner disposed during setup')
+ try {
+ await entered.promise
+ const disposing = owner.dispose()
+ expect(ctx.agents.get(id)).toBeDefined()
+ release.resolve(undefined)
+ await rejected
+ await disposing
+ expect(order).toEqual(['created:end', 'scope-disposed', 'agent-disposed', 'session-disposed'])
+ expect(ctx.agents.get(id)).toBeUndefined()
+ expect(ctx.sessions.get(id)).toBeUndefined()
+ } finally {
+ release.resolve(undefined)
+ await ctx.fiber.dispose()
+ }
+ })
+
+ it('rolls back both registries and the scope when asynchronous creation fails', async () => {
+ const adapter = new MockAdapter([textResponse('unexpected')])
+ const ctx = await harness(adapter)
+ const reason = new Error('async creation veto')
+ const order: string[] = []
+ ctx.on('agent/created', async ({ agent }) => {
+ agent.followup(createUserMessage({ content: text('queued during creation'), source: { kind: 'user' } }))
+ await Promise.resolve()
+ throw reason
+ })
+ ctx.on('agent/created', () => { order.push('later-listener') })
+ ctx.on('agent/disposed', () => { order.push('agent-disposed') })
+ ctx.on('session/disposed', () => { order.push('session-disposed') })
+ await expect(ctx.agents.create({
+ sessionId: SessionId('async-veto'),
+ setup(agentCtx) { agentCtx.effect(() => () => { order.push('scope-disposed') }) },
+ })).rejects.toBe(reason)
+ expect(order).toEqual(['scope-disposed', 'agent-disposed', 'session-disposed'])
+ expect(ctx.agents.list()).toEqual([])
+ expect(ctx.sessions.list()).toEqual([])
+ expect(adapter.requests).toHaveLength(0)
+ await ctx.fiber.dispose()
+ })
+
it('rejects an already-aborted creation signal before publishing either object', async () => {
const ctx = await harness()
const reason = new Error('cancelled before creation')
@@ -241,12 +332,12 @@ describe('agent scope lifecycle', () => {
expect(heard).toContain('a-sees:user-message')
})
- it('runs setup in the guaranteed slot: scoped world complete before session-start and the first assembly', async () => {
+ it('runs setup in the guaranteed slot: scoped world complete before agent/created and the first assembly', async () => {
const ctx = await harness()
const order: string[] = []
- ctx.on('agent/session-start', ({ agent }) => {
- order.push('session-start')
- // The scoped section is already registered by the time session-start fires.
+ ctx.on('agent/created', ({ agent }) => {
+ order.push('agent/created')
+ // The scoped section is already registered by the time agent/created fires.
void ctx.systemPrompt.assemble(assembleContextFor(agent)).then((assembly) => {
order.push(`persona:${assembly.sections.find(s => s.name === 'deployment:persona-prefix')?.text}`)
})
@@ -261,8 +352,7 @@ describe('agent scope lifecycle', () => {
agentCtx.systemPrompt.section({ name: 'deployment:persona-prefix', order: 0, text: 'You are the child.' })
},
})
- await new Promise(resolve => setTimeout(resolve, 0))
- expect(order).toEqual(['setup', 'session-start', 'persona:You are the child.'])
+ expect(order).toEqual(['setup', 'agent/created', 'persona:You are the child.'])
await handle.dispose()
})
@@ -277,7 +367,6 @@ describe('agent scope lifecycle', () => {
order.push('session/created')
})
ctx.on('agent/created', () => void order.push('agent/created'))
- ctx.on('agent/session-start', () => void order.push('agent/session-start'))
const acceptedOptions = { provider: 'mock', model: 'mock' }
const creating = ctx.agents.create({
@@ -315,7 +404,6 @@ describe('agent scope lifecycle', () => {
'setup-listener:session/created',
'agent/created',
'setup-listener:agent/created',
- 'agent/session-start',
])
await handle.dispose()
})
@@ -745,33 +833,7 @@ describe('agent scope lifecycle', () => {
await ctx.fiber.dispose()
})
- it('rechecks caller liveness after creation listeners before unlocking the driver', async () => {
- const ctx = await harness()
- const starts: string[] = []
- let ownerCtx!: Context
- let creating!: ReturnType
- ctx.on('agent/session-start', ({ agent }) => void starts.push(agent.id))
- ctx.on('agent/created', ({ agent }) => {
- if (agent.id === SessionId('listener-dispose-s')) disposeCurrentLifecycle(ownerCtx)
- })
-
- const owner = await ctx.plugin(Object.assign((inner: Context) => {
- ownerCtx = inner
- creating = inner.agents.create({
- sessionId: SessionId('listener-dispose-s'),
- agentOptions: { provider: 'mock', model: 'mock' },
- })
- }, { inject: ['agents'] }))
-
- await expect(creating).rejects.toThrow(/owner disposed during setup/)
- await owner.dispose()
- expect(starts).toEqual([])
- expect(ctx.agents.get(SessionId('listener-dispose-s')) === undefined).toBe(true)
- expect(ctx.sessions.get(SessionId('listener-dispose-s')) === undefined).toBe(true)
- await ctx.fiber.dispose()
- })
-
- it('rechecks caller liveness after session-start before starting the driver', async () => {
+ it('rechecks caller liveness after agent/created before starting the driver', async () => {
const ctx = await harness()
let ownerCtx!: Context
let creating!: ReturnType
@@ -780,15 +842,15 @@ describe('agent scope lifecycle', () => {
let scopeDisposed = false
let observerSawLive = false
ctx.on('agent/status', ({ agent, status }) => {
- if (agent.id === SessionId('session-start-dispose-s')) statuses.push(status)
+ if (agent.id === SessionId('agent/created-dispose-s')) statuses.push(status)
})
- ctx.on('agent/session-start', ({ agent }) => {
- if (agent.id !== SessionId('session-start-dispose-s')) return
+ ctx.on('agent/created', ({ agent }) => {
+ if (agent.id !== SessionId('agent/created-dispose-s')) return
announced = agent
disposeCurrentLifecycle(ownerCtx)
})
- ctx.on('agent/session-start', ({ agent }) => {
- if (agent.id !== SessionId('session-start-dispose-s')) return
+ ctx.on('agent/created', ({ agent }) => {
+ if (agent.id !== SessionId('agent/created-dispose-s')) return
expect(ctx.agents.get(agent.id)).toBe(agent)
expect(ctx.sessions.get(agent.session.id)).toBe(agent.session)
agent.ctx.effect(() => () => { scopeDisposed = true })
@@ -798,7 +860,7 @@ describe('agent scope lifecycle', () => {
const owner = await ctx.plugin(Object.assign((inner: Context) => {
ownerCtx = inner
creating = inner.agents.create({
- sessionId: SessionId('session-start-dispose-s'),
+ sessionId: SessionId('agent/created-dispose-s'),
agentOptions: { provider: 'mock', model: 'mock' },
})
}, { inject: ['agents'] }))
@@ -810,8 +872,8 @@ describe('agent scope lifecycle', () => {
expect(observerSawLive).toBe(true)
expect(scopeDisposed).toBe(true)
expect(announced.session.snapshotEvents()).toEqual([])
- expect(ctx.agents.get(SessionId('session-start-dispose-s'))).toBeUndefined()
- expect(ctx.sessions.get(SessionId('session-start-dispose-s'))).toBeUndefined()
+ expect(ctx.agents.get(SessionId('agent/created-dispose-s'))).toBeUndefined()
+ expect(ctx.sessions.get(SessionId('agent/created-dispose-s'))).toBeUndefined()
await ctx.fiber.dispose()
})
@@ -820,7 +882,6 @@ describe('agent scope lifecycle', () => {
const published: string[] = []
ctx.on('session/created', () => void published.push('session/created'))
ctx.on('agent/created', () => void published.push('agent/created'))
- ctx.on('agent/session-start', () => void published.push('agent/session-start'))
await expect(ctx.agents.create({
sessionId: SessionId('bad-s'),
agentOptions: { provider: 'mock', model: 'mock' },
diff --git a/packages/core/agent-loop/tests/serial-listener-review.spec.ts b/packages/core/agent-loop/tests/serial-listener-review.spec.ts
new file mode 100644
index 0000000000..88eb59c909
--- /dev/null
+++ b/packages/core/agent-loop/tests/serial-listener-review.spec.ts
@@ -0,0 +1,123 @@
+import { setImmediate } from 'node:timers/promises'
+import { afterEach, describe, expect, it } from 'vitest'
+import { mkdtemp, rm } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { Context } from '@deepseek-ai/cordis'
+import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
+import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
+import { SessionId } from '@deepseek-ai/dsh-session'
+import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
+import { bindScopeParent, createScope, scopeOf } from '@deepseek-ai/dsh-scope'
+import LocalFileReference from '@deepseek-ai/dsh-file-reference-local'
+import SubagentRuntime from '@deepseek-ai/dsh-subagent'
+import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
+import * as SubagentTool from '@deepseek-ai/dsh-tool-subagent'
+import Selection from '@deepseek-ai/dsh-tool-subagent/model-selection-settings'
+import * as Schedule from '@deepseek-ai/dsh-schedule'
+
+const roots: string[] = []
+const contexts: Context[] = []
+afterEach(async () => {
+ for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
+ for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
+})
+
+async function core(persistenceRoot?: string) {
+ const ctx = new Context()
+ contexts.push(ctx)
+ await mountAgentLoopTestDependencies(ctx)
+ if (persistenceRoot !== undefined) await ctx.plugin(JsonlSessionPersistence, { root: persistenceRoot, compression: 'none' })
+ await ctx.plugin(AgentLoop, { agents: [] })
+ return ctx
+}
+
+describe('serial creation listener integrations', () => {
+ it('rolls back creation when file-reference prompt installation fails', async () => {
+ const ctx = await core()
+ const errors: string[] = []
+ ctx.logger.error = ((error: unknown) => { errors.push(String(error)) }) as typeof ctx.logger.error
+ await ctx.plugin(LocalFileReference)
+ try {
+ await expect(ctx.agents.create({
+ sessionId: SessionId('review-file-reference'),
+ setup(agentCtx) {
+ agentCtx.systemPrompt.section({ name: 'context:file-reference', order: 0, text: 'occupied' })
+ },
+ })).rejects.toThrow('context:file-reference')
+ expect(errors.some(error => error.includes('context:file-reference'))).toBe(true)
+ expect(ctx.agents.list()).toEqual([])
+ expect(ctx.sessions.list()).toEqual([])
+ } finally {
+ await ctx.fiber.dispose()
+ }
+ })
+
+ it('rolls back creation when a shared preset tool installation fails', async () => {
+ const ctx = await core()
+ const errors: string[] = []
+ ctx.logger.error = ((error: unknown) => { errors.push(String(error)) }) as typeof ctx.logger.error
+ await ctx.plugin(Selection)
+ await ctx.plugin(SubagentRuntime)
+ await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
+ const preset = createScope(ctx, { preset: 'review' })
+ await preset.ctx.plugin(SubagentTool, { provider: 'spawn', modelSelectionSettings: true, backgroundMode: 'continuable' })
+ try {
+ await expect(ctx.agents.create({
+ sessionId: SessionId('review-tool'),
+ setup(agentCtx) {
+ bindScopeParent(scopeOf(agentCtx)!, scopeOf(preset.ctx)!)
+ agentCtx.tools.register(defineContentToolFixture({ name: 'subagent', description: 'occupied', parameters: {}, execute: () => Promise.resolve([]) }))
+ },
+ })).rejects.toThrow('subagent')
+ expect(errors.some(error => error.includes('subagent'))).toBe(true)
+ expect(ctx.agents.list()).toEqual([])
+ expect(ctx.sessions.list()).toEqual([])
+ } finally {
+ await ctx.fiber.dispose()
+ }
+ })
+
+ it('starts due Schedule work only after every creation listener finishes', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'dsh-serial-schedule-'))
+ roots.push(root)
+ const ctx = await core(root)
+ await ctx.plugin(Schedule)
+ const entered = Promise.withResolvers()
+ const release = Promise.withResolvers()
+ const turn = Promise.withResolvers()
+ let turns = 0
+ let created = false
+ let laterCreated = false
+ ctx.on('agent/created', async () => { entered.resolve(undefined); await release.promise; created = true })
+ ctx.on('agent/created', () => { laterCreated = true })
+ ctx.on('session/event', (_session, event) => { if (event.type === 'turn/start') { turns += 1; turn.resolve(undefined) } })
+ const creating = ctx.agents.create({
+ sessionId: SessionId('review-schedule'),
+ setup(_agentCtx, agent) {
+ agent.session.append('schedule/change', {
+ version: 1,
+ operation: 'create',
+ schedule: Schedule.createAfterScheduleRecord(Schedule.ScheduleId('schedule-1'), 'due reminder', 1, Date.now() - 2_000),
+ })
+ },
+ })
+ try {
+ await entered.promise
+ // A full event-loop turn lets the due runtime run if it starts during creation.
+ await setImmediate()
+ expect(turns).toBe(0)
+ expect(laterCreated).toBe(false)
+ release.resolve(undefined)
+ await creating
+ await turn.promise
+ expect(created).toBe(true)
+ expect(laterCreated).toBe(true)
+ } finally {
+ release.resolve(undefined)
+ await creating
+ await ctx.fiber.dispose()
+ }
+ })
+})
diff --git a/packages/core/agent/README.i18n.yaml b/packages/core/agent/README.i18n.yaml
index ac94cf9d7c..59f4628536 100644
--- a/packages/core/agent/README.i18n.yaml
+++ b/packages/core/agent/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/agent/README.md
-README.md: 82460df7492d470a55c37c4691af2a7d349144fe
-README.zh.md: 9ece26dbacad4576a72114573af3d77fa41693e8
+README.md: 8ba3dc730790acb0f792173e9ee01d2dc954957e
+README.zh.md: 0718ae5f3f73b1fd0bb5b600de3a08c50e67421e
diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md
index 82460df749..8ba3dc7307 100644
--- a/packages/core/agent/README.md
+++ b/packages/core/agent/README.md
@@ -104,7 +104,7 @@ The package is built on one separation: the public `Agent` surface and registry
### Registry and lifecycle
-`AgentRegistry` keeps one entry per live agent with its carrier and creator relation. `register()` records an already-constructed agent; the async factory uses the split `enter()`/`announce()` pair so setup and publication stay rollback-covered. A detach requested during a creation dispatch waits for that dispatch to unwind, and each detach is bound to the exact entry, so a stale disposer cannot remove a later same-id replacement. Teardown order is stop-and-drain the loop, unwind the scope, detach the agent, detach the session; the id becomes reusable after private cleanup.
+`AgentRegistry` keeps one entry per live agent with its carrier and creator relation. Await `register()` to finish serial creation listeners with source `startup` before using an already-constructed agent; the async factory uses the split `enter()`/`announce()` pair so setup and initialization stay rollback-covered. A detach requested during creation waits for every awaited listener to settle, and each detach is bound to the exact entry, so a stale disposer cannot remove a later same-id replacement. Teardown stops and drains the loop, unwinds the scope, detaches the agent, then detaches the session; the id becomes reusable after private cleanup.
### Initiator scope
@@ -171,7 +171,7 @@ These limits define when this package needs special care. They are current packa
- **Initiator scope is process-local** — workers, child processes, HTTP, durable queues, and restarts must materialize any required identity explicitly.
- **Ambient identity may outlive liveness** — consumers still check `agent.status`, cancellation, and the owning capability contract before lifecycle-sensitive work.
-- **`agent/session-start` cannot gate startup** — it remains a synchronous, veto-less notification; async composition that must finish before publication belongs in the factory's `setup(agentCtx, agent)` transaction instead.
+- **Creation listeners share the initialization lifetime.** An `agent/created` listener must not await `agent.whenIdle()` or its own owner's disposal: those operations wait for creation to finish. Return only after required asynchronous tool and prompt installation completes.
- **`cancel()` clears the inbox by default** — it aborts the in-flight turn plus queued and steering work; `cancel(cause, { keepInbox: true })` aborts only the turn and preserves pending items, and there is no step-only abort that keeps the turn running.
- **Each additional `UserMessage` carries exactly one `MessageSource`** — contributions from several plugins merged onto one message collapse under one source, so the message cannot name several producers.
diff --git a/packages/core/agent/README.zh.md b/packages/core/agent/README.zh.md
index 9ece26dbac..0718ae5f3f 100644
--- a/packages/core/agent/README.zh.md
+++ b/packages/core/agent/README.zh.md
@@ -104,7 +104,7 @@ await handle.agent.whenIdle()
### 注册表与生命周期
-`AgentRegistry` 为每个实时 agent 保留一个条目,含其载体与创建者关系。`register()` 记录一个已构造完成的 agent;异步工厂使用拆分的 `enter()`/`announce()` 对,使 setup 与发布始终处于回滚保护之下。创建分发期间请求的 detach 会等待该次分发退栈,且每次 detach 都绑定到确切条目,因此陈旧 disposer 无法移除之后出现的同 id 替代项。Teardown 顺序是停止并排空循环、撤销作用域、detach agent、detach 会话;私有清理完成后该 id 即可复用。
+`AgentRegistry` 为每个实时 agent 保留一个条目,含其载体与创建者关系。使用已构造的 agent 前,等待 `register()` 以 `startup` 来源完成串行创建监听器;异步工厂使用拆分的 `enter()`/`announce()` 对,使 setup 与初始化始终受回滚保护。创建期间请求的 detach 会等待所有已调用的异步监听器结算,且每次 detach 都绑定到确切条目,因此陈旧 disposer 无法移除之后出现的同 id 替代项。Teardown 停止并排空循环、撤销作用域、detach agent,再 detach 会话;私有清理完成后该 id 即可复用。
### 发起方作用域
@@ -171,7 +171,7 @@ await handle.agent.whenIdle()
- **发起方作用域只存在于进程内**:worker、子进程、HTTP、持久队列和重启必须显式传递所需身份。
- **环境身份可能比存活状态更久**:消费方在生命周期敏感工作前,仍要检查 `agent.status`、取消状态和所属能力约定。
-- **`agent/session-start` 不能为启动设置门禁**:它仍是同步且不可 veto 的通知;必须在发布前完成的异步组合属于工厂的 `setup(agentCtx, agent)` 事务。
+- **创建监听器共享初始化生命周期。** `agent/created` 监听器不得等待 `agent.whenIdle()` 或自身所有者的 dispose:这些操作要等待创建完成。所需的异步工具与提示词安装完成后,监听器才可返回。
- **`cancel()` 默认清空收件箱**:它会中止正在处理的轮次以及排队和 steering 工作;`cancel(cause, { keepInbox: true })` 只中止轮次并保留待处理项,且不存在让轮次继续运行、只中止步骤的操作。
- **每条附加 `UserMessage` 恰好携带一个 `MessageSource`**:多个插件合并到一条消息上的贡献会归入同一来源,因此该消息无法列出多个生产者。
diff --git a/packages/core/agent/src/index.ts b/packages/core/agent/src/index.ts
index 21c95b5c60..a7d39f6f9a 100644
--- a/packages/core/agent/src/index.ts
+++ b/packages/core/agent/src/index.ts
@@ -13,7 +13,7 @@ import { scopeTarget } from '@deepseek-ai/dsh-scope'
import type { Scoped } from '@deepseek-ai/dsh-scope'
import type { SessionEvent, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
import type { Agent } from './types.ts'
-import type { AgentOptions } from './runtime-types.ts'
+import type { AgentOptions, SessionStartSource } from './runtime-types.ts'
export * from './runtime-types.ts'
export * from './types.ts'
@@ -107,7 +107,7 @@ export interface CreateAgentOptions {
* the exact publication boundary. Everything registered through `agentCtx`
* (scoped tools, prompt sections/variables, `restrict()`, listeners, awaited
* child plugins) exists before `session/created`, `agent/created`,
- * `agent/session-start`, and the first prompt assembly. A setup
+ * and the first prompt assembly. A setup
* throw/rejection, commit throw, or owner disposal rolls the scope back
* without publishing either id.
*
@@ -172,8 +172,8 @@ export interface AgentFactory {
/**
* Create a new agent on a caller-supplied session id. Async because creation
* awaits unpublished setup, invokes its optional synchronous commit, inserts
- * both session and agent, emits their creation notifications in order, emits
- * `agent/session-start`, and only then starts the loop. The sequence is
+ * both session and agent, announces session creation, and awaits serial
+ * `agent/created` listeners before releasing queued work. The sequence is
* rollback-covered, but notifications delivered before a later listener
* failure remain observable; every agent or session creation announcement
* that began is paired by `agent/disposed` or `session/disposed` during
@@ -185,7 +185,7 @@ export interface AgentFactory {
* ownership from the factory object's registration context.
* @param ownerCtx - caller-bound context that owns the transaction and live handle.
* @param options - agent/session identity, configuration, optional live parent, and setup.
- * @returns the owned handle after setup, both announcements, and loop start complete.
+ * @returns the owned handle after setup and both creation announcements complete.
*/
createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise
/**
@@ -413,16 +413,16 @@ export class AgentRegistry extends Service {
}
/**
- * Register a live agent. Throws if an agent with the same id is already
- * registered. Emits `agent/created` on registration and `agent/disposed`
+ * Register a live agent with source `startup`. Rejects if the id is already registered or a
+ * serial `agent/created` listener fails. Emits `agent/disposed`
* when the calling fiber is disposed — both with the agent's scope carrier
* (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the
* emits are scope-filtered regardless of which context invoked `register`
* (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
* requires passing the carrier). The entry is a runtime root; factory-backed
- * creation uses `options.parentAgent` for child ownership. Returns the disposer.
+ * creation uses `options.parentAgent` for child ownership. Await the registration before using the agent.
* @param agent - the already-constructed agent to record in the store.
- * @returns the EXACT Cordis effect disposer (single-shot; a repeat call
+ * @returns the awaitable Cordis effect disposer (single-shot; a repeat call
* returns undefined without awaiting an in-flight teardown). Exact
* identity is load-bearing: a composite (generator) effect that owns a
* teardown ORDER — the agent factory's lifecycle chain — must yield THIS
@@ -431,13 +431,11 @@ export class AgentRegistry extends Service {
* owner unload, unregistering the agent (and emitting `agent/disposed`)
* while its final turn is still draining.
*/
- register(agent: Agent): () => void {
- const dispose = this.ctx.effect(function* (this: AgentRegistry) {
+ register(agent: Agent): ReturnType {
+ return this.ctx.effect(async function* (this: AgentRegistry) {
yield this.enter(agent, undefined)
- this.announce(agent)
+ await this.announce(agent, 'startup')
}.bind(this), 'agents.register()')
- // oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
- return dispose
}
/**
@@ -452,8 +450,8 @@ export class AgentRegistry extends Service {
* the resumed session's durable parent lineage.
* @returns an idempotent closure that removes this exact entry and emits
* `agent/disposed` with listener failures contained. When called from a
- * synchronous `agent/created` listener, removal and disposal wait until
- * that creation dispatch unwinds.
+ * `agent/created` listener, removal and disposal wait until the serial
+ * creation dispatch settles.
*/
enter(agent: Agent, owner: Agent | undefined): () => void {
const id = agent.id
@@ -482,7 +480,7 @@ export class AgentRegistry extends Service {
// live entry, and disposal must follow creation. A listener may own
// the advanced detach capability, so make that ordering structural:
// visibility and the paired disposal are deferred until announce()'s
- // synchronous dispatch has unwound.
+ // serial dispatch has settled.
if (entry.announcing) {
entry.detachRequested = true
return
@@ -526,11 +524,14 @@ export class AgentRegistry extends Service {
/**
* Announce an agent previously inserted with {@link enter}.
* @param agent - the live inserted agent to announce.
+ * @param source - fresh creation, resume, clear, or compaction source.
+ * @param signal - optional factory initialization cancellation signal passed to listeners.
+ * @returns completion of the serial creation listeners; a listener failure rejects.
* @throws if `agent` is not the exact live registry entry for its id, or its
* creation announcement already began (including a reentrant call from a
* creation listener).
*/
- announce(agent: Agent): void {
+ async announce(agent: Agent, source: SessionStartSource, signal?: AbortSignal): Promise {
const entry = this.store.get(agent.id)
if (entry === undefined || entry.agent !== agent) {
throw new Error(`agent "${agent.id}" is not live in this registry`)
@@ -542,17 +543,12 @@ export class AgentRegistry extends Service {
// lifecycle edge; detach still pairs a partially delivered first edge.
entry.announcing = true
entry.announced = true
- const args: unknown[] = [entry.carrier, 'agent/created', { agent: entry.agent }]
try {
- for (const callback of this.ctx.events.dispatch('emit', args)) {
- // A synchronous creation failure vetoes publication and rolls back.
- // Returned-promise rejection happens after this synchronous boundary, so
- // observe and report it instead of leaking an unhandled rejection.
- const returned: unknown = callback(...args)
- void Promise.resolve(returned).catch((error: unknown) => {
- this.ctx.logger.warn(`agent "${entry.id}": agent/created listener rejected: ${String(error)}`)
- })
- }
+ await this.ctx.serial(entry.carrier, 'agent/created', {
+ agent: entry.agent,
+ source,
+ ...signal === undefined ? {} : { signal },
+ })
} finally {
entry.announcing = false
if (entry.detachRequested) this.detachEntered(entry)
diff --git a/packages/core/agent/src/runtime-types.ts b/packages/core/agent/src/runtime-types.ts
index 31338e8d8d..2118507b8c 100644
--- a/packages/core/agent/src/runtime-types.ts
+++ b/packages/core/agent/src/runtime-types.ts
@@ -244,18 +244,21 @@ declare module './types.ts' {
declare module '@deepseek-ai/cordis' {
interface Events {
- // ---- lifecycle (emit) ----
+ // ---- lifecycle ----
/**
- * A fully configured agent and live session were published. Setup is
- * composition-only; `agent/session-start` is the first startup-driving extension point.
- * Synchronous listener failure vetoes publication, while returned-promise
- * rejection is reported. Detach requested during dispatch waits until every
- * creation listener has observed the stable entry.
+ * An entered agent is ready for per-agent initialization after factory setup.
+ * Listeners run in order and are awaited before creation resolves. AgentLoop
+ * holds queued input until all listeners finish. A throw or rejection fails
+ * creation and skips later listeners. Disposal retains the scope and session
+ * until dispatch settles; listeners must not await agent.whenIdle() or their
+ * own owner's disposal.
* @param payload.agent - the newly registered agent with its live session and completed setup.
+ * @param payload.source - fresh creation, resume, clear, or compaction source.
+ * @param payload.signal - factory initialization cancellation signal, when provided.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
+ * @mode serial
*/
- 'agent/created'(this: Scoped, payload: { agent: Agent }): void
+ 'agent/created'(this: Scoped, payload: { agent: Agent; source: SessionStartSource; signal?: AbortSignal }): undefined | Promise
/**
* An agent left the registry; AgentLoop emits this after driver quiescence
* and scoped-registration unwind, but before session detachment. Custom
@@ -302,19 +305,6 @@ declare module '@deepseek-ai/cordis' {
* @mode emit
*/
'agent/inbox/discarded'(this: Scoped, payload: { agent: Agent; message: UserMessage }): void
- // ---- session lifecycle (emit) ----
- /**
- * The session lifecycle began, once before the first turn. Use
- * `agent.inject()` to seed model-facing context. This is a notification, not
- * a veto; disposal requested by a lifecycle owner is rechecked before the
- * driver starts.
- * @param payload.agent - the agent whose session lifecycle began.
- * @param payload.source - why the session started (fresh startup, resume, …).
- * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- * @mode emit
- */
- 'agent/session-start'(this: Scoped, payload: { agent: Agent; source: SessionStartSource }): void
-
// ---- the machine's extension points ----
/**
* Reject a proposed step or replace the messages that enter it. Calling
diff --git a/packages/core/agent/tests/agent.spec.ts b/packages/core/agent/tests/agent.spec.ts
index ed652ef558..c31acf5c66 100644
--- a/packages/core/agent/tests/agent.spec.ts
+++ b/packages/core/agent/tests/agent.spec.ts
@@ -45,7 +45,7 @@ describe('AgentRegistry', () => {
await agentFiber
await ctx.plugin(TypertRegistry)
const agent = stubAgent('remote-agent')
- const disposeAgent = ctx.agents.register(agent)
+ const disposeAgent = await ctx.agents.register(agent)
const lookup = ctx.typert.lookups.get('agent')
expect(lookup).toMatchObject({
@@ -58,7 +58,7 @@ describe('AgentRegistry', () => {
const context = ctx.typert.contexts.getHost('agent')
expect(context?.resolve(agent.id)).toBe(agent.ctx)
- disposeAgent()
+ await disposeAgent()
expect(lookup?.resolve(agent.id)).toBeUndefined()
await agentFiber.dispose()
expect(ctx.typert.lookups.get('agent')).toBeUndefined()
@@ -73,13 +73,13 @@ describe('AgentRegistry', () => {
ctx.on('agent/disposed', ({ agent }) => void lifecycle.push(`disposed:${agent.id}`))
const agent = stubAgent('a1')
- const dispose = ctx.agents.register(agent)
+ const dispose = await ctx.agents.register(agent)
expect(ctx.agents.get(agent.id)).toBe(agent)
expect(ctx.agents.list()).toEqual([agent])
expect(ctx.agents.roots()).toEqual([agent])
- expect(() => ctx.agents.register(stubAgent('a1'))).toThrow(/already registered/)
+ await expect(Promise.resolve(ctx.agents.register(stubAgent('a1')))).rejects.toThrow(/already registered/)
- dispose()
+ await dispose()
expect(ctx.agents.get(agent.id)).toBeUndefined()
expect(lifecycle).toEqual(['created:a1', 'disposed:a1'])
})
@@ -100,9 +100,9 @@ describe('AgentRegistry', () => {
const root = stubAgent('root')
const child = stubAgent('child')
const detachRoot = ctx.agents.enter(root, undefined)
- ctx.agents.announce(root)
+ await ctx.agents.announce(root, 'startup')
const detachChild = ctx.agents.enter(child, root)
- ctx.agents.announce(child)
+ await ctx.agents.announce(child, 'startup')
expect(ctx.agents.list()).toEqual([root, child])
expect(ctx.agents.roots()).toEqual([root])
@@ -123,35 +123,64 @@ describe('AgentRegistry', () => {
ctx.on('agent/created', () => { throw new Error('creation veto') })
ctx.on('agent/disposed', ({ agent }) => void lifecycle.push(`disposed:${agent.id}`))
- expect(() => ctx.agents.register(stubAgent('vetoed'))).toThrow('creation veto')
+ await expect(Promise.resolve(ctx.agents.register(stubAgent('vetoed')))).rejects.toThrow('creation veto')
expect(ctx.agents.get(SessionId('vetoed'))).toBeUndefined()
expect(lifecycle).toEqual(['created:vetoed', 'disposed:vetoed'])
})
- it('contains asynchronous creation rejection and every disposal-listener failure', async () => {
+ it('rolls back asynchronous creation rejection and contains every disposal-listener failure', async () => {
const ctx = new Context()
await ctx.plugin(AgentRegistry)
const warnings: string[] = []
const heard: string[] = []
ctx.logger.warn = ((message: unknown) => { warnings.push(String(message)) }) as typeof ctx.logger.warn
- ctx.on('agent/created', () => Promise.reject(new Error('created async')) as never)
+ ctx.on('agent/created', () => Promise.reject(new Error('created async')))
+ ctx.on('agent/created', () => { heard.push('unreachable') })
ctx.on('agent/disposed', () => { throw new Error('disposed sync') })
ctx.on('agent/disposed', () => Promise.reject(new Error('disposed async')) as never)
ctx.on('agent/disposed', ({ agent }) => void heard.push(agent.id))
- const dispose = ctx.agents.register(stubAgent('contained'))
- await Promise.resolve()
- dispose()
+ await expect(Promise.resolve(ctx.agents.register(stubAgent('contained')))).rejects.toThrow('created async')
await Promise.resolve()
expect(heard).toEqual(['contained'])
+ expect(ctx.agents.list()).toEqual([])
expect(warnings).toEqual([
- 'agent "contained": agent/created listener rejected: Error: created async',
'agent "contained": agent/disposed listener threw: Error: disposed sync',
'agent "contained": agent/disposed listener rejected: Error: disposed async',
])
})
+ it('awaits each creation listener before the next listener and registration completion', async () => {
+ const ctx = new Context()
+ await ctx.plugin(AgentRegistry)
+ const entered = Promise.withResolvers()
+ const release = Promise.withResolvers()
+ const order: string[] = []
+ ctx.on('agent/created', async () => {
+ order.push('first:start')
+ entered.resolve(undefined)
+ await release.promise
+ order.push('first:end')
+ })
+ ctx.on('agent/created', () => { order.push('second') })
+ const agent = stubAgent('serial')
+ const registration = ctx.agents.register(agent)
+ const ready = Promise.resolve(registration).then(() => { order.push('ready') })
+ try {
+ await entered.promise
+ expect(order).toEqual(['first:start'])
+ await expect(ctx.agents.announce(agent, 'startup')).rejects.toThrow('already announced')
+ release.resolve(undefined)
+ await ready
+ expect(order).toEqual(['first:start', 'first:end', 'second', 'ready'])
+ } finally {
+ release.resolve(undefined)
+ await registration()
+ await ctx.fiber.dispose()
+ }
+ })
+
it('separates entry from announcement and stale/idempotent detach cannot remove a replacement', async () => {
const ctx = new Context()
await ctx.plugin(AgentRegistry)
@@ -162,8 +191,8 @@ describe('AgentRegistry', () => {
const first = stubAgent('split')
const detachFirst = ctx.agents.enter(first, undefined)
expect(lifecycle).toEqual([])
- ctx.agents.announce(first)
- expect(() => { ctx.agents.announce(first) }).toThrow(/already announced/)
+ await ctx.agents.announce(first, 'startup')
+ await expect(ctx.agents.announce(first, 'startup')).rejects.toThrow(/already announced/)
detachFirst()
detachFirst()
@@ -171,7 +200,7 @@ describe('AgentRegistry', () => {
const detachReplacement = ctx.agents.enter(replacement, undefined)
detachFirst()
expect(ctx.agents.get(replacement.id)).toBe(replacement)
- expect(() => { ctx.agents.announce(first) }).toThrow(/not live/)
+ await expect(ctx.agents.announce(first, 'startup')).rejects.toThrow(/not live/)
detachReplacement()
expect(lifecycle).toEqual(['created:split', 'disposed:split'])
})
@@ -189,7 +218,7 @@ describe('AgentRegistry', () => {
ctx.on('agent/created', () => void order.push(`second:${ctx.agents.get(agent.id) === agent}`))
ctx.on('agent/disposed', () => void order.push('disposed'))
const detach = ctx.agents.enter(agent, undefined)
- ctx.agents.announce(agent)
+ await ctx.agents.announce(agent, 'startup')
expect(order).toEqual(['first:true', 'after-detach:true', 'second:true', 'disposed'])
expect(ctx.agents.get(agent.id)).toBeUndefined()
})
@@ -296,12 +325,12 @@ describe('AgentRegistry factory seam', () => {
const { factory, calls } = stubFactory()
ctx.agents.setFactory(factory)
const parent = stubAgent('parent')
- const unregister = ctx.agents.register(parent)
+ const unregister = await ctx.agents.register(parent)
await ctx.agents.create({ sessionId: SessionId('child'), parentAgent: parent })
expect(calls.create[0]?.options.parentAgent).toBe(parent)
- unregister()
+ await unregister()
})
it('rejects a second factory and clears the slot with its owner (HMR)', async () => {
diff --git a/packages/core/scope/src/scoped-events.generated.ts b/packages/core/scope/src/scoped-events.generated.ts
index 8a35f59873..d0e4a9cb97 100644
--- a/packages/core/scope/src/scoped-events.generated.ts
+++ b/packages/core/scope/src/scoped-events.generated.ts
@@ -18,7 +18,6 @@ const scopedSubjectResolvers: Readonly (args[0] as Record)['agent'],
'agent/request': args => (args[0] as Record)['agent'],
'agent/request-error': args => (args[0] as Record)['agent'],
- 'agent/session-start': args => (args[0] as Record)['agent'],
'agent/status': args => (args[0] as Record)['agent'],
'agent/turn-stopping': args => (args[0] as Record)['agent'],
'approval/request': args => (args[0] as Record)['agent'],
diff --git a/packages/core/scope/tests/invariant.spec.ts b/packages/core/scope/tests/invariant.spec.ts
index 57a4a6d091..8e3e358a27 100644
--- a/packages/core/scope/tests/invariant.spec.ts
+++ b/packages/core/scope/tests/invariant.spec.ts
@@ -45,13 +45,12 @@ describe('scoped-dispatch invariants', () => {
source: { kind: 'user' },
})
const agentRows = {
- 'agent/created': [{ agent }],
+ 'agent/created': [{ agent, source: 'startup' }],
'agent/disposed': [{ agent }],
'agent/status': [{ agent, status: 'idle' }],
'agent/inbox/inserted': [{ agent, message }],
'agent/inbox/claimed': [{ agent, message, turn: 1 }],
'agent/inbox/discarded': [{ agent, message }],
- 'agent/session-start': [{ agent, source: 'startup' }],
'agent/pre-step': [{ agent, messages: [message], turn: 1, step: 1, signal }, () => Promise.resolve({ kind: 'enter', messages: [message] })],
'agent/request': [{ agent, turn: 1, step: 1, signal }, () => Promise.resolve(config)],
'agent/assistant-stream': [{
diff --git a/packages/experimental/agent-team/src/index.ts b/packages/experimental/agent-team/src/index.ts
index cb4fcea25a..9066e185ca 100644
--- a/packages/experimental/agent-team/src/index.ts
+++ b/packages/experimental/agent-team/src/index.ts
@@ -108,7 +108,7 @@ export class TeamService extends TypertRemoteService {
this.tasks = new TeamTaskBoard(this.journal, this.config.maxTasks)
ctx.on('session/event', (session, event) => { this.mailbox.observeSessionEvent(session, event) })
- ctx.on('agent/session-start', ({ agent }) => { this.scheduleRecovery(agent) })
+ ctx.on('agent/created', ({ agent }) => { this.scheduleRecovery(agent) })
ctx.on('agent/status', ({ agent }) => {
const membership = this.roster.tryMembership(agent)
if (membership !== undefined) this.activity.notify(membership.id)
diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts
index f0e07bbf2d..bba999184d 100644
--- a/packages/extensions/tool-cordis/src/api-catalog.ts
+++ b/packages/extensions/tool-cordis/src/api-catalog.ts
@@ -301,21 +301,22 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
returns: 'the handle after setup, rollback-covered publication, and loop start complete.',
},
{
- signature: 'register(agent: Agent): () => void',
- description: 'Register a live agent. Throws if an agent with the same id is already registered. Emits `agent/created` on registration and `agent/disposed` when the calling fiber is disposed — both with the agent\'s scope carrier (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the emits are scope-filtered regardless of which context invoked `register` (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always requires passing the carrier). The entry is a runtime root; factory-backed creation uses `options.parentAgent` for child ownership. Returns the disposer.',
+ signature: 'register(agent: Agent): ReturnType',
+ description: 'Register a live agent with source `startup`. Rejects if the id is already registered or a serial `agent/created` listener fails. Emits `agent/disposed` when the calling fiber is disposed — both with the agent\'s scope carrier (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the emits are scope-filtered regardless of which context invoked `register` (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always requires passing the carrier). The entry is a runtime root; factory-backed creation uses `options.parentAgent` for child ownership. Await the registration before using the agent.',
parameters: [{ name: 'agent', description: 'the already-constructed agent to record in the store.' }],
- returns: 'the EXACT Cordis effect disposer (single-shot; a repeat call returns undefined without awaiting an in-flight teardown). Exact identity is load-bearing: a composite (generator) effect that owns a teardown ORDER — the agent factory\'s lifecycle chain — must yield THIS function so Cordis nests the unregistration at that yield position; yielding a wrapper would leave it disposing as a concurrent sibling on owner unload, unregistering the agent (and emitting `agent/disposed`) while its final turn is still draining.',
+ returns: 'the awaitable Cordis effect disposer (single-shot; a repeat call returns undefined without awaiting an in-flight teardown). Exact identity is load-bearing: a composite (generator) effect that owns a teardown ORDER — the agent factory\'s lifecycle chain — must yield THIS function so Cordis nests the unregistration at that yield position; yielding a wrapper would leave it disposing as a concurrent sibling on owner unload, unregistering the agent (and emitting `agent/disposed`) while its final turn is still draining.',
},
{
signature: 'enter(agent: Agent, owner: Agent | undefined): () => void',
description: 'Insert an already-constructed agent without announcing it. This is the advanced ordered-lifecycle primitive used by the async agent factory: it first completes setup while the agent is unpublished, then assigns the returned detach closure into its pre-installed composite teardown before calling announce. Ordinary callers use register.',
parameters: [{ name: 'agent', description: 'the prepared, unpublished agent.' }, { name: 'owner', description: 'explicitly supplied live runtime owner, or undefined for a top-level runtime root. This is runtime ownership, not the resumed session\'s durable parent lineage.' }],
- returns: 'an idempotent closure that removes this exact entry and emits `agent/disposed` with listener failures contained. When called from a synchronous `agent/created` listener, removal and disposal wait until that creation dispatch unwinds.',
+ returns: 'an idempotent closure that removes this exact entry and emits `agent/disposed` with listener failures contained. When called from a `agent/created` listener, removal and disposal wait until the serial creation dispatch settles.',
},
{
- signature: 'announce(agent: Agent): void',
+ signature: 'async announce(agent: Agent, source: SessionStartSource, signal?: AbortSignal): Promise',
description: 'Announce an agent previously inserted with enter.',
- parameters: [{ name: 'agent', description: 'the live inserted agent to announce.' }],
+ parameters: [{ name: 'agent', description: 'the live inserted agent to announce.' }, { name: 'source', description: 'fresh creation, resume, clear, or compaction source.' }, { name: 'signal', description: 'optional factory initialization cancellation signal passed to listeners.' }],
+ returns: 'completion of the serial creation listeners; a listener failure rejects.',
throws: ['if `agent` is not the exact live registry entry for its id, or its creation announcement already began (including a reentrant call from a creation listener).'],
},
{
@@ -3187,11 +3188,11 @@ export const EVENT_API: readonly EventApiEntry[] = [
},
{
name: 'agent/created',
- mode: 'emit',
- signature: '\'agent/created\'(this: Scoped, payload: { agent: Agent }): void',
- summary: 'A fully configured agent and live session were published.',
- description: 'A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving extension point. Synchronous listener failure vetoes publication, while returned-promise rejection is reported. Detach requested during dispatch waits until every creation listener has observed the stable entry.',
- parameters: [{ name: 'payload', description: '.agent - the newly registered agent with its live session and completed setup. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.' }],
+ mode: 'serial',
+ signature: '\'agent/created\'(this: Scoped, payload: { agent: Agent; source: SessionStartSource; signal?: AbortSignal }): undefined | Promise',
+ summary: 'An entered agent is ready for per-agent initialization after factory setup.',
+ description: 'An entered agent is ready for per-agent initialization after factory setup. Listeners run in order and are awaited before creation resolves. AgentLoop holds queued input until all listeners finish. A throw or rejection fails creation and skips later listeners. Disposal retains the scope and session until dispatch settles; listeners must not await agent.whenIdle() or their own owner\'s disposal.',
+ parameters: [{ name: 'payload', description: '.signal - factory initialization cancellation signal, when provided. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.' }],
},
{
name: 'agent/disposed',
@@ -3257,14 +3258,6 @@ export const EVENT_API: readonly EventApiEntry[] = [
description: 'Handle one failed model-request attempt before the loop retries or closes its step. A listener returns `{ kind: \'retry\' }` without calling `next()` when it owns recovery, or calls `next()` to delegate. The default `undefined` leaves the failure terminal.',
parameters: [{ name: 'payload', description: '.signal - the turn abort signal. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.' }],
},
- {
- name: 'agent/session-start',
- mode: 'emit',
- signature: '\'agent/session-start\'(this: Scoped, payload: { agent: Agent; source: SessionStartSource }): void',
- summary: 'The session lifecycle began, once before the first turn.',
- description: 'The session lifecycle began, once before the first turn. Use `agent.inject()` to seed model-facing context. This is a notification, not a veto; disposal requested by a lifecycle owner is rechecked before the driver starts.',
- parameters: [{ name: 'payload', description: '.source - why the session started (fresh startup, resume, …). Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.' }],
- },
{
name: 'agent/status',
mode: 'emit',
diff --git a/packages/feedback/command-feedback/tests/command-feedback.spec.ts b/packages/feedback/command-feedback/tests/command-feedback.spec.ts
index a4e66ceac9..4e5401c09e 100644
--- a/packages/feedback/command-feedback/tests/command-feedback.spec.ts
+++ b/packages/feedback/command-feedback/tests/command-feedback.spec.ts
@@ -57,7 +57,7 @@ async function harness(): Promise {
await ctx.plugin(SessionStore)
const plugin = await ctx.plugin(commandFeedback)
const { agent, session } = stubAgent(ctx, `command-feedback-${Math.random()}`)
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
return { ctx, agent, session, plugin }
}
diff --git a/packages/feedback/command-feedback/tests/loader-composition.spec.ts b/packages/feedback/command-feedback/tests/loader-composition.spec.ts
index 6ab826e516..273657e579 100644
--- a/packages/feedback/command-feedback/tests/loader-composition.spec.ts
+++ b/packages/feedback/command-feedback/tests/loader-composition.spec.ts
@@ -26,7 +26,7 @@ afterEach(async () => {
})
/** Register one idle agent over a store-owned session, as an app's spine does. */
-function agent(ctx: Context): Agent {
+async function agent(ctx: Context): Promise {
const scope = ctx.plugin(() => {})
const id = SessionId('feedback-loader-agent')
const session = ctx.sessions.create(id)
@@ -46,7 +46,7 @@ function agent(ctx: Context): Agent {
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
- ctx.agents.register(value)
+ await ctx.agents.register(value)
return value
}
@@ -83,7 +83,7 @@ describe('/feedback real Loader composition through cordis.yml', () => {
await context.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } })
await context.loader.await()
- const owner = agent(context)
+ const owner = await agent(context)
const signal = new AbortController().signal
// Discoverable through the composed registry, as a UI adapter finds it.
diff --git a/packages/fs/tool-present/tests/built-errors.e2e.ts b/packages/fs/tool-present/tests/built-errors.e2e.ts
index 466145038e..c0dbbec371 100644
--- a/packages/fs/tool-present/tests/built-errors.e2e.ts
+++ b/packages/fs/tool-present/tests/built-errors.e2e.ts
@@ -32,7 +32,7 @@ try {
const scope = ctx.plugin(() => {})
const session = Session.create('built-present', [], { version: SESSION_FORMAT_VERSION, id: 'built-present', createdAt: 0, cwd: root, isSeeded: false })
const owner = { id: 'built-present', session, ctx: scope.ctx, options: {}, status: 'idle' }
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const events = []
ctx.on('tools/result', (_exec, result) => events.push(result))
let n = 0
diff --git a/packages/fs/tool-present/tests/present.spec.ts b/packages/fs/tool-present/tests/present.spec.ts
index 0f219f7b14..6a603163bd 100644
--- a/packages/fs/tool-present/tests/present.spec.ts
+++ b/packages/fs/tool-present/tests/present.spec.ts
@@ -46,7 +46,7 @@ async function agent(ctx: Context, cwd: string | undefined): Promise {
whenIdle: () => Promise.resolve(),
}
await ctx.plugin(Object.assign((inner: Context) => { scope = createScope(inner, value) }, { inject: ['tools'] }))
- ctx.agents.register(value)
+ await ctx.agents.register(value)
return value
}
diff --git a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts
index e50f87f3a3..1f893f37fe 100644
--- a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts
+++ b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts
@@ -27,7 +27,7 @@ afterEach(async () => {
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true })
})
-function agent(ctx: Context, cwd: string): Agent {
+async function agent(ctx: Context, cwd: string): Promise {
const id = SessionId(`str-replace-editor-owner-${callNumber}`)
const scope = ctx.plugin(() => {})
const session = Session.create(id, [], {
@@ -48,7 +48,7 @@ function agent(ctx: Context, cwd: string): Agent {
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
- ctx.agents.register(value)
+ await ctx.agents.register(value)
return value
}
@@ -88,7 +88,7 @@ async function setup(
}
if (options.fsPolicy === true) await ctx.plugin(FsPolicy)
const fiber = await ctx.plugin(ToolStrReplaceEditor, config)
- return { ctx, root, fiber, owner: agent(ctx, root) }
+ return { ctx, root, fiber, owner: await agent(ctx, root) }
}
describe('tool-str-replace-editor', () => {
diff --git a/packages/goal/command-goal/tests/command-goal.spec.ts b/packages/goal/command-goal/tests/command-goal.spec.ts
index 98e6e4a73c..1e4e65c7a1 100644
--- a/packages/goal/command-goal/tests/command-goal.spec.ts
+++ b/packages/goal/command-goal/tests/command-goal.spec.ts
@@ -52,7 +52,7 @@ async function harness(): Promise {
await ctx.plugin(GoalService)
const plugin = await ctx.plugin(commandGoal)
const { agent, session } = stubAgent(ctx, `command-goal-${Math.random()}`)
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
return { ctx, agent, session, plugin }
}
diff --git a/packages/goal/goal-round-driver/src/index.ts b/packages/goal/goal-round-driver/src/index.ts
index 0074c5178d..cac74fe66c 100644
--- a/packages/goal/goal-round-driver/src/index.ts
+++ b/packages/goal/goal-round-driver/src/index.ts
@@ -248,9 +248,8 @@ export function apply(ctx: Context): void {
disarm(state)
})
- ctx.on('agent/created', ({ agent }) => { stateFor(agent) })
ctx.on('agent/disposed', ({ agent }) => { states.delete(agent) })
- ctx.on('agent/session-start', ({ agent }) => {
+ ctx.on('agent/created', ({ agent }) => {
const state = stateFor(agent)
state.attempt = undefined
state.competingQueued = false
diff --git a/packages/goal/goal-round-driver/tests/goal-round-driver.spec.ts b/packages/goal/goal-round-driver/tests/goal-round-driver.spec.ts
index 2ec805d71b..c7c5ff41f6 100644
--- a/packages/goal/goal-round-driver/tests/goal-round-driver.spec.ts
+++ b/packages/goal/goal-round-driver/tests/goal-round-driver.spec.ts
@@ -937,7 +937,7 @@ describe('same-session goal driving', () => {
it('resets process-local scheduling state at a session-start edge', async () => {
const test = await harness([textResponse('after explicit resume')])
const created = test.ctx.goals.create(test.agent, { objective: 'restart safely', maxGoalRounds: 1 })
- agentEvents(test.ctx, test.agent).emit('agent/session-start', { source: 'resume' })
+ await agentEvents(test.ctx, test.agent).serial('agent/created', { source: 'resume' })
await Promise.resolve()
expect(test.ctx.goals.get(test.agent)).toMatchObject({ activation: 'disarmed', roundsStarted: 0 })
diff --git a/packages/goal/goal/README.i18n.yaml b/packages/goal/goal/README.i18n.yaml
index 52695a36d6..becd6aa8f7 100644
--- a/packages/goal/goal/README.i18n.yaml
+++ b/packages/goal/goal/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/goal/goal/README.md
-README.md: 7810fbb5f78aab73b2959538f1dc8fe64b2b5d62
-README.zh.md: 75e2cbec7dfb280e31f0d33686717901e29b2598
+README.md: 8c282dd94ce0548eed426e33f08e509a0cafd800
+README.zh.md: a5b3f7d71365fd01fd99556024cbf948af81c8a8
diff --git a/packages/goal/goal/README.md b/packages/goal/goal/README.md
index 7810fbb5f7..8c282dd94c 100644
--- a/packages/goal/goal/README.md
+++ b/packages/goal/goal/README.md
@@ -96,7 +96,7 @@ This section explains how the service realizes the behavior above; the observabl
- **Event-sourced state.** Every mutation appends a durable `goal/change` event (version 1) carrying the complete post-mutation snapshot; `clear` writes a revisioned tombstone. The session log is the only durable authority.
- **Compare-and-set mutations.** `ctx.goals` accepts only the exact live `Agent` registered under its id. `get()` returns a detached `GoalView`; mutations take a `GoalRef { id, revision }` and reject stale refs. Creation resolves the deployment default internally before committing.
-- **Activation is process-local.** `armed` and `disarmed` live in a per-session cache and are never persisted. A fresh cache and every `agent/session-start` edge disarm continuation even when replay finds an active durable phase; `disarm()` removes authority without writing a revision or emitting a mutation.
+- **Activation is process-local.** `armed` and `disarmed` live in a per-session cache and are never persisted. A fresh cache and every `agent/created` edge disarm continuation even when replay finds an active durable phase; `disarm()` removes authority without writing a revision or emitting a mutation.
- **Strict replay.** The fold derives lifecycle mutations only from `goal/change` and rejects malformed shapes, discontinuous revisions, illegal phase transitions, non-monotonic per-goal timestamps, and non-sequential admitted rounds. Positive rounds advance only on admitted goal-sourced `user/message` events, and mutation timestamps clamp against the preceding update when wall time moves backward.
- **Projection unit.** The package requires the projection registry and registers a strict `goal` unit. Its host state retains replay validation data and the first failure, while its client view exposes the latest valid whole goal or `null`; `GoalService` rejects access after a retained replay failure.
diff --git a/packages/goal/goal/README.zh.md b/packages/goal/goal/README.zh.md
index 75e2cbec7d..a5b3f7d713 100644
--- a/packages/goal/goal/README.zh.md
+++ b/packages/goal/goal/README.zh.md
@@ -96,7 +96,7 @@ view.activation // 'armed' | 'disarmed' — not persisted
- **事件溯源状态。** 每次变更都追加持久的 `goal/change` 事件(版本 1),携带变更后的完整快照;clear 写入带 revision 的 tombstone。会话日志是唯一的持久权威。
- **比较并设置的变更。** `ctx.goals` 只接受以对应 id 注册的完全相同的活跃 `Agent` 实例。`get()` 返回脱离状态的 `GoalView`;变更携带 `GoalRef { id, revision }` 并拒绝陈旧引用。创建在提交前于内部解析部署默认值。
-- **续行启用状态是进程本地的。** `armed` 与 `disarmed` 保存在每会话缓存中,绝不持久化。新缓存与每次 `agent/session-start` 边界都会停用续行,即使回放发现持久 phase 为 active;`disarm()` 移除续行权限,不写入 revision 也不发出变更事件。
+- **续行启用状态是进程本地的。** `armed` 与 `disarmed` 保存在每会话缓存中,绝不持久化。新缓存与每次 `agent/created` 边界都会停用续行,即使回放发现持久 phase 为 active;`disarm()` 移除续行权限,不写入 revision 也不发出变更事件。
- **严格回放。** 折叠只从 `goal/change` 派生生命周期变更,并拒绝形状错误、不连续 revision、非法 phase 转换、每目标时间戳非单调,以及不连续的已准入 Round。只有已准入的来源为 goal 的 `user/message` 事件会推进正数 Round;挂钟时间倒退时,变更时间戳会限制在不早于上一次更新的值。
- **投影单元。** 本包要求提供投影注册表,并注册一个严格的 `goal` 单元。其宿主状态保留回放校验数据与第一次失败,客户端视图提供最新有效的完整 goal 或 `null`;保留回放失败后,`GoalService` 会拒绝访问。
diff --git a/packages/goal/goal/src/index.ts b/packages/goal/goal/src/index.ts
index 8fa9f6d39b..48a02e485c 100644
--- a/packages/goal/goal/src/index.ts
+++ b/packages/goal/goal/src/index.ts
@@ -252,7 +252,7 @@ export class GoalService extends TypertRemoteService {
this.resolved = {
defaultMaxGoalRounds: resolveMaxGoalRounds(config.defaultMaxGoalRounds ?? 256),
}
- ctx.on('agent/session-start', ({ agent }) => {
+ ctx.on('agent/created', ({ agent }) => {
this.setActivation(agent.session, 'disarmed')
})
ctx.sessionProjections.register(goalProjectionDefinition)
diff --git a/packages/goal/goal/tests/goal.spec.ts b/packages/goal/goal/tests/goal.spec.ts
index ac32cd3765..5bf72d11bf 100644
--- a/packages/goal/goal/tests/goal.spec.ts
+++ b/packages/goal/goal/tests/goal.spec.ts
@@ -87,7 +87,7 @@ async function harness(config: { defaultMaxGoalRounds?: number } = {}) {
await ctx.plugin(AgentRegistry)
await ctx.plugin(GoalService, config)
const stub = stubAgent(`goal-test-${Math.random()}`, undefined, ctx)
- ctx.agents.register(stub.agent)
+ await ctx.agents.register(stub.agent)
return { ctx, ...stub }
}
@@ -165,7 +165,7 @@ describe('GoalService creation and replay', () => {
await ctx.plugin(AgentRegistry)
await ctx.plugin(SessionProjectionRegistry)
const stub = stubAgent('goal-direct-construction')
- ctx.agents.register(stub.agent)
+ await ctx.agents.register(stub.agent)
const goals = new GoalService(ctx)
await new Promise(resolve => setImmediate(resolve))
expect(goals.create(stub.agent, { objective: 'direct' })).toMatchObject({
@@ -193,7 +193,7 @@ describe('GoalService creation and replay', () => {
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(GoalService)
const resumed = stubAgent('seeded-goal', first.session.snapshotEvents())
- ctx.agents.register(resumed.agent)
+ await ctx.agents.register(resumed.agent)
expect(ctx.goals.get(resumed.agent)).toMatchObject({
id: created.id,
roundsStarted: 2,
@@ -208,12 +208,12 @@ describe('GoalService creation and replay', () => {
await ctx.plugin(AgentRegistry)
await ctx.plugin(GoalService)
const parent = stubAgentForSession(ctx.sessions.create(SessionId('goal-fork-parent')), ctx)
- ctx.agents.register(parent.agent)
+ await ctx.agents.register(parent.agent)
const goal = ctx.goals.create(parent.agent, { objective: 'inherit through fork', maxGoalRounds: 5 })
appendRound(parent.session, goal, 1)
const child = stubAgentForSession(ctx.sessions.fork(parent.session), ctx)
- ctx.agents.register(child.agent)
+ await ctx.agents.register(child.agent)
expect(ctx.goals.get(child.agent)).toMatchObject({
id: goal.id,
objective: goal.objective,
@@ -233,7 +233,7 @@ describe('GoalService creation and replay', () => {
})
let goal = ctx.goals.create(agent, { objective: 'stay stopped after resume' })
expect(goal.activation).toBe('armed')
- agentEvents(ctx, agent).emit('agent/session-start', { source: 'resume' })
+ await agentEvents(ctx, agent).serial('agent/created', { source: 'resume' })
expect(ctx.goals.get(agent)?.activation).toBe('disarmed')
goal = ctx.goals.resume(agent, goal)
expect(goal).toMatchObject({ phase: 'active', activation: 'armed', revision: 2 })
@@ -264,7 +264,7 @@ describe('GoalService creation and replay', () => {
const fiber = await ctx.plugin(GoalService)
const first = ctx.goals
const stub = stubAgent('goal-hmr')
- ctx.agents.register(stub.agent)
+ await ctx.agents.register(stub.agent)
const goal = ctx.goals.create(stub.agent, { objective: 'survive service reload' })
await fiber.dispose()
@@ -472,7 +472,7 @@ describe('GoalService mutations', () => {
await ctx.plugin(AgentRegistry)
await ctx.plugin(GoalService)
const stub = stubAgentForSession(ctx.sessions.create(SessionId('goal-reentrant-observer')), ctx)
- ctx.agents.register(stub.agent)
+ await ctx.agents.register(stub.agent)
let observed: ReturnType
ctx.on('session/event', (session, event) => {
if (session === stub.session && event.type === 'goal/change') observed = ctx.goals.get(stub.agent)
@@ -492,7 +492,7 @@ describe('GoalService mutations', () => {
await ctx.plugin(GoalService)
const stub = stubAgent('goal-independent-injection')
stub.agent.inject = () => { throw new Error('injection must not be called') }
- ctx.agents.register(stub.agent)
+ await ctx.agents.register(stub.agent)
expect(ctx.goals.create(stub.agent, { objective: 'persist directly' })).toMatchObject({
objective: 'persist directly',
diff --git a/packages/goal/goal/tests/projection.spec.ts b/packages/goal/goal/tests/projection.spec.ts
index 45cd1ddb37..7a9d85d844 100644
--- a/packages/goal/goal/tests/projection.spec.ts
+++ b/packages/goal/goal/tests/projection.spec.ts
@@ -29,7 +29,7 @@ interface Bench {
}
/** Register a minimal registry-compatible live agent over a store session. */
-function liveAgent(ctx: Context, session: Session): Agent {
+async function liveAgent(ctx: Context, session: Session): Promise {
const status: AgentStatus = 'idle'
const agent: Agent = {
id: session.id,
@@ -46,7 +46,7 @@ function liveAgent(ctx: Context, session: Session): Agent {
runMaintenance: task => task(new AbortController().signal),
whenIdle() { return Promise.resolve() },
}
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
return agent
}
@@ -57,7 +57,7 @@ async function harness(withGoal: boolean): Promise {
await ctx.plugin(SessionProjectionRegistry)
if (withGoal) await ctx.plugin(GoalService)
const session = ctx.sessions.create()
- const agent = liveAgent(ctx, session)
+ const agent = await liveAgent(ctx, session)
return {
ctx,
session,
@@ -231,9 +231,8 @@ describe('goal projection unit', () => {
expect(state).toBeDefined()
Object.assign(state!, { failure })
- expect(() => {
- agentEvents(bench.ctx, bench.agent).emit('agent/session-start', { source: 'resume' })
- }).not.toThrow()
+ await expect(agentEvents(bench.ctx, bench.agent).serial('agent/created', { source: 'resume' }))
+ .resolves.toBeUndefined()
expect(() => bench.ctx.goals.get(bench.agent)).toThrow(failure)
expect(bench.tailValues().goal).toMatchObject({ goal: { objective: 'poisoned replay' } })
})
diff --git a/packages/goal/tool-goal/tests/tool-goal.spec.ts b/packages/goal/tool-goal/tests/tool-goal.spec.ts
index b4b4d54808..50949298ba 100644
--- a/packages/goal/tool-goal/tests/tool-goal.spec.ts
+++ b/packages/goal/tool-goal/tests/tool-goal.spec.ts
@@ -101,7 +101,7 @@ async function harness(config: toolGoal.Config = {}) {
await ctx.plugin(GoalService)
const fiber = await ctx.plugin(toolGoal, config)
const root = stubAgent(`goal-tool-root-${Math.random()}`, undefined, ctx)
- ctx.agents.register(root.agent)
+ await ctx.agents.register(root.agent)
return { ctx, fiber, root }
}
@@ -256,7 +256,7 @@ describe('goal tool execution authority', () => {
const child = stubAgent('goal-tool-child')
ctx.agents.enter(child.agent, root.agent)
- ctx.agents.announce(child.agent)
+ await ctx.agents.announce(child.agent, 'startup')
openTurn(child, { kind: 'user' })
const childResult = await execute(ctx, 'create_goal', { objective: 'child goal' }, child.agent)
expect(childResult.error?.info?.code).toBe('GOAL_TOOL_AUTHORITY_REQUIRED')
@@ -290,7 +290,7 @@ describe('goal tool execution authority', () => {
isSeeded: true,
}, SessionLogOffset(root.session.seq))
const fork = stubAgent(forkId, forkSession)
- ctx.agents.register(fork.agent)
+ await ctx.agents.register(fork.agent)
expect(ctx.goals.get(fork.agent)).toMatchObject({ id: created.id, activation: 'disarmed' })
openTurn(fork, { kind: 'user' }, '继续这个目标')
@@ -345,7 +345,7 @@ describe('goal tool execution authority', () => {
it('rejects an initiator different from exec.agent', async () => {
const { ctx, root } = await harness()
const other = stubAgent('goal-tool-other')
- ctx.agents.register(other.agent)
+ await ctx.agents.register(other.agent)
openTurn(other, { kind: 'user' })
const result = await execute(ctx, 'get_goal', {}, other.agent, root.agent)
expect(result.error?.info?.code).toBe('GOAL_TOOL_DRIVER_REQUIRED')
@@ -447,7 +447,7 @@ describe('goal tool state transitions', () => {
let turn = openTurn(root, { kind: 'user' })
const created = ctx.goals.create(root.agent, { objective: 'continue later' })
closeTurn(root, turn)
- agentEvents(ctx, root.agent).emit('agent/session-start', { source: 'resume' })
+ await agentEvents(ctx, root.agent).serial('agent/created', { source: 'resume' })
expect(ctx.goals.get(root.agent)?.activation).toBe('disarmed')
turn = openTurn(root, { kind: 'user' }, '继续')
const resumed = await execute(ctx, 'update_goal', {
diff --git a/packages/hooks/hooks-claude-code/README.i18n.yaml b/packages/hooks/hooks-claude-code/README.i18n.yaml
index c17a52b720..49de5b8053 100644
--- a/packages/hooks/hooks-claude-code/README.i18n.yaml
+++ b/packages/hooks/hooks-claude-code/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/hooks/hooks-claude-code/README.md
-README.md: fc7bbf753059cb0ec5de48d57f42547b5f2c6770
-README.zh.md: 2e771d2a72c2e356d7c1233492e439075264404b
+README.md: 8a9f235019d588306ba33e77cf57e9c8f41e88bd
+README.zh.md: c7e3607b57a719024ce6df9527c40da2d6d5b1f1
diff --git a/packages/hooks/hooks-claude-code/README.md b/packages/hooks/hooks-claude-code/README.md
index fc7bbf7530..8a9f235019 100644
--- a/packages/hooks/hooks-claude-code/README.md
+++ b/packages/hooks/hooks-claude-code/README.md
@@ -84,7 +84,7 @@ This section explains the design decisions behind the bridge and points at the c
### Hook point mapping
-Each supported event programs against one harness extension point: `SessionStart` emits context into the new session (`agent/session-start`), `UserPromptSubmit` and `PreToolUse` are waterfalls that can reject the incoming action (`agent/pre-step`, `tools/pre-execute`), `PostToolUse` is a waterfall that can block with feedback or add context to the downstream decision (`tools/post-execute`), and `Stop` is a serial listener whose blocking result forces another step through `steer()` (`agent/turn-stopping`). The two subagent events emit into the child lifecycle (`subagent/start`, `subagent/end`): start injects context into a live in-process child, stop observes only. Context-only hooks always delegate via `next()` before folding a sourced message into the downstream decision, so a later listener can still reject or rewrite; blocking decisions map to `deny` (`ask` for `PreToolUse`). The per-event wiring lives in [`src/index.ts`](src/index.ts).
+Each supported event programs against one harness extension point: `SessionStart` adds context through awaited `agent/created` initialization before the first turn, `UserPromptSubmit` and `PreToolUse` are waterfalls that can reject the incoming action (`agent/pre-step`, `tools/pre-execute`), `PostToolUse` is a waterfall that can block with feedback or add context to the downstream decision (`tools/post-execute`), and `Stop` is a serial listener whose blocking result forces another step through `steer()` (`agent/turn-stopping`). The two subagent events emit into the child lifecycle (`subagent/start`, `subagent/end`): start injects context into a live in-process child, stop observes only. Context-only hooks always delegate via `next()` before folding a sourced message into the downstream decision, so a later listener can still reject or rewrite; blocking decisions map to `deny` (`ask` for `PreToolUse`). The per-event wiring lives in [`src/index.ts`](src/index.ts).
### Payloads and environment
diff --git a/packages/hooks/hooks-claude-code/README.zh.md b/packages/hooks/hooks-claude-code/README.zh.md
index 2e771d2a72..c7e3607b57 100644
--- a/packages/hooks/hooks-claude-code/README.zh.md
+++ b/packages/hooks/hooks-claude-code/README.zh.md
@@ -84,7 +84,7 @@ kind: "package-reference"
### Hook 点映射
-每个受支持事件都面向一个 harness 扩展点:`SessionStart` 向新会话发射上下文(`agent/session-start`),`UserPromptSubmit` 与 `PreToolUse` 是能拒绝传入动作的 waterfall(瀑布式事件)(`agent/pre-step`、`tools/pre-execute`),`PostToolUse` 是能带反馈阻塞或向下游决策添加上下文的 waterfall(`tools/post-execute`),`Stop` 是串行监听器,其阻塞结果通过 `steer()` 强制再执行一步(`agent/turn-stopping`)。两个 subagent 事件面向 child 生命周期发射(`subagent/start`、`subagent/end`):start 向仍在运行的同进程 child 注入上下文,stop 只观测。仅提供上下文的 hook 总是先通过 `next()` 委托,再把带来源的消息折叠进下游决策,因此后续监听器仍可拒绝或改写;阻塞决策映射为 `deny`(`PreToolUse` 为 `ask`)。逐事件接线位于 [`src/index.ts`](src/index.ts)。
+每个受支持事件都面向一个 harness 扩展点:`SessionStart` 在首个轮次前通过需等待的 `agent/created` 初始化加入上下文,`UserPromptSubmit` 与 `PreToolUse` 是能拒绝传入动作的 waterfall(瀑布式事件)(`agent/pre-step`、`tools/pre-execute`),`PostToolUse` 是能带反馈阻塞或向下游决策添加上下文的 waterfall(`tools/post-execute`),`Stop` 是串行监听器,其阻塞结果通过 `steer()` 强制再执行一步(`agent/turn-stopping`)。两个 subagent 事件面向 child 生命周期发射(`subagent/start`、`subagent/end`):start 向仍在运行的同进程 child 注入上下文,stop 只观测。仅提供上下文的 hook 总是先通过 `next()` 委托,再把带来源的消息折叠进下游决策,因此后续监听器仍可拒绝或改写;阻塞决策映射为 `deny`(`PreToolUse` 为 `ask`)。逐事件接线位于 [`src/index.ts`](src/index.ts)。
### 载荷与环境
diff --git a/packages/hooks/hooks-claude-code/src/index.ts b/packages/hooks/hooks-claude-code/src/index.ts
index 9c46a465b7..469c1c0dc4 100644
--- a/packages/hooks/hooks-claude-code/src/index.ts
+++ b/packages/hooks/hooks-claude-code/src/index.ts
@@ -199,18 +199,18 @@ export function apply(ctx: Context, config: Config): void {
return [ours, ...theirs ?? []]
}
- // SessionStart injects context when its detached hook resolves; a slow hook
- // may miss the first request.
- // TODO(session-start-gating): add a startup gate before promising first-turn delivery.
- ctx.on('agent/session-start', ({ agent, source }) => {
- detached.track(runPoint('SessionStart', source, sessionStartPayload(agent, source), { agent, signal: detached.signal })
+ ctx.on('agent/created', async ({ agent, source, signal }) => {
+ const ownerSignal = signal === undefined ? detached.signal : AbortSignal.any([signal, detached.signal])
+ const run = runPoint('SessionStart', source, sessionStartPayload(agent, source), { agent, signal: ownerSignal })
.then((merged) => {
const context = contextFrom(merged)
if (context) agent.inject(context)
})
.catch((error: unknown) => {
ctx.logger.warn(`hooks-claude-code: SessionStart hook failed: ${String(error)}`)
- }))
+ })
+ detached.track(run)
+ await run
})
// --- UserPromptSubmit → PreStepDecision. The prompt text is the payload; no
diff --git a/packages/hooks/hooks-claude-code/tests/coverage-cases.ts b/packages/hooks/hooks-claude-code/tests/coverage-cases.ts
index 8627a970cd..a05812666d 100644
--- a/packages/hooks/hooks-claude-code/tests/coverage-cases.ts
+++ b/packages/hooks/hooks-claude-code/tests/coverage-cases.ts
@@ -23,7 +23,11 @@ const testToolSignal = new AbortController().signal
* fallbacks, contextFrom-empty, and the detached-listener catch handlers. */
const dirs: string[] = []
-afterEach(() => { for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }) })
+const contexts: Context[] = []
+afterEach(async () => {
+ for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
+ for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
+})
function subagentCarrier(ctx: Context) {
return scopeTarget(ctx as unknown as SubagentRuntime, undefined)
@@ -40,6 +44,7 @@ function hooks(d: string, h: unknown): string {
type HarnessOpts = { pluginRoot?: string; projectDir?: string; stderrSummaryMaxChars?: number; sessionRoot?: string }
async function harness(configPath: string, adapter: MockAdapter, opts: HarnessOpts = {}): Promise {
const ctx = new Context()
+ contexts.push(ctx)
await mountAgentLoopTestDependencies(ctx)
if (opts.sessionRoot !== undefined) await ctx.plugin(JsonlSessionPersistence, { root: opts.sessionRoot })
await ctx.plugin(AgentLoop, { agents: [] })
@@ -238,7 +243,7 @@ export function defineCoverageCases(group: CoverageGroup): void {
},
session: { id: SessionId('child-x'), header: { id: 'child-x' } },
} as unknown as Parameters[0]
- ctx.agents.register(child)
+ await ctx.agents.register(child)
ctx.emit(subagentCarrier(ctx), 'subagent/start', { runId: SubagentRunId('run-x'), provider: 'p', id: SessionId('child-x'), local: true })
await waitFor(() => injected.includes('child guidance'))
expect(injected).toContain('child guidance')
@@ -254,7 +259,7 @@ export function defineCoverageCases(group: CoverageGroup): void {
const ctx = await harness(path, new MockAdapter([]))
const warn = vi.fn(); ctx.logger.warn = warn as never
const child = { id: SessionId('child-y'), inject: () => { throw new Error('inject boom') }, session: { id: SessionId('child-y'), header: { id: 'child-y' } } } as unknown as Parameters[0]
- ctx.agents.register(child)
+ await ctx.agents.register(child)
ctx.emit(subagentCarrier(ctx), 'subagent/start', { runId: SubagentRunId('run-y'), provider: 'p', id: SessionId('child-y'), local: true })
await waitFor(() => warn.mock.calls.some(c => String(c[0]).includes('SubagentStart hook failed')))
expect(warn).toHaveBeenCalledWith(expect.stringContaining('SubagentStart hook failed'))
@@ -627,24 +632,29 @@ export function defineCoverageCases(group: CoverageGroup): void {
})
- if (group === 'edge-paths') describe('hooks-claude-code coverage — detached-listener catch handlers', () => {
+ if (group === 'edge-paths') describe('hooks-claude-code coverage — lifecycle error handling', () => {
it('a throwing SessionStart inject is contained (logged, agent still runs)', async () => {
const d = dir()
const s = sh(d, 'start.sh', '#!/usr/bin/env bash\necho \'{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"x"}}\'\n')
const path = hooks(d, { SessionStart: [{ hooks: [{ type: 'command', command: s }] }] })
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(path, adapter)
- const agent = await ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
- // Make inject throw, forcing the SessionStart .catch path.
- const original = agent.inject.bind(agent)
- let threw = false
- agent.inject = (() => { threw = true; throw new Error('inject boom') })
- await waitFor(() => threw)
- expect(threw).toBe(true)
- agent.inject = original
- agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
- await waitForIdle(ctx, agent)
- expect(adapter.requests).toHaveLength(1) // loop survived the thrown inject
+ const warn = vi.fn(); ctx.logger.warn = warn as never
+ try {
+ const { agent } = await ctx.agents.create({
+ sessionId: SessionId('a1'),
+ agentOptions: { provider: 'mock', model: 'mock' },
+ setup(_agentCtx, agent) {
+ vi.spyOn(agent, 'inject').mockImplementationOnce(() => { throw new Error('inject boom') })
+ },
+ })
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('SessionStart hook failed'))
+ agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
+ await waitForIdle(ctx, agent)
+ expect(adapter.requests).toHaveLength(1)
+ } finally {
+ await ctx.fiber.dispose()
+ }
})
})
diff --git a/packages/hooks/hooks-codex/README.i18n.yaml b/packages/hooks/hooks-codex/README.i18n.yaml
index 394e134144..2ca40dc327 100644
--- a/packages/hooks/hooks-codex/README.i18n.yaml
+++ b/packages/hooks/hooks-codex/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/hooks/hooks-codex/README.md
-README.md: 77fd99835340b0629b927d57b177a1fbe826c4df
-README.zh.md: 90606678fee694555007ee4b0c5d9cc56733ece0
+README.md: 14c7df7a87faf4735ed95bb1f94e597b721f0a00
+README.zh.md: b26eebe4ca1a21909e2cd2f6ea0e9ef6a2c87463
diff --git a/packages/hooks/hooks-codex/README.md b/packages/hooks/hooks-codex/README.md
index 77fd998353..14c7df7a87 100644
--- a/packages/hooks/hooks-codex/README.md
+++ b/packages/hooks/hooks-codex/README.md
@@ -80,7 +80,7 @@ This section explains the design decisions behind the bridge and points at the c
### Hook point mapping
-Each supported event programs against one harness extension point: `SessionStart` emits context into the new session (`agent/session-start`), `UserPromptSubmit` and `PreToolUse` are waterfalls that can reject the incoming action (`agent/pre-step`, `tools/pre-execute`), `PostToolUse` is a waterfall that can block with feedback or add context to the downstream decision (`tools/post-execute`), and `Stop` is a serial listener whose blocking result forces another step through `steer()` (`agent/turn-stopping`). Context-only hooks always delegate via `next()` before folding a sourced message into the downstream decision, so a later listener can still reject or rewrite; blocking decisions map to `deny` (`PreToolUse` has no `allow` or `ask`). The per-event wiring lives in [`src/index.ts`](src/index.ts).
+Each supported event programs against one harness extension point: `SessionStart` adds context through awaited `agent/created` initialization before the first turn, `UserPromptSubmit` and `PreToolUse` are waterfalls that can reject the incoming action (`agent/pre-step`, `tools/pre-execute`), `PostToolUse` is a waterfall that can block with feedback or add context to the downstream decision (`tools/post-execute`), and `Stop` is a serial listener whose blocking result forces another step through `steer()` (`agent/turn-stopping`). Context-only hooks always delegate via `next()` before folding a sourced message into the downstream decision, so a later listener can still reject or rewrite; blocking decisions map to `deny` (`PreToolUse` has no `allow` or `ask`). The per-event wiring lives in [`src/index.ts`](src/index.ts).
### Payloads and environment
diff --git a/packages/hooks/hooks-codex/README.zh.md b/packages/hooks/hooks-codex/README.zh.md
index 90606678fe..b26eebe4ca 100644
--- a/packages/hooks/hooks-codex/README.zh.md
+++ b/packages/hooks/hooks-codex/README.zh.md
@@ -80,7 +80,7 @@ kind: "package-reference"
### Hook 点映射
-每个受支持事件都面向一个 harness 扩展点:`SessionStart` 向新会话发射上下文(`agent/session-start`),`UserPromptSubmit` 与 `PreToolUse` 是能拒绝传入动作的 waterfall(瀑布式事件)(`agent/pre-step`、`tools/pre-execute`),`PostToolUse` 是能带反馈阻塞或向下游决策添加上下文的 waterfall(`tools/post-execute`),`Stop` 是串行监听器,其阻塞结果通过 `steer()` 强制再执行一步(`agent/turn-stopping`)。仅提供上下文的 hook 总是先通过 `next()` 委托,再把带来源的消息折叠进下游决策,因此后续监听器仍可拒绝或改写;阻塞决策映射为 `deny`(`PreToolUse` 没有 `allow` 或 `ask`)。逐事件接线位于 [`src/index.ts`](src/index.ts)。
+每个受支持事件都面向一个 harness 扩展点:`SessionStart` 在首个轮次前通过需等待的 `agent/created` 初始化加入上下文,`UserPromptSubmit` 与 `PreToolUse` 是能拒绝传入动作的 waterfall(瀑布式事件)(`agent/pre-step`、`tools/pre-execute`),`PostToolUse` 是能带反馈阻塞或向下游决策添加上下文的 waterfall(`tools/post-execute`),`Stop` 是串行监听器,其阻塞结果通过 `steer()` 强制再执行一步(`agent/turn-stopping`)。仅提供上下文的 hook 总是先通过 `next()` 委托,再把带来源的消息折叠进下游决策,因此后续监听器仍可拒绝或改写;阻塞决策映射为 `deny`(`PreToolUse` 没有 `allow` 或 `ask`)。逐事件接线位于 [`src/index.ts`](src/index.ts)。
### 载荷与环境
diff --git a/packages/hooks/hooks-codex/src/index.ts b/packages/hooks/hooks-codex/src/index.ts
index 6c42a063e5..037252dcde 100644
--- a/packages/hooks/hooks-codex/src/index.ts
+++ b/packages/hooks/hooks-codex/src/index.ts
@@ -181,16 +181,16 @@ export function apply(ctx: Context, config: Config): void {
return [ours, ...theirs ?? []]
}
- // SessionStart injects plain stdout when its detached hook resolves; a slow
- // hook may miss the first request.
- // TODO(session-start-gating): add a startup gate before promising first-turn delivery.
- ctx.on('agent/session-start', ({ agent, source }) => {
- detached.track(runPoint('SessionStart', source, { ...base(agent, 'SessionStart', model), source }, { agent, plainStdoutAsContext: true, signal: detached.signal })
+ ctx.on('agent/created', async ({ agent, source, signal }) => {
+ const ownerSignal = signal === undefined ? detached.signal : AbortSignal.any([signal, detached.signal])
+ const run = runPoint('SessionStart', source, { ...base(agent, 'SessionStart', model), source }, { agent, plainStdoutAsContext: true, signal: ownerSignal })
.then((merged) => {
const context = contextFrom(merged)
if (context) agent.inject(context)
})
- .catch((error: unknown) => { ctx.logger.warn(`hooks-codex: SessionStart hook failed: ${String(error)}`) }))
+ .catch((error: unknown) => { ctx.logger.warn(`hooks-codex: SessionStart hook failed: ${String(error)}`) })
+ detached.track(run)
+ await run
/* jscpd:ignore-end */
})
diff --git a/packages/hooks/hooks-codex/tests/bridge.spec.ts b/packages/hooks/hooks-codex/tests/bridge.spec.ts
index 1ef87de7c1..a910f00e9b 100644
--- a/packages/hooks/hooks-codex/tests/bridge.spec.ts
+++ b/packages/hooks/hooks-codex/tests/bridge.spec.ts
@@ -22,7 +22,11 @@ import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent
*/
const dirs: string[] = []
-afterEach(() => { for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }) })
+const contexts: Context[] = []
+afterEach(async () => {
+ for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
+ for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
+})
function configDir(): string {
const dir = mkdtempSync(join(tmpdir(), 'dsh-hooks-codex-'))
@@ -41,6 +45,7 @@ function writeHooks(dir: string, hooks: unknown): void {
async function harness(dir: string, adapter: MockAdapter, beforeHooks?: (ctx: Context) => void): Promise {
const ctx = new Context()
+ contexts.push(ctx)
await mountAgentLoopTestDependencies(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LocalSubprocessRuntime)
@@ -66,6 +71,23 @@ async function waitFor(predicate: () => boolean, timeout = 5000, interval = 10):
}
describe('hooks-codex bridge', () => {
+ it('awaits a registry-announced resume hook without a creation signal', async () => {
+ const dir = configDir()
+ const capture = script(dir, 'resume.sh', '#!/usr/bin/env bash\necho "resumed context"\n')
+ writeHooks(dir, { SessionStart: [{ matcher: 'resume', hooks: [{ type: 'command', command: capture }] }] })
+ const ctx = await harness(dir, new MockAdapter([]))
+ const session = ctx.sessions.create(SessionId('registry-resume'))
+ const inject = vi.fn()
+ const agent = { id: session.id, session, ctx, inject } as unknown as Agent
+
+ ctx.effect(() => ctx.agents.enter(agent, undefined))
+ await ctx.agents.announce(agent, 'resume')
+
+ expect(inject).toHaveBeenCalledWith(expect.objectContaining({
+ content: [{ type: 'text', text: 'resumed context' }],
+ }))
+ })
+
it('a PreToolUse hook (exit 2) denies a tool the regex matcher matches as a substring', async () => {
const dir = configDir()
const deny = script(dir, 'deny.sh', '#!/usr/bin/env bash\necho "codex blocked it" >&2\nexit 2\n')
@@ -212,16 +234,20 @@ describe('hooks-codex bridge', () => {
ctx.llm.registerAdapter(['mock'], new MockAdapter([]))
const warn = vi.fn()
ctx.logger.warn = warn as never
- await ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) // fires agent/session-start
- await waitFor(() => existsSync(marker))
- const pid = Number(readFileSync(pidFile, 'utf8').trim())
- await fiber.dispose()
- // Disposal reaches quiescence only after the aborted run settles and the process is reaped, so
- // `kill(pid, 0)` must report ESRCH. Untracked fire-and-forget work would remain.
- expect(() => process.kill(pid, 0)).toThrow()
- // runHook resolves an aborted run as a non-blocking error, so draining must
- // not log a rejected continuation.
- expect(warn).not.toHaveBeenCalledWith(expect.stringContaining('SessionStart hook failed'))
+ const creating = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
+ try {
+ await waitFor(() => existsSync(marker))
+ const pid = Number(readFileSync(pidFile, 'utf8').trim())
+ await fiber.dispose()
+ await creating
+ // The aborted hook must be reaped before bridge disposal resolves.
+ expect(() => process.kill(pid, 0)).toThrow()
+ expect(warn).not.toHaveBeenCalledWith(expect.stringContaining('SessionStart hook failed'))
+ } finally {
+ await fiber.dispose()
+ await creating
+ await ctx.fiber.dispose()
+ }
})
it('has the namespace-plugin export shape (no stray default) so the Loader keeps name/inject/apply', () => {
diff --git a/packages/hooks/hooks-codex/tests/coverage-cases.ts b/packages/hooks/hooks-codex/tests/coverage-cases.ts
index 12a3c370ab..6eebc5e7c1 100644
--- a/packages/hooks/hooks-codex/tests/coverage-cases.ts
+++ b/packages/hooks/hooks-codex/tests/coverage-cases.ts
@@ -18,7 +18,11 @@ import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent
const testToolSignal = new AbortController().signal
const dirs: string[] = []
-afterEach(() => { for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }) })
+const contexts: Context[] = []
+afterEach(async () => {
+ for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
+ for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
+})
function dir(): string { const d = mkdtempSync(join(tmpdir(), 'dsh-hx-cov-')); dirs.push(d); return d }
function sh(d: string, name: string, body: string): string {
const p = join(d, name); writeFileSync(p, body); chmodSync(p, 0o755); return p
@@ -30,6 +34,7 @@ function hooks(d: string, h: unknown): string {
type HarnessOpts = { stderrSummaryMaxChars?: number; sessionRoot?: string }
async function harness(configPath: string, adapter: MockAdapter, opts: HarnessOpts = {}): Promise {
const ctx = new Context()
+ contexts.push(ctx)
await mountAgentLoopTestDependencies(ctx)
if (opts.sessionRoot !== undefined) await ctx.plugin(JsonlSessionPersistence, { root: opts.sessionRoot })
await ctx.plugin(AgentLoop, { agents: [] })
@@ -353,10 +358,18 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(join(d, 'hooks.json'), adapter)
const warn = vi.fn(); ctx.logger.warn = warn as never
- const agent = await ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
- agent.inject = (() => { throw new Error('inject boom') })
- await waitFor(() => warn.mock.calls.some(c => String(c[0]).includes('SessionStart hook failed')))
- expect(warn).toHaveBeenCalledWith(expect.stringContaining('SessionStart hook failed'))
+ try {
+ await ctx.agents.create({
+ sessionId: SessionId('a1'),
+ agentOptions: { provider: 'mock', model: 'mock' },
+ setup(_agentCtx, agent) {
+ vi.spyOn(agent, 'inject').mockImplementationOnce(() => { throw new Error('inject boom') })
+ },
+ })
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('SessionStart hook failed'))
+ } finally {
+ await ctx.fiber.dispose()
+ }
})
})
diff --git a/packages/jobs/jobs-local/tests/jobs.spec.ts b/packages/jobs/jobs-local/tests/jobs.spec.ts
index 14f4ecd847..dafd91844f 100644
--- a/packages/jobs/jobs-local/tests/jobs.spec.ts
+++ b/packages/jobs/jobs-local/tests/jobs.spec.ts
@@ -135,8 +135,8 @@ describe('LocalJobRegistry.start', () => {
const served = stubAgent(ctx, 'served', scopeOf(withControls.ctx))
const unserved = stubAgent(ctx, 'unserved', scopeOf(withoutControls.ctx))
- ctx.agents.register(served)
- ctx.agents.register(unserved)
+ await ctx.agents.register(served)
+ await ctx.agents.register(unserved)
expect(() => ctx.jobs.start(producer({ owner: served }).spec)).not.toThrow()
expect(() => ctx.jobs.start(producer({ owner: unserved }).spec))
@@ -154,7 +154,7 @@ describe('LocalJobRegistry.start', () => {
// holds them and every owner's read includes it.
await attachControllerIn(ctx)
const scoped = stubAgent(ctx, 'scoped', scopeOf(createScope(ctx, {}).ctx))
- ctx.agents.register(scoped)
+ await ctx.agents.register(scoped)
expect(() => ctx.jobs.start(producer({ owner: scoped }).spec)).not.toThrow()
expect(() => ctx.jobs.start(producer().spec)).not.toThrow()
@@ -239,17 +239,17 @@ describe('LocalJobRegistry.start', () => {
it('isolates exact owners, replacement objects with the same session id, and the unowned bucket', async () => {
const ctx = await harness({ maxConcurrentJobsPerOwner: 1 })
const oldOwner = stubAgent(ctx, 'shared-session')
- const detachOld = ctx.agents.register(oldOwner)
+ const detachOld = await ctx.agents.register(oldOwner)
const oldTask = producer({ owner: oldOwner })
ctx.jobs.start(oldTask.spec)
const otherOwner = stubAgent(ctx, 'other-session')
- ctx.agents.register(otherOwner)
+ await ctx.agents.register(otherOwner)
expect(() => ctx.jobs.start(producer({ owner: otherOwner }).spec)).not.toThrow()
- detachOld()
+ await detachOld()
const replacement = stubAgent(ctx, 'shared-session')
- ctx.agents.register(replacement)
+ await ctx.agents.register(replacement)
expect(() => ctx.jobs.start(producer({ owner: replacement }).spec)).not.toThrow()
ctx.jobs.start(producer().spec)
@@ -561,7 +561,7 @@ describe('LocalJobRegistry owner isolation', () => {
it('fences read/kill/wait to the owning session and keeps unowned jobs open', async () => {
const ctx = await harness()
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const other = stubAgent(ctx, 'other')
const owned = ctx.jobs.start(producer({ owner }).spec)
@@ -582,8 +582,8 @@ describe('LocalJobRegistry owner isolation', () => {
const ctx = await harness()
const alice = stubAgent(ctx, 'alice')
const bob = stubAgent(ctx, 'bob')
- ctx.agents.register(alice)
- ctx.agents.register(bob)
+ await ctx.agents.register(alice)
+ await ctx.agents.register(bob)
const aliceTask = ctx.jobs.start(producer({ owner: alice }).spec)
const bobTask = ctx.jobs.start(producer({ owner: bob }).spec)
@@ -615,7 +615,7 @@ describe('LocalJobRegistry owner isolation', () => {
expect(ctx.jobs.list(ghost)).toEqual([])
// A later valid registration must still attach cleanup for the same object.
- ctx.agents.register(ghost)
+ await ctx.agents.register(ghost)
const cancels: (string | undefined)[] = []
let settle!: (outcome: JobOutcome) => void
const id = ctx.jobs.start({
@@ -636,11 +636,11 @@ describe('LocalJobRegistry owner isolation', () => {
it('rejects a stale owner instance after another agent reuses its id', async () => {
const ctx = await harness()
const staleOwner = stubAgent(ctx, 'owner')
- const unregisterStale = ctx.agents.register(staleOwner)
- unregisterStale()
+ const unregisterStale = await ctx.agents.register(staleOwner)
+ await unregisterStale()
const currentOwner = stubAgent(ctx, 'owner')
- ctx.agents.register(currentOwner)
+ await ctx.agents.register(currentOwner)
const current = producer({ owner: currentOwner })
ctx.jobs.start(current.spec) // Attach the current owner's cleanup first.
@@ -665,7 +665,7 @@ describe('LocalJobRegistry owner cleanup', () => {
it('drains the owner: cancels live jobs, awaits settlement, drops snapshots', async () => {
const ctx = await harness()
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
// The producer settles only when cancelled — models a child that stops on request.
let settle!: (outcome: JobOutcome) => void
@@ -693,7 +693,7 @@ describe('LocalJobRegistry owner cleanup', () => {
it('publishes the settled visible set before announcing completion', async () => {
const ctx = await harness()
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const p = producer({ owner })
ctx.jobs.start(p.spec)
// Registered after start so only the settlement's notifications are ordered.
@@ -713,7 +713,7 @@ describe('LocalJobRegistry owner cleanup', () => {
it('reports a teardown-cancelled record so completion reporters stay quiet', async () => {
const ctx = await harness()
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const seen: JobSnapshot[] = []
ctx.jobs.onJobDone(snapshot => void seen.push(snapshot))
@@ -738,7 +738,7 @@ describe('LocalJobRegistry owner cleanup', () => {
it('attaches one cleanup per owner and drains all owned jobs with the scope', async () => {
const ctx = await harness()
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const first = producer({ owner })
const second = producer({ owner })
@@ -755,7 +755,7 @@ describe('LocalJobRegistry owner cleanup', () => {
it('does not let an old scope cleanup cancel a same-id/session replacement job', async () => {
const ctx = await harness()
const oldOwner = stubAgent(ctx, 'owner')
- const detachOld = ctx.agents.register(oldOwner)
+ const detachOld = await ctx.agents.register(oldOwner)
const cancels: string[] = []
function start(owner: Agent, label: string): JobId {
@@ -772,9 +772,9 @@ describe('LocalJobRegistry owner cleanup', () => {
}
start(oldOwner, 'old job')
- detachOld()
+ await detachOld()
const replacement = stubAgent(ctx, 'owner')
- ctx.agents.register(replacement)
+ await ctx.agents.register(replacement)
const replacementId = start(replacement, 'replacement job')
await disposeAgentScope(oldOwner)
@@ -791,7 +791,7 @@ describe('LocalJobRegistry owner cleanup', () => {
const tasksFiber = await ctx.plugin(LocalJobRegistry)
ctx.jobs.attachController('test-controller')
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const ownerCleanupEffects = () => owner.ctx.fiber.getEffects()
.filter(effect => effect.label === 'jobs.ownerCleanup()')
@@ -815,7 +815,7 @@ describe('LocalJobRegistry owner cleanup', () => {
const ctx = await harness()
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const seen: JobSnapshot[] = []
ctx.jobs.onJobDone(snapshot => void seen.push(snapshot))
@@ -927,7 +927,7 @@ describe('LocalJobRegistry disposal', () => {
const tasksFiber = await ctx.plugin(LocalJobRegistry)
ctx.jobs.attachController('test-controller')
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
let settle!: (outcome: JobOutcome) => void
ctx.jobs.start({
kind: 'bash',
@@ -962,7 +962,7 @@ describe('LocalJobRegistry disposal', () => {
},
})
const owner = stubAgent(ctx, 'joined', scopeOf(standing.ctx))
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
expect(() => ctx.jobs.start(producer({ owner }).spec)).not.toThrow()
await mount.dispose()
@@ -994,7 +994,7 @@ describe('LocalJobRegistry.onJobsChanged', () => {
it('fires after registration, the stopping transition, and settlement', async () => {
const ctx = await harness()
const owner = stubAgent(ctx, 'alice')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const seen: (string | undefined)[] = []
ctx.jobs.onJobsChanged(changed => void seen.push(changed?.id))
@@ -1028,8 +1028,8 @@ describe('LocalJobRegistry.onJobsChanged', () => {
const ctx = await harness()
const owner = stubAgent(ctx, 'alice')
const bystander = stubAgent(ctx, 'bob')
- ctx.agents.register(owner)
- ctx.agents.register(bystander)
+ await ctx.agents.register(owner)
+ await ctx.agents.register(bystander)
const p = producer({ owner })
ctx.jobs.start(p.spec)
@@ -1087,7 +1087,7 @@ describe('LocalJobRegistry teardown change notifications', () => {
it('announces the stopping transition during owner teardown, before settlement', async () => {
const ctx = await harness()
const owner = stubAgent(ctx, 'alice')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const p = producer({ owner })
const id = ctx.jobs.start(p.spec)
diff --git a/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts b/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts
index d146cab224..6f32878207 100644
--- a/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts
+++ b/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts
@@ -16,7 +16,7 @@ import { statusLine } from '@deepseek-ai/dsh-tool-jobs'
const testToolSignal = new AbortController().signal
-const agentRegistryDisposers = new WeakMap void>()
+const agentRegistryDisposers = new WeakMap Promise>()
const agentScopeFibers = new WeakMap Promise }>()
async function setup(config: ToolJobs.Config = {}) {
@@ -41,7 +41,7 @@ interface FakeDelivery {
* A fake agent with the shared agent/session identity, registered in
* `ctx.agents` with a dedicated lifecycle scope.
*/
-function fakeAgent(ctx: Context, sessionId: string, delivery: FakeDelivery = {}): Agent {
+async function fakeAgent(ctx: Context, sessionId: string, delivery: FakeDelivery = {}): Promise {
const scopeFiber = ctx.plugin(() => {})
const id = SessionId(sessionId)
const agent = {
@@ -52,15 +52,15 @@ function fakeAgent(ctx: Context, sessionId: string, delivery: FakeDelivery = {})
status: delivery.status ?? 'running',
session: { id, header: { version: 0, id, createdAt: 0 } },
} as unknown as Agent
- agentRegistryDisposers.set(agent, ctx.agents.register(agent))
+ agentRegistryDisposers.set(agent, await ctx.agents.register(agent))
agentScopeFibers.set(agent, scopeFiber)
return agent
}
-function detachAgent(agent: Agent): void {
+async function detachAgent(agent: Agent): Promise {
const dispose = agentRegistryDisposers.get(agent)
if (dispose === undefined) throw new Error(`missing registry disposer for agent "${agent.id}"`)
- dispose()
+ await dispose()
}
/** Dispose the agent's own lifecycle scope, which is what drains its owned jobs. */
@@ -353,7 +353,7 @@ describe('job_list', () => {
const { ctx } = await setup()
expect(text(await call(ctx, 'job_list', {}))).toBe('(no background jobs)')
- const alice = fakeAgent(ctx, 'sess-alice')
+ const alice = await fakeAgent(ctx, 'sess-alice')
ctx.jobs.start(producer({ owner: alice, label: 'pnpm test' }).spec)
ctx.jobs.start(producer({ kind: 'subagent', label: 'open research' }).spec)
const p = producer({ owner: alice, label: 'build' })
@@ -377,7 +377,7 @@ describe('job_list', () => {
'bash-2 [bash] completed — build',
].join('\n'))
// A different caller sees only the unowned job.
- const bob = fakeAgent(ctx, 'sess-bob')
+ const bob = await fakeAgent(ctx, 'sess-bob')
expect(text(await call(ctx, 'job_list', {}, bob))).toBe('subagent-1 [subagent] running — open research')
})
})
@@ -536,7 +536,7 @@ describe('completion notices across scoped mounts', () => {
inject,
session: { id: SessionId('sess-scoped'), header: { version: 0, id: SessionId('sess-scoped'), createdAt: 0 } },
} as unknown as Agent
- const dispose = ctx.agents.register(owner)
+ const dispose = await ctx.agents.register(owner)
try {
// No waiter: `settle()` leaves `reported` false, which is the only path
@@ -548,7 +548,7 @@ describe('completion notices across scoped mounts', () => {
expect(inject).toHaveBeenCalledTimes(1)
} finally {
- dispose()
+ await dispose()
}
})
})
@@ -558,7 +558,7 @@ describe('completion notice delivery', () => {
const { ctx } = await setup()
const inject = vi.fn()
const followup = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { inject, followup, status: 'idle' })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject, followup, status: 'idle' })
const p = producer({ owner, label: 'pnpm test' })
ctx.jobs.start(p.spec)
@@ -572,7 +572,7 @@ describe('completion notice delivery', () => {
const { ctx } = await setup({ completionDelivery: 'quiet' })
const inject = vi.fn()
const followup = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { inject, followup, status: 'idle' })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject, followup, status: 'idle' })
const p = producer({ owner })
ctx.jobs.start(p.spec)
@@ -586,7 +586,7 @@ describe('completion notice delivery', () => {
const { ctx } = await setup({ maxConsecutiveWakes: 2 })
const inject = vi.fn()
const followup = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { inject, followup, status: 'idle' })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject, followup, status: 'idle' })
await settleTasks(ctx, owner, 3)
// A woken turn that starts another job is the self-exciting case: the
@@ -599,7 +599,7 @@ describe('completion notice delivery', () => {
const { ctx } = await setup({ maxConsecutiveWakes: 1 })
const inject = vi.fn()
const followup = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { inject, followup, status: 'idle' })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject, followup, status: 'idle' })
await settleTasks(ctx, owner, 2)
expect(followup).toHaveBeenCalledTimes(1)
@@ -616,7 +616,7 @@ describe('completion notice delivery', () => {
const { ctx } = await setup()
const inject = vi.fn()
const followup = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { inject, followup, status: 'idle' })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject, followup, status: 'idle' })
let settle!: (outcome: JobOutcome) => void
ctx.jobs.start({
kind: 'bash',
@@ -641,7 +641,7 @@ describe('completion notice delivery', () => {
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
const inject = vi.fn()
const followup = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { inject, followup, status: 'idle' })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject, followup, status: 'idle' })
ctx.jobs.start({
kind: 'bash',
label: 'broken producer',
@@ -666,7 +666,7 @@ describe('completion notice delivery', () => {
it('keeps the budget spent when the owner only claims plugin notices', async () => {
const { ctx } = await setup({ maxConsecutiveWakes: 1 })
const followup = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { followup, status: 'idle' })
+ const owner = await fakeAgent(ctx, 'sess-1', { followup, status: 'idle' })
await settleTasks(ctx, owner, 1)
emitAgentEvent(ctx, owner, 'agent/inbox/claimed', {
@@ -685,7 +685,7 @@ describe('completion notices', () => {
it('injects a notice into the owning agent when an unreported job settles', async () => {
const { ctx } = await setup()
const inject = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { inject })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject })
const p = producer({ owner, label: 'pnpm test' })
ctx.jobs.start(p.spec)
@@ -708,7 +708,7 @@ describe('completion notices', () => {
it('preserves job ids and collection guidance in bounded completion notices', async () => {
const { ctx } = await setup()
const inject = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { inject })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject })
const first = producer({
owner,
kind: 'subagent',
@@ -762,7 +762,7 @@ describe('completion notices', () => {
await tick()
}
const inject = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { inject })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject })
const target = producer({
owner,
kind: 'pty-send',
@@ -783,7 +783,7 @@ describe('completion notices', () => {
it('reserves the collection-action tail when a producer supplies a smaller budget', async () => {
const { ctx } = await setup()
const inject = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { inject })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject })
const tiny = producer({ owner, kind: 'pty-send', label: 'x'.repeat(100), outputLimitBytes: 8 })
const short = producer({ owner, kind: 'pty-send', label: 'x'.repeat(100), outputLimitBytes: 32 })
ctx.jobs.start(tiny.spec)
@@ -804,7 +804,7 @@ describe('completion notices', () => {
it('suppresses the notice for a job the model already killed', async () => {
const { ctx } = await setup()
const inject = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { inject })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject })
const p = producer({ owner })
ctx.jobs.start(p.spec)
@@ -817,7 +817,7 @@ describe('completion notices', () => {
it('suppresses the notice when a wait returned the terminal state', async () => {
const { ctx } = await setup()
const inject = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { inject })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject })
const p = producer({ owner, kind: 'subagent' })
ctx.jobs.start(p.spec)
@@ -842,13 +842,13 @@ describe('completion notices', () => {
// terminal state, so the notice lands in the old owner's (detached)
// session instead of throwing or re-routing.
const oldInject = vi.fn()
- const oldOwner = fakeAgent(ctx, 'shared', { inject: oldInject })
+ const oldOwner = await fakeAgent(ctx, 'shared', { inject: oldInject })
const p = producer({ owner: oldOwner })
ctx.jobs.start(p.spec)
- detachAgent(oldOwner)
+ await detachAgent(oldOwner)
const replacementInject = vi.fn()
- fakeAgent(ctx, 'shared', { inject: replacementInject })
+ await fakeAgent(ctx, 'shared', { inject: replacementInject })
p.settle({ status: 'completed' })
await tick()
@@ -859,7 +859,7 @@ describe('completion notices', () => {
it('surfaces an inject failure through listener containment (a real bug must be visible)', async () => {
const { ctx } = await setup()
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
- const owner = fakeAgent(ctx, 'sess-1', { inject: () => { throw new Error('unexpected inject bug') } })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject: () => { throw new Error('unexpected inject bug') } })
const p = producer({ owner })
ctx.jobs.start(p.spec)
p.settle({ status: 'completed' })
@@ -872,7 +872,7 @@ describe('completion notices', () => {
it('keeps using the exact owner after the agent registry is gone', async () => {
const { ctx, agentsFiber } = await setup()
const inject = vi.fn()
- const owner = fakeAgent(ctx, 'sess-1', { inject })
+ const owner = await fakeAgent(ctx, 'sess-1', { inject })
// Settlement must not depend on a later registry lookup: the exact owner
// supplied at start remains the destination while its own scope is live.
diff --git a/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts b/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts
index d09a22a02f..1faf55e7a6 100644
--- a/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts
+++ b/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts
@@ -119,7 +119,7 @@ describe('DeepSeek plugin package inventory', () => {
const id = SessionId('bare-agent')
const agentScope = createScope(ctx, {})
- ctx.agents.register({ id, ctx: agentScope.ctx, session: { id } } as unknown as Agent)
+ await ctx.agents.register({ id, ctx: agentScope.ctx, session: { id } } as unknown as Agent)
const bare = await ctx.deepseekLlmApiExtensions.prepare({ body: { messages: [] }, signal: SIGNAL, sessionId: id })
expect(bare.fields.dsh_plugin_packages?.packages).toEqual([{ name: 'host-only', version: '3.0.0' }])
})
@@ -219,7 +219,7 @@ describe('DeepSeek plugin package inventory', () => {
const agentScope = createScope(ctx, agentKey, { parent: standingKey })
const id = SessionId('preset-agent')
const agent = { id, ctx: agentScope.ctx, session: { id } } as unknown as Agent
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
const prepared = await ctx.deepseekLlmApiExtensions.prepare({ body: { messages: [] }, signal: SIGNAL, sessionId: id })
expect(prepared.fields.dsh_plugin_packages?.packages).toEqual([{ name: 'preset-only', version: '4.0.0' }])
diff --git a/packages/plan/plan-mode/tests/plan-mode.spec.ts b/packages/plan/plan-mode/tests/plan-mode.spec.ts
index 5bfa464fb1..313a3c9192 100644
--- a/packages/plan/plan-mode/tests/plan-mode.spec.ts
+++ b/packages/plan/plan-mode/tests/plan-mode.spec.ts
@@ -63,10 +63,10 @@ async function agentWithSession(
// fold-only benches retain the direct lifecycle event used before it exists.
const agents = ctx.get('agents')
if (agents === undefined) {
- ctx.emit('agent/created', { agent })
+ await ctx.serial('agent/created', { agent, source: 'startup' })
} else {
agents.enter(agent, owner)
- agents.announce(agent)
+ await agents.announce(agent, 'startup')
}
return agent
}
diff --git a/packages/plan/plan-mode/tests/projection.spec.ts b/packages/plan/plan-mode/tests/projection.spec.ts
index f4d46d5b62..3c2fc36b5c 100644
--- a/packages/plan/plan-mode/tests/projection.spec.ts
+++ b/packages/plan/plan-mode/tests/projection.spec.ts
@@ -29,7 +29,7 @@ async function harness(withPlanMode: boolean): Promise {
await ctx.plugin(SessionProjectionRegistry)
if (withPlanMode) await ctx.plugin(PlanModeController, { section: 'plan policy' })
const session = ctx.sessions.create()
- ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
+ await ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
return {
ctx,
session,
diff --git a/packages/schedule/schedule/tests/runtime.spec.ts b/packages/schedule/schedule/tests/runtime.spec.ts
index f216babd7d..5c415c7c6b 100644
--- a/packages/schedule/schedule/tests/runtime.spec.ts
+++ b/packages/schedule/schedule/tests/runtime.spec.ts
@@ -97,7 +97,10 @@ async function harness(): Promise {
steer(_message: UserMessage) {},
inject(_message: UserMessage) {},
}
- const disposeAgent = ctx.agents.register(agent)
+ // Dispatch callbacks remove registry visibility synchronously while the schedule is running.
+ const disposeAgent = ctx.agents.enter(agent, undefined)
+ ctx.effect(() => disposeAgent)
+ await ctx.agents.announce(agent, 'startup')
ctx.on('session/event', (_session, event) => {
if (event.type === 'schedule/change' && event.data.operation === 'dispatch') order.push('dispatch')
})
diff --git a/packages/schedule/schedule/tests/tools.spec.ts b/packages/schedule/schedule/tests/tools.spec.ts
index 35e4dc1724..a34281c1b5 100644
--- a/packages/schedule/schedule/tests/tools.spec.ts
+++ b/packages/schedule/schedule/tests/tools.spec.ts
@@ -51,7 +51,7 @@ async function harness(withPersistence = true): Promise {
await ctx.plugin(SystemPrompt, {})
await ctx.plugin(ToolRuntime)
const agent = stubAgent(ctx, `schedule-tools-${Math.random()}`)
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
const flushes = { count: 0, outcomes: [] as Array<'resolve' | 'reject' | Promise<'resolve' | 'reject'>> }
if (withPersistence) {
ctx.on('session/flush', async () => {
@@ -550,7 +550,7 @@ describe('Schedule persistence failure boundaries', () => {
stop()
const other = stubAgent(test.ctx, `other-${Math.random()}`)
- test.ctx.agents.register(other)
+ await test.ctx.agents.register(other)
expect(value(await execute(test, 'schedule_create', { prompt: 'x', after_seconds: 1 }, other)))
.toEqual({ code: 'internal_error', message: 'The schedule operation failed.' })
expect(value(await execute(test, 'schedule_list', {}, other)))
diff --git a/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts b/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts
index 3ff22bb6f3..ea3688fefb 100644
--- a/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts
+++ b/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts
@@ -38,7 +38,7 @@ class PassthroughSandbox extends SandboxProvider {
}
}
-function agent(ctx: Context, cwd: string): Agent {
+async function agent(ctx: Context, cwd: string): Promise {
const id = SessionId('persistent-bash-loader-agent')
const scope = ctx.plugin(() => {})
const session = Session.create(id, [], {
@@ -59,7 +59,7 @@ function agent(ctx: Context, cwd: string): Agent {
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
- ctx.agents.register(value)
+ await ctx.agents.register(value)
return value
}
@@ -129,7 +129,7 @@ suite('persistent Bash through a real cordis.yml Loader composition', () => {
await context.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } })
await context.loader.await()
- const owner = agent(context, root)
+ const owner = await agent(context, root)
const signal = new AbortController().signal
const execute = (id: string, command: string) => context!.tools.execute({
signal,
diff --git a/packages/shell/tool-bash-persistent/tests/tools.spec.ts b/packages/shell/tool-bash-persistent/tests/tools.spec.ts
index 7f3ed90c52..ce453d18ce 100644
--- a/packages/shell/tool-bash-persistent/tests/tools.spec.ts
+++ b/packages/shell/tool-bash-persistent/tests/tools.spec.ts
@@ -27,7 +27,7 @@ afterEach(async () => {
for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
})
-function agent(ctx: Context, cwd: string | undefined): Agent {
+async function agent(ctx: Context, cwd: string | undefined): Promise {
const id = SessionId(`persistent-bash-owner-${callNumber}`)
const scope = ctx.plugin(() => {})
const session = Session.create(id, [], {
@@ -52,7 +52,7 @@ function agent(ctx: Context, cwd: string | undefined): Agent {
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
- ctx.agents.register(value)
+ await ctx.agents.register(value)
return value
}
@@ -309,7 +309,7 @@ async function setup(
const stub = stubBackend(initialMode)
ctx.terminals.registerBackend(stub.backend)
const fiber = await ctx.plugin(ToolBashPersistent, config)
- return { ctx, stub, fiber, owner: agent(ctx, '/workspace') }
+ return { ctx, stub, fiber, owner: await agent(ctx, '/workspace') }
}
describe('tool-bash-persistent', () => {
@@ -333,7 +333,7 @@ describe('tool-bash-persistent', () => {
expect(stub.sessions).toHaveLength(1)
expect(stub.sessions[0]?.sends).toBe(3)
- const ownerWithoutCwd = agent(ctx, undefined)
+ const ownerWithoutCwd = await agent(ctx, undefined)
expect(text(await call(ctx, ownerWithoutCwd, 'pwd'))).toBe('hello from stub\n[Command finished with exit code 0]')
expect(stub.sessions).toHaveLength(2)
@@ -569,7 +569,7 @@ describe('tool-bash-persistent', () => {
}),
})
const fiber = await ctx.plugin(ToolBashPersistent, { backendType: 'slow' })
- const owner = agent(ctx, '/workspace')
+ const owner = await agent(ctx, '/workspace')
const running = call(ctx, owner, 'pwd')
await spawnStarted.promise
await fiber.dispose()
diff --git a/packages/shell/tool-bash/tests/tools.spec.ts b/packages/shell/tool-bash/tests/tools.spec.ts
index 81fc5a3b27..1b7ab406e7 100644
--- a/packages/shell/tool-bash/tests/tools.spec.ts
+++ b/packages/shell/tool-bash/tests/tools.spec.ts
@@ -67,7 +67,7 @@ async function setupWithJobs() {
* Build a fake {@link Agent} with the shared agent/session identity, give it a
* dedicated lifecycle fiber for `Agent.ctx`, and register it in `ctx.agents`.
*/
-function registerFakeAgent(ctx: Context, sessionId: string, inject: (...args: unknown[]) => void = () => {}): Agent {
+async function registerFakeAgent(ctx: Context, sessionId: string, inject: (...args: unknown[]) => void = () => {}): Promise {
const scopeFiber = ctx.plugin(() => {})
const id = SessionId(sessionId)
const agent = {
@@ -76,7 +76,7 @@ function registerFakeAgent(ctx: Context, sessionId: string, inject: (...args: un
inject,
session: { id, header: { version: 0, id, createdAt: 0 } },
} as unknown as Agent
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
return agent
}
let callCounter = 0
@@ -493,7 +493,7 @@ describe('background execution through the job runtime', () => {
it('a background job started by an agent is registered with that agent as owner', async () => {
// The producer must forward exec.agent as the job owner.
const ctx = await setupWithJobs()
- const agent = registerFakeAgent(ctx, 'sess-owner')
+ const agent = await registerFakeAgent(ctx, 'sess-owner')
const started = await call(ctx, 'bash', { command: 'sleep 60', description: 'test command', run_in_background: true }, agent)
expect(text(started)).toBe('started background job bash-1')
@@ -663,7 +663,7 @@ describe('sandbox escalation through the generic task producer', () => {
const { ctx, bash } = await setupSandboxed(true)
ctx.on('approval/request', () => Promise.resolve('allowed-once'))
const agent = sandboxAgent(undefined, ctx)
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
const foreground = await ctx.tools.execute({
callId: ToolCallId('sandbox-signal'),
name: 'bash',
@@ -683,7 +683,7 @@ describe('sandbox escalation through the generic task producer', () => {
const agent = sandboxAgent(undefined, ctx, (type) => {
if (type === 'approval/decided') controller.abort()
})
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
ctx.on('approval/request', () => Promise.resolve('allowed-once'))
const start = vi.spyOn(bash, 'start')
@@ -1140,7 +1140,7 @@ describe('the model-facing bash tool builds its request from named args only (no
it('injects built-ins and the stable session id into a foreground request', async () => {
const { ctx, bash } = await setupRecording()
- const agent = registerFakeAgent(ctx, 'request-fg', () => undefined)
+ const agent = await registerFakeAgent(ctx, 'request-fg', () => undefined)
const ambient = process.env.DSH_SESSION_ID
await ctx.tools.execute({
@@ -1161,7 +1161,7 @@ describe('the model-facing bash tool builds its request from named args only (no
it('injects the same trusted variables into a background request without forwarding model env', async () => {
const { ctx, bash } = await setupRecording()
- const agent = registerFakeAgent(ctx, 'request-bg', () => undefined)
+ const agent = await registerFakeAgent(ctx, 'request-bg', () => undefined)
await ctx.tools.execute({
signal: testToolSignal,
@@ -1186,8 +1186,8 @@ describe('the model-facing bash tool builds its request from named args only (no
it('keeps parent and child agent session environments isolated', async () => {
const { ctx, bash } = await setupRecording()
- const parent = registerFakeAgent(ctx, 'request-parent', () => undefined)
- const child = registerFakeAgent(ctx, 'request-child', () => undefined)
+ const parent = await registerFakeAgent(ctx, 'request-parent', () => undefined)
+ const child = await registerFakeAgent(ctx, 'request-child', () => undefined)
for (const [callId, agent] of [['parent', parent], ['child', child]] as const) {
await ctx.tools.execute({
diff --git a/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts b/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts
index 36e722ff5b..eacd51d586 100644
--- a/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts
+++ b/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts
@@ -45,7 +45,7 @@ class PassthroughSandbox extends SandboxProvider {
}
}
-function agent(ctx: Context, cwd: string): Agent {
+async function agent(ctx: Context, cwd: string): Promise {
const id = SessionId('persistent-pwsh-loader-agent')
const scope = ctx.plugin(() => {})
const session = Session.create(id, [], {
@@ -66,7 +66,7 @@ function agent(ctx: Context, cwd: string): Agent {
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
- ctx.agents.register(value)
+ await ctx.agents.register(value)
return value
}
@@ -140,7 +140,7 @@ describe.skipIf(!hasPwsh)('persistent pwsh through a real cordis.yml Loader comp
await context.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } })
await context.loader.await()
- const owner = agent(context, root)
+ const owner = await agent(context, root)
const signal = new AbortController().signal
const execute = (id: string, command: string) => context!.tools.execute({
signal,
diff --git a/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts b/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts
index e65859e4c8..54d88197e8 100644
--- a/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts
+++ b/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts
@@ -27,7 +27,7 @@ afterEach(async () => {
for (const ctx of contexts.splice(0)) await ctx.fiber.dispose()
})
-function agent(ctx: Context, cwd: string | undefined): Agent {
+async function agent(ctx: Context, cwd: string | undefined): Promise {
const id = SessionId(`persistent-pwsh-owner-${callNumber}`)
const scope = ctx.plugin(() => {})
const session = Session.create(id, [], {
@@ -52,7 +52,7 @@ function agent(ctx: Context, cwd: string | undefined): Agent {
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
- ctx.agents.register(value)
+ await ctx.agents.register(value)
return value
}
@@ -323,7 +323,7 @@ async function setup(
const stub = stubBackend(initialMode)
ctx.terminals.registerBackend(stub.backend)
const fiber = await ctx.plugin(ToolPwshPersistent, config)
- return { ctx, stub, fiber, owner: agent(ctx, '/workspace') }
+ return { ctx, stub, fiber, owner: await agent(ctx, '/workspace') }
}
describe('tool-pwsh-persistent', () => {
@@ -347,7 +347,7 @@ describe('tool-pwsh-persistent', () => {
expect(stub.sessions).toHaveLength(1)
expect(stub.sessions[0]?.sends).toBe(3)
- const ownerWithoutCwd = agent(ctx, undefined)
+ const ownerWithoutCwd = await agent(ctx, undefined)
expect(text(await call(ctx, ownerWithoutCwd, 'pwd'))).toBe('hello from stub')
expect(stub.sessions).toHaveLength(2)
@@ -603,7 +603,7 @@ describe('tool-pwsh-persistent', () => {
}),
})
const fiber = await ctx.plugin(ToolPwshPersistent, { backendType: 'slow' })
- const owner = agent(ctx, '/workspace')
+ const owner = await agent(ctx, '/workspace')
const running = call(ctx, owner, 'pwd')
await spawnStarted.promise
await fiber.dispose()
diff --git a/packages/shell/tool-pwsh/tests/tools.spec.ts b/packages/shell/tool-pwsh/tests/tools.spec.ts
index 98a86121be..b0895adc7e 100644
--- a/packages/shell/tool-pwsh/tests/tools.spec.ts
+++ b/packages/shell/tool-pwsh/tests/tools.spec.ts
@@ -289,7 +289,7 @@ function sandboxAgent(
* The fake session carries an empty event log (the sandbox-policy resolver
* folds the log for mode overrides, mirroring a real session).
*/
-function registerFakeAgent(ctx: Context, sessionId: string): Agent {
+async function registerFakeAgent(ctx: Context, sessionId: string): Promise {
const scopeFiber = ctx.plugin(() => {})
const id = SessionId(sessionId)
const agent = {
@@ -305,7 +305,7 @@ function registerFakeAgent(ctx: Context, sessionId: string): Agent {
snapshotEvents: () => [],
},
} as unknown as Agent
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
return agent
}
@@ -397,7 +397,7 @@ describe('execution through the bash seam', () => {
tempDirs.push(dshHome)
const { ctx, bash } = await setup({}, dshHome)
bash.handler = () => runResult('hi\n')
- const agent = registerFakeAgent(ctx, 'session-1')
+ const agent = await registerFakeAgent(ctx, 'session-1')
Object.assign(agent.session.header, { cwd: '/sessions/s1' })
const result = await call(ctx, 'pwsh', {
command: 'Write-Output hi',
@@ -420,7 +420,7 @@ describe('execution through the bash seam', () => {
it('resolves a relative workdir against the session cwd, absolute ones verbatim', async () => {
const { ctx, bash } = await setup()
bash.handler = () => runResult('ok\n')
- const agent = registerFakeAgent(ctx, 'session-cwd')
+ const agent = await registerFakeAgent(ctx, 'session-cwd')
Object.assign(agent.session.header, { cwd: '/sessions/s1' })
await call(ctx, 'pwsh', { command: 'pwd', description: 'cwd', workdir: 'sub/dir' }, agent)
expect(bash.requests[0]?.workdir).toBe(resolvePath('/sessions/s1', 'sub/dir'))
@@ -546,7 +546,7 @@ describe('per-call sandbox policy resolution', () => {
const { ctx, bash } = await setupSandboxed()
const sessionCwd = mkdtempSync(join(tmpdir(), 'dsh-tool-pwsh-policy-'))
tempDirs.push(sessionCwd)
- const agent = registerFakeAgent(ctx, 'policy-session')
+ const agent = await registerFakeAgent(ctx, 'policy-session')
Object.assign(agent.session.header, { cwd: sessionCwd })
const result = await call(ctx, 'pwsh', { command: 'Write-Output hi', description: 'say hi' }, agent)
expect(result.isError).toBe(false)
@@ -666,7 +666,7 @@ describe('sandbox escalation through ctx.approval', () => {
const { ctx, bash } = await setupSandboxed(true)
ctx.on('approval/request', () => Promise.resolve('allowed-once'))
const agent = sandboxAgent(undefined, ctx)
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
const foreground = await ctx.tools.execute({
callId: ToolCallId('sandbox-signal'),
name: 'pwsh',
@@ -686,7 +686,7 @@ describe('sandbox escalation through ctx.approval', () => {
const agent = sandboxAgent(undefined, ctx, (type) => {
if (type === 'approval/decided') controller.abort()
})
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
ctx.on('approval/request', () => Promise.resolve('allowed-once'))
const start = vi.spyOn(bash, 'start')
@@ -769,7 +769,7 @@ describe('background execution through the job runtime', () => {
it('a background job started by an agent is registered with that agent as owner', async () => {
const { ctx } = await setupWithJobs()
- const agent = registerFakeAgent(ctx, 'sess-owner')
+ const agent = await registerFakeAgent(ctx, 'sess-owner')
const started = await call(ctx, 'pwsh', { command: 'Start-Sleep -Seconds 60', description: 'test command', run_in_background: true }, agent)
expect(text(started)).toBe('started background job pwsh-1')
diff --git a/packages/subagent/subagent-spawn-in-process/tests/subagent-spawn-in-process.spec.ts b/packages/subagent/subagent-spawn-in-process/tests/subagent-spawn-in-process.spec.ts
index b678f5c263..5df47ab30e 100644
--- a/packages/subagent/subagent-spawn-in-process/tests/subagent-spawn-in-process.spec.ts
+++ b/packages/subagent/subagent-spawn-in-process/tests/subagent-spawn-in-process.spec.ts
@@ -195,7 +195,6 @@ describe('dsh-subagent-spawn-in-process', () => {
const published: string[] = []
ctx.on('session/created', () => void published.push('session/created'))
ctx.on('agent/created', () => void published.push('agent/created'))
- ctx.on('agent/session-start', () => void published.push('agent/session-start'))
ctx.on('subagent/start', () => void published.push('subagent/start'))
ctx.on('subagent/end', () => void published.push('subagent/end'))
const controller = new AbortController()
@@ -469,7 +468,6 @@ describe('dsh-subagent-spawn-in-process', () => {
const published: string[] = []
ctx.on('session/created', () => void published.push('session/created'))
ctx.on('agent/created', () => void published.push('agent/created'))
- ctx.on('agent/session-start', () => void published.push('agent/session-start'))
await expect(start(ctx, 'spawn', {
prompt: [{ type: 'text', text: 'do X' }],
parent: parentHandle.agent,
@@ -488,7 +486,6 @@ describe('dsh-subagent-spawn-in-process', () => {
const published: string[] = []
ctx.on('session/created', () => void published.push('session/created'))
ctx.on('agent/created', () => void published.push('agent/created'))
- ctx.on('agent/session-start', () => void published.push('agent/session-start'))
let teardownStarted = false
ctx.on('internal/plugin', (fiber) => {
if (teardownStarted || fiber.name !== 'scope') return
diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml
index c014c37dc2..73c067942e 100644
--- a/packages/subagent/tool-subagent/README.i18n.yaml
+++ b/packages/subagent/tool-subagent/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/tool-subagent/README.md
-README.md: 97bb9b143f003c830b68ffae9e25dee588a8fa67
-README.zh.md: 34126d818a211ada44b66e8dd5a3d98d12f33538
+README.md: f5ae099747ac1d6177df24ba90c61f17c3f7d05e
+README.zh.md: c9c962ad6773403e63c252e6ae321f02af476440
diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md
index 97bb9b143f..f5ae099747 100644
--- a/packages/subagent/tool-subagent/README.md
+++ b/packages/subagent/tool-subagent/README.md
@@ -80,7 +80,7 @@ This section explains how the tool mirrors provider lifecycle and settles runs;
### Design concept
-One instance is one provider plus one tool name. The plugin mirrors provider lifecycle: it registers the tool when the named provider appears and disposes it when the provider leaves, so sibling load order and HMR replacement cannot strand a dangling tool. Direct Agent setup passes its unpublished Session explicitly and awaits installation before publication. A settings-backed standing preset receives each matching Agent from lifecycle events, selects policy from its Session, and installs through its Context. A numeric `maxDepth` or configured LLM selection the provider cannot enforce fails the mount instead of the first delegation. At most one instance in a tool scope may own model selection because `list_subagent_models` has a global name.
+One instance is one provider plus one tool name. The plugin mirrors provider lifecycle: it registers the tool when the named provider appears and disposes it when the provider leaves, so sibling load order and HMR replacement cannot strand a dangling tool. Direct Agent setup passes its unpublished Session explicitly and awaits installation before publication. A settings-backed standing preset receives each matching Agent through `agent/created`, selects policy from its Session, and awaits installation through its Context; installation failure rejects creation. A numeric `maxDepth` or configured LLM selection the provider cannot enforce fails the mount instead of the first delegation. At most one instance in a tool scope may own model selection because `list_subagent_models` has a global name.
### Foreground settlement
diff --git a/packages/subagent/tool-subagent/README.zh.md b/packages/subagent/tool-subagent/README.zh.md
index 34126d818a..c9c962ad67 100644
--- a/packages/subagent/tool-subagent/README.zh.md
+++ b/packages/subagent/tool-subagent/README.zh.md
@@ -80,7 +80,7 @@ kind: "package-reference"
### 设计理念
-一个实例就是一个提供方加一个工具名称。插件镜像提供方生命周期:具名提供方出现时注册工具,提供方离开时释放工具,因此同级加载顺序与 HMR 替换不会让工具悬空。直接 Agent setup 显式传入尚未发布的 Session,并在发布前等待安装完成。由设置控制的常驻 preset 从生命周期事件接收每个匹配 Agent,从其 Session 选择策略,并通过其 Context 安装。提供方无法执行的数值型 `maxDepth` 或已配置 LLM 选择会在挂载时失败,而不是在首次委派时失败。每个工具作用域内最多一个实例可以拥有模型选择,因为 `list_subagent_models` 使用全局名称。
+一个实例就是一个提供方加一个工具名称。插件镜像提供方生命周期:具名提供方出现时注册工具,提供方离开时释放工具,因此同级加载顺序与 HMR 替换不会让工具悬空。直接 Agent setup 显式传入尚未发布的 Session,并在发布前等待安装完成。由设置控制的常驻 preset 通过 `agent/created` 接收每个匹配 Agent,从其 Session 选择策略,并等待通过其 Context 发起的安装;安装失败会拒绝创建。提供方无法执行的数值型 `maxDepth` 或已配置 LLM 选择会在挂载时失败,而不是在首次委派时失败。每个工具作用域内最多一个实例可以拥有模型选择,因为 `list_subagent_models` 使用全局名称。
### 前台结算
diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts
index 4e8cc4b6b1..cd61f1aa70 100644
--- a/packages/subagent/tool-subagent/src/index.ts
+++ b/packages/subagent/tool-subagent/src/index.ts
@@ -662,8 +662,10 @@ export function apply(ctx: Context, config: Config, session?: Session): void {
const installing = new WeakSet()
const belongsToComposition = (candidate: Agent): boolean =>
scopeChainOf(scopeOf(candidate.ctx)).includes(compositionScope)
- const installScoped = (candidate: Agent): void => {
- if (scopedInstalls.has(candidate) || installing.has(candidate)) return
+ const installScoped = (candidate: Agent): ReturnType | undefined => {
+ const existing = scopedInstalls.get(candidate)
+ if (existing !== undefined) return existing
+ if (installing.has(candidate)) return
// Reserve before the injected fiber runs: tool registration emits
// `tools/change` synchronously, which re-enters the reconciliation below.
installing.add(candidate)
@@ -677,6 +679,7 @@ export function apply(ctx: Context, config: Config, session?: Session): void {
installing.delete(candidate)
}
scopedInstalls.set(candidate, fiber)
+ return fiber
}
const removeScoped = (candidate: Agent): void => {
const fiber = scopedInstalls.get(candidate)
@@ -696,8 +699,8 @@ export function apply(ctx: Context, config: Config, session?: Session): void {
// The preset-scoped listener admits descendant Agents and installs the
// sampled tool definition in each Agent's own scope, so a later settings
// change cannot mutate a live session.
- ctx.on('agent/created', ({ agent: created }) => {
- installScoped(created)
+ ctx.on('agent/created', async ({ agent: created }) => {
+ await installScoped(created)
})
ctx.on('agent/disposed', ({ agent: disposed }) => { removeScoped(disposed) })
// Reparenting an Agent between standing presets changes its inherited tool
diff --git a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts
index f5921a3fab..d7a5923526 100644
--- a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts
+++ b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts
@@ -209,6 +209,30 @@ describe('SubagentModelSelectionConfig', () => {
await ctx.fiber.dispose()
})
+ it('installs one tool when recording the Session policy triggers a registry refresh', async () => {
+ const ctx = await boot()
+ try {
+ await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
+ enabled: true,
+ allowedModels: ALLOWED_MODELS,
+ })
+ const preset = modelSelectionPresets.get(ctx)!
+ ctx.on('session/event', (_session, event) => {
+ if (event.type === 'subagent/model-selection-policy') {
+ ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change')
+ }
+ })
+ const register = vi.spyOn(ctx.tools, 'register')
+
+ const agent = await createAgent(ctx, 'policy-refresh')
+
+ expect(selectable(ctx, agent)).toBe(true)
+ expect(register.mock.calls.filter(([definition]) => definition.name === 'subagent')).toHaveLength(1)
+ } finally {
+ await ctx.fiber.dispose()
+ }
+ })
+
it('rejects a forced route outside the Session policy before child creation', async () => {
const ctx = await boot()
await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, {
diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts
index d070fa00d8..b90312c259 100644
--- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts
+++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts
@@ -801,7 +801,7 @@ describe('dsh-tool-subagent', () => {
describe('dsh-tool-subagent background mode', () => {
/** A live parent with a dedicated scope fiber for structural task cleanup. */
- function ownerAgent(ctx: Context, sessionId: string, inject: (...args: unknown[]) => void = () => {}): Agent {
+ async function ownerAgent(ctx: Context, sessionId: string, inject: (...args: unknown[]) => void = () => {}): Promise {
const scopeFiber = ctx.plugin(() => {})
const id = SessionId(sessionId)
const agent = {
@@ -811,7 +811,7 @@ describe('dsh-tool-subagent background mode', () => {
options: {},
session: Session.create(id),
} as unknown as Agent
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
return agent
}
@@ -825,7 +825,7 @@ describe('dsh-tool-subagent background mode', () => {
it('keeps a continuable-capable provider one-shot when backgroundMode selects one-shot', async () => {
const ctx = await backgroundSetup({ provider: 'mock' })
- const parent = ownerAgent(ctx, 'sess-parent')
+ const parent = await ownerAgent(ctx, 'sess-parent')
let prepareCalls = 0
ctx.subagents.registerProvider({
name: 'resumable',
@@ -866,7 +866,7 @@ describe('dsh-tool-subagent background mode', () => {
it('returns a job id immediately and the answer is collected through job_output', async () => {
const ctx = await backgroundSetup({ provider: 'mock' }, { reply: 'background answer' })
- const parent = ownerAgent(ctx, 'sess-parent')
+ const parent = await ownerAgent(ctx, 'sess-parent')
const start = await callSubagent(ctx, { description: 'deep research', prompt: 'dig in', run_in_background: true }, { agent: parent })
expect(start.isError).toBe(false)
@@ -900,7 +900,7 @@ describe('dsh-tool-subagent background mode', () => {
diagnostic: 'Claude Code cancelled an unattended dialog',
stopReason: 'error',
})
- const parent = ownerAgent(ctx, 'sess-parent')
+ const parent = await ownerAgent(ctx, 'sess-parent')
const started = await ctx.tools.execute({
signal: testToolSignal,
@@ -933,7 +933,7 @@ describe('dsh-tool-subagent background mode', () => {
it('skips background startup when the tool signal is already aborted', async () => {
const ctx = await backgroundSetup({ provider: 'mock' })
- const parent = ownerAgent(ctx, 'sess-parent')
+ const parent = await ownerAgent(ctx, 'sess-parent')
const controller = new AbortController()
controller.abort()
const result = await callSubagent(ctx, { description: 'd', prompt: 'p', run_in_background: true }, { agent: parent, signal: controller.signal })
@@ -950,7 +950,7 @@ describe('dsh-tool-subagent background mode', () => {
provider: 'mock',
agentOptions: { provider: 'alpha', model: 'selected-model' },
})
- const parent = ownerAgent(ctx, 'sess-parent')
+ const parent = await ownerAgent(ctx, 'sess-parent')
const adapter = new MockAdapter([])
let releasePreflight!: () => void
const preflightGate = new Promise((resolve) => { releasePreflight = resolve })
@@ -1021,7 +1021,7 @@ describe('dsh-tool-subagent background mode', () => {
it('settles an asynchronous provider-start failure as a failed task', async () => {
const ctx = await backgroundSetup({ provider: 'mock' })
- const parent = ownerAgent(ctx, 'sess-parent')
+ const parent = await ownerAgent(ctx, 'sess-parent')
ctx.subagents.registerProvider({
name: 'broken-start',
capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -1050,7 +1050,7 @@ describe('dsh-tool-subagent background mode', () => {
it('kills a subagent task while provider readiness is still pending', async () => {
const ctx = await backgroundSetup({ provider: 'mock' })
- const parent = ownerAgent(ctx, 'sess-parent')
+ const parent = await ownerAgent(ctx, 'sess-parent')
ctx.subagents.registerProvider({
name: 'pending-start',
capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -1087,7 +1087,7 @@ describe('dsh-tool-subagent background mode', () => {
it('reports startup rollback failure after cancellation as a failed job', async () => {
const ctx = await backgroundSetup({ provider: 'mock' })
- const parent = ownerAgent(ctx, 'sess-parent')
+ const parent = await ownerAgent(ctx, 'sess-parent')
ctx.subagents.registerProvider({
name: 'broken-start-rollback',
capabilities: { agentOptions: false, outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -1130,7 +1130,7 @@ describe('dsh-tool-subagent background mode', () => {
it('forwards job_kill reasons through the run signal (and defaults one when absent)', async () => {
// Use a provider that remains live until its signal is aborted.
const ctx = await backgroundSetup({ provider: 'mock', agentOptions: { model: 'child-model' } })
- const parent = ownerAgent(ctx, 'sess-parent')
+ const parent = await ownerAgent(ctx, 'sess-parent')
const cancels: (string | undefined)[] = []
let starts = 0
ctx.subagents.registerProvider({
@@ -1354,7 +1354,7 @@ describe('background preflight failure (no orphaned child, by construction)', ()
options: {},
session: Session.create(id),
} as unknown as Agent
- ctx.agents.register(parent)
+ await ctx.agents.register(parent)
let starts = 0
ctx.subagents.registerProvider({
diff --git a/packages/terminal/terminal-bash/tests/index.spec.ts b/packages/terminal/terminal-bash/tests/index.spec.ts
index 7030b72e3e..f317714e2b 100644
--- a/packages/terminal/terminal-bash/tests/index.spec.ts
+++ b/packages/terminal/terminal-bash/tests/index.spec.ts
@@ -628,7 +628,7 @@ describe('terminal-bash plugin shape', () => {
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const providerFiber = await registerStubLocalBackend(ctx, () => stubLocalSession())
const created = await ctx.terminals.spawn(owner, { type: 'stub' })
@@ -678,7 +678,7 @@ describe('terminal-bash plugin shape', () => {
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const gate = Promise.withResolvers()
await registerStubLocalBackend(ctx, () => stubLocalSession(() => gate.promise))
const spawning = ctx.terminals.spawn(owner, { type: 'stub' })
diff --git a/packages/terminal/terminal-bash/tests/local.spec.ts b/packages/terminal/terminal-bash/tests/local.spec.ts
index aa7ced5ddd..7ef28c6929 100644
--- a/packages/terminal/terminal-bash/tests/local.spec.ts
+++ b/packages/terminal/terminal-bash/tests/local.spec.ts
@@ -79,7 +79,7 @@ async function harness(
maxReadBytes: 16_384,
})
const agent = stubAgent(ctx, `agent-${mode}`)
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
return { ctx, root, agent, fiber, sandbox: ctx.sandbox as PassthroughSandbox }
}
diff --git a/packages/terminal/terminal/tests/service.spec.ts b/packages/terminal/terminal/tests/service.spec.ts
index c5c552d18f..b0bf91af6a 100644
--- a/packages/terminal/terminal/tests/service.spec.ts
+++ b/packages/terminal/terminal/tests/service.spec.ts
@@ -160,8 +160,8 @@ describe('TerminalSessionService ownership and lifecycle', () => {
ctx.terminals.registerBackend(b.provider)
const owner = stubAgent(ctx, 'owner')
const foreign = stubAgent(ctx, 'foreign')
- ctx.agents.register(owner)
- ctx.agents.register(foreign)
+ await ctx.agents.register(owner)
+ await ctx.agents.register(foreign)
const created = await ctx.terminals.spawn(owner, { type: 'stub', name: 'main', cwd: '/tmp' })
expect(created).toMatchObject({ sessionId: 'pty-1', name: 'main', type: 'stub', pid: 123, motd: 'stub ready', status: { kind: 'running' } })
@@ -177,7 +177,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
const ctx = await harness()
const owner = stubAgent(ctx, 'owner')
await expect(ctx.terminals.spawn(owner, { type: 'missing' })).rejects.toMatchObject({ code: 'OWNER_NOT_LIVE' })
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
await expect(ctx.terminals.spawn(owner, { type: 'missing' })).rejects.toMatchObject({ code: 'NO_BACKEND' })
const b = backend()
ctx.terminals.registerBackend(b.provider)
@@ -209,7 +209,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
const session = new StubSession()
ctx.terminals.registerBackend({ type: 'slow', spawn: () => gate.promise })
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const pending = ctx.terminals.spawn(owner, { type: 'slow', name: 'main' })
await expect(ctx.terminals.spawn(owner, { type: 'slow', name: 'main' })).rejects.toMatchObject({ code: 'DUPLICATE_NAME' })
const disposal = disposeAgentScope(owner)
@@ -225,7 +225,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
const session = new StubSession()
ctx.terminals.registerBackend({ type: 'slow', spawn: () => gate.promise })
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const controller = new AbortController()
const reason = new Error('cancelled by caller')
@@ -245,7 +245,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
session.rejectClose = true
ctx.terminals.registerBackend({ type: 'slow', spawn: () => gate.promise })
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const controller = new AbortController()
const reason = new Error('cancelled by caller')
@@ -274,7 +274,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
}),
})
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const controller = new AbortController()
const reason = new Error('cancelled by caller')
@@ -300,7 +300,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
}),
})
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const controller = new AbortController()
const reason = new Error('cancelled by caller')
@@ -337,7 +337,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
},
})
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const pending = ctx.terminals.spawn(owner, { type: 'slow' })
const pendingFailure = pending.then(
@@ -369,7 +369,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
session.rejectClose = true
ctx.terminals.registerBackend({ type: 'slow', spawn: () => gate.promise })
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const pending = ctx.terminals.spawn(owner, { type: 'slow' })
const pendingFailure = expect(pending).rejects.toThrow('PTY spawn and rollback both failed')
@@ -402,7 +402,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
}),
})
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const pending = ctx.terminals.spawn(owner, { type: 'cleanup-failing' })
await started.promise
@@ -438,7 +438,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
spawn: () => ++count === 1 ? firstGate.promise : secondGate.promise,
})
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const first = ctx.terminals.spawn(owner, { type: 'slow', name: 'one' })
const second = ctx.terminals.spawn(owner, { type: 'slow', name: 'two' })
firstGate.resolve(new StubSession())
@@ -458,7 +458,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
it('omits optional pid metadata when a backend has no process id', async () => {
const ctx = await harness()
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const session = new StubSession()
Object.defineProperty(session, 'pid', { value: undefined })
ctx.terminals.registerBackend({ type: 'virtual', spawn: () => Promise.resolve(session) })
@@ -468,7 +468,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
it('reports rollback and close failures without publishing false success', async () => {
const ctx = await harness()
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const failedSpawn = new StubSession()
failedSpawn.rejectClose = true
let ownerDisposal = Promise.resolve()
@@ -494,7 +494,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
await expect(ownerDisposal).rejects.toThrow('failed to clean up PTY lifecycle')
const nextOwner = stubAgent(ctx, 'next')
- ctx.agents.register(nextOwner)
+ await ctx.agents.register(nextOwner)
const b = backend('bad-close')
ctx.terminals.registerBackend(b.provider)
const created = await ctx.terminals.spawn(nextOwner, { type: 'bad-close' })
@@ -506,7 +506,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
it('joins an already-running close and refuses new sends while closing', async () => {
const ctx = await harness()
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const b = backend()
ctx.terminals.registerBackend(b.provider)
const created = await ctx.terminals.spawn(owner, { type: 'stub' })
@@ -525,7 +525,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
const b = backend()
const disposeBackend = ctx.terminals.registerBackend(b.provider)
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
const created = await ctx.terminals.spawn(owner, { type: 'stub' })
disposeBackend()
expect(ctx.terminals.listBackends()).toEqual([])
@@ -542,8 +542,8 @@ describe('TerminalSessionService ownership and lifecycle', () => {
ctx.terminals.registerBackend(b.provider)
const first = stubAgent(ctx, 'first')
const second = stubAgent(ctx, 'second')
- ctx.agents.register(first)
- ctx.agents.register(second)
+ await ctx.agents.register(first)
+ await ctx.agents.register(second)
const a = await ctx.terminals.spawn(first, { type: 'stub' })
await ctx.terminals.spawn(second, { type: 'stub' })
expect(await ctx.terminals.kill(first, a.sessionId)).toBe(true)
@@ -561,7 +561,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
const b = backend()
ctx.terminals.registerBackend(b.provider)
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
await ctx.terminals.spawn(owner, { type: 'stub' })
b.sessions[0]!.rejectClose = true
const internal = service as unknown as {
@@ -587,7 +587,7 @@ describe('TerminalSessionService ownership and lifecycle', () => {
const b = backend()
service.registerBackend(b.provider)
const owner = stubAgent(ctx, 'owner')
- ctx.agents.register(owner)
+ await ctx.agents.register(owner)
await service.spawn(owner, { type: 'stub' })
b.sessions[0]!.rejectClose = true
const internal = service as unknown as {
diff --git a/packages/terminal/tool-terminal/tests/loader-composition.spec.ts b/packages/terminal/tool-terminal/tests/loader-composition.spec.ts
index bc991bb0ce..b983872934 100644
--- a/packages/terminal/tool-terminal/tests/loader-composition.spec.ts
+++ b/packages/terminal/tool-terminal/tests/loader-composition.spec.ts
@@ -38,7 +38,7 @@ class PassthroughSandbox extends SandboxProvider {
}
}
-function agent(ctx: Context): Agent {
+async function agent(ctx: Context): Promise {
const scope = ctx.plugin(() => {})
const id = SessionId('pty-loader-agent')
const session = Session.create(id)
@@ -51,7 +51,7 @@ function agent(ctx: Context): Agent {
runMaintenance: job => job(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
- ctx.agents.register(value)
+ await ctx.agents.register(value)
return value
}
@@ -115,7 +115,7 @@ suite('terminal real Loader composition through cordis.yml', () => {
await context.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } })
await context.loader.await()
- const owner = agent(context)
+ const owner = await agent(context)
const signal = new AbortController().signal
const spawn = await context.tools.execute({
signal, callId: ToolCallId('spawn'), name: 'terminal_open', arguments: { type: 'shell', name: 'main', cwd: root }, agent: owner,
diff --git a/packages/terminal/tool-terminal/tests/tools.spec.ts b/packages/terminal/tool-terminal/tests/tools.spec.ts
index e7a7467204..934b4c6d60 100644
--- a/packages/terminal/tool-terminal/tests/tools.spec.ts
+++ b/packages/terminal/tool-terminal/tests/tools.spec.ts
@@ -14,7 +14,7 @@ import * as ToolJobs from '@deepseek-ai/dsh-tool-jobs'
import * as ToolPty from '@deepseek-ai/dsh-tool-terminal'
import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit'
-function fakeAgent(ctx: Context, rawId: string): Agent {
+async function fakeAgent(ctx: Context, rawId: string): Promise {
const scope = ctx.plugin(() => {})
const id = SessionId(rawId)
const session = Session.create(id)
@@ -27,7 +27,7 @@ function fakeAgent(ctx: Context, rawId: string): Agent {
runMaintenance: job => job(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
- ctx.agents.register(agent)
+ await ctx.agents.register(agent)
return agent
}
@@ -116,7 +116,7 @@ async function setupBase(jobs: boolean) {
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolJobs)
}
- return { ctx, stub, agent: fakeAgent(ctx, jobs ? 'with-tasks' : 'foreground') }
+ return { ctx, stub, agent: await fakeAgent(ctx, jobs ? 'with-tasks' : 'foreground') }
}
let callNumber = 0
diff --git a/packages/todo/tool-todo/tests/loader-composition.spec.ts b/packages/todo/tool-todo/tests/loader-composition.spec.ts
index 7365651f21..a1e76b01b3 100644
--- a/packages/todo/tool-todo/tests/loader-composition.spec.ts
+++ b/packages/todo/tool-todo/tests/loader-composition.spec.ts
@@ -29,7 +29,7 @@ afterEach(async () => {
root = undefined
})
-function agent(ctx: Context): Agent {
+async function agent(ctx: Context): Promise {
const scope = ctx.plugin(() => {})
const id = SessionId('todo-loader-agent')
const session = Session.create(id)
@@ -40,7 +40,7 @@ function agent(ctx: Context): Agent {
runMaintenance: task => task(new AbortController().signal),
whenIdle: () => Promise.resolve(),
}
- ctx.agents.register(value)
+ await ctx.agents.register(value)
return value
}
@@ -103,7 +103,7 @@ describe('tool-todo real Loader composition through cordis.yml', () => {
expect(description).toContain('Keep AT MOST ONE todo `in_progress`')
expect(description).not.toContain('several at once')
- const owner = agent(ctx)
+ const owner = await agent(ctx)
const result = await ctx.tools.execute({
signal: new AbortController().signal,
callId: ToolCallId('parallel'),
@@ -121,7 +121,7 @@ describe('tool-todo real Loader composition through cordis.yml', () => {
const description = ctx.tools.schemas().find(s => s.name === 'todo_write')?.description ?? ''
expect(description).toContain('several at once when work genuinely runs in parallel')
- const owner = agent(ctx)
+ const owner = await agent(ctx)
const result = await ctx.tools.execute({
signal: new AbortController().signal,
callId: ToolCallId('parallel-enabled'),
diff --git a/packages/todo/tool-todo/tests/projection.spec.ts b/packages/todo/tool-todo/tests/projection.spec.ts
index 1e790eb96c..20975df8c0 100644
--- a/packages/todo/tool-todo/tests/projection.spec.ts
+++ b/packages/todo/tool-todo/tests/projection.spec.ts
@@ -37,7 +37,7 @@ async function harness(withTodoTool: boolean): Promise {
await ctx.plugin(SessionProjectionRegistry)
if (withTodoTool) await ctx.plugin(ToolTodo, { allowParallelInProgress: true })
const session = ctx.sessions.create()
- ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
+ await ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
return {
ctx,
session,
diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts
index ff2edacad4..973ddc9a2a 100644
--- a/scripts/gen-cordis-catalog.ts
+++ b/scripts/gen-cordis-catalog.ts
@@ -700,6 +700,7 @@ export const FOUNDATION_TYPE_NAMES: ReadonlySet = new Set([
'ReadonlyMap',
'Request',
'Response',
+ 'ReturnType',
'Uint8Array',
])
diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts
index f97e272168..5574d4e91a 100644
--- a/scripts/gen-tool-catalog.ts
+++ b/scripts/gen-tool-catalog.ts
@@ -579,7 +579,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
tryMembership: (candidate: Agent) => candidate === agent ? membership : undefined,
membership: () => membership,
} as unknown as TeamService)
- await ctx.plugin(Object.assign((inner: Context) => {
+ await ctx.plugin(Object.assign(async (inner: Context) => {
agent = {
id: session.id,
session,
@@ -587,7 +587,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
status: 'idle',
} as unknown as Agent
Object.assign(agent, { ctx: createScope(inner, agent).ctx })
- inner.agents.register(agent)
+ await inner.agents.register(agent)
}, { inject: ['tools', 'systemPrompt', 'agents', 'agentTeams'] }))
await ctx.plugin(ToolTeam)
catalogChildScopes.set(ctx, agent)
diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py
index af31bc9de2..2f02c08fe0 100644
--- a/scripts/smoke-python-runtime.py
+++ b/scripts/smoke-python-runtime.py
@@ -1347,13 +1347,19 @@ def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool)
Path(__file__).resolve().parent / "fixtures/python-browser-use-events.mjs"
).as_uri()},
]}])
+ creation_patch = write_profile_patch(root, "creation.patch.yml", sessions, [
+ {"insert": [{
+ "id": "serial-created-fixture",
+ "name": str(Path(__file__).resolve().parents[1] / "packages/core/agent-loop/tests/fixtures/serial-created.mjs"),
+ }]},
+ ])
with DeepSeekHarness(
provider="deepseek-official",
model="smoke-model",
cwd=str(root),
dsh_bin=str(executable),
dsh_home=str(dsh_home),
- patches=(str(patch), str(feedback_patch)),
+ patches=(str(patch), str(feedback_patch), str(creation_patch)),
env={
"DSH_PERMISSION_MODE": "danger-full-access",
"DSH_TELEMETRY_DISABLED": "1",
diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/result.json b/scripts/snapshots/python-sdk-single-exe/advanced/result.json
index 29f5074a00..23f59215b2 100644
--- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json
+++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json
@@ -4,7 +4,7 @@
"events": [
{
"type": "agent/inbox/spliced",
- "seq": 3,
+ "seq": 4,
"time": 0,
"data": {
"target": "next-turn",
@@ -28,7 +28,7 @@
},
{
"type": "turn/start",
- "seq": 4,
+ "seq": 5,
"time": 0,
"data": {
"turn": 1
@@ -36,7 +36,18 @@
},
{
"type": "agent/inbox/spliced",
- "seq": 5,
+ "seq": 6,
+ "time": 0,
+ "data": {
+ "target": "next-step",
+ "start": 0,
+ "removedCount": 1,
+ "inserted": []
+ }
+ },
+ {
+ "type": "agent/inbox/spliced",
+ "seq": 7,
"time": 0,
"data": {
"target": "next-turn",
@@ -47,7 +58,7 @@
},
{
"type": "step/start",
- "seq": 6,
+ "seq": 8,
"time": 0,
"data": {
"turn": 1,
@@ -56,7 +67,7 @@
},
{
"type": "system/message",
- "seq": 7,
+ "seq": 9,
"time": 0,
"data": {
"turn": 1,
@@ -80,7 +91,27 @@
},
{
"type": "user/message",
- "seq": 8,
+ "seq": 10,
+ "time": 0,
+ "data": {
+ "id": "{{messageId}}",
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": "Serial agent creation completed."
+ }
+ ],
+ "source": {
+ "kind": "plugin",
+ "plugin": "serial-created-fixture"
+ }
+ },
+ "surfaceOp": "append"
+ },
+ {
+ "type": "user/message",
+ "seq": 11,
"time": 0,
"data": {
"content": [
@@ -99,7 +130,7 @@
},
{
"type": "user/message",
- "seq": 9,
+ "seq": 12,
"time": 0,
"data": {
"content": [
@@ -130,7 +161,7 @@
},
{
"type": "user/message",
- "seq": 10,
+ "seq": 13,
"time": 0,
"data": {
"id": "{{messageId}}",
@@ -156,7 +187,7 @@
},
{
"type": "request/header",
- "seq": 11,
+ "seq": 14,
"time": 0,
"data": {
"header": {
@@ -191,7 +222,7 @@
},
{
"type": "request/context",
- "seq": 12,
+ "seq": 15,
"time": 0,
"data": {
"provider": "deepseek-official",
@@ -201,12 +232,12 @@
},
{
"type": "session/title",
- "seq": 13,
+ "seq": 16,
"time": 0,
"data": {
"title": "Run the advanced packaged-runtime snapsh",
"messageSeqs": [
- 8
+ 11
],
"source": {
"kind": "fallback"
@@ -215,7 +246,7 @@
},
{
"type": "assistant/attempt",
- "seq": 14,
+ "seq": 17,
"time": 0,
"data": {
"turn": 1,
@@ -241,12 +272,12 @@
},
{
"type": "image/offload",
- "seq": 15,
+ "seq": 18,
"time": 0,
"data": {
"targets": [
{
- "seq": 10,
+ "seq": 13,
"imageIndexes": [
0
]
@@ -256,7 +287,7 @@
},
{
"type": "request/header",
- "seq": 16,
+ "seq": 19,
"time": 0,
"data": {
"header": {
@@ -291,17 +322,17 @@
},
{
"type": "session-log-deepseek/delivery-accepted",
- "seq": 17,
+ "seq": 20,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 16
+ "throughSeq": 19
}
},
{
"type": "assistant/message",
- "seq": 18,
+ "seq": 21,
"time": 0,
"data": {
"turn": 1,
@@ -425,7 +456,7 @@
},
{
"type": "tool/call",
- "seq": 19,
+ "seq": 22,
"time": 0,
"data": {
"turn": 1,
@@ -437,7 +468,7 @@
},
{
"type": "tool/result",
- "seq": 20,
+ "seq": 23,
"time": 0,
"data": {
"turn": 1,
@@ -469,13 +500,13 @@
}
},
"sourceEventSeqs": [
- 19
+ 22
],
"surfaceOp": "append"
},
{
"type": "step/end",
- "seq": 21,
+ "seq": 24,
"time": 0,
"data": {
"turn": 1,
@@ -484,7 +515,7 @@
},
{
"type": "step/start",
- "seq": 22,
+ "seq": 25,
"time": 0,
"data": {
"turn": 1,
@@ -493,17 +524,17 @@
},
{
"type": "session-log-deepseek/delivery-accepted",
- "seq": 23,
+ "seq": 26,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 22
+ "throughSeq": 25
}
},
{
"type": "assistant/message",
- "seq": 24,
+ "seq": 27,
"time": 0,
"data": {
"turn": 1,
@@ -627,7 +658,7 @@
},
{
"type": "tool/call",
- "seq": 25,
+ "seq": 28,
"time": 0,
"data": {
"turn": 1,
@@ -639,7 +670,7 @@
},
{
"type": "tool/result",
- "seq": 26,
+ "seq": 29,
"time": 0,
"data": {
"turn": 1,
@@ -672,13 +703,13 @@
}
},
"sourceEventSeqs": [
- 25
+ 28
],
"surfaceOp": "append"
},
{
"type": "step/end",
- "seq": 27,
+ "seq": 30,
"time": 0,
"data": {
"turn": 1,
@@ -687,7 +718,7 @@
},
{
"type": "step/start",
- "seq": 28,
+ "seq": 31,
"time": 0,
"data": {
"turn": 1,
@@ -696,7 +727,7 @@
},
{
"type": "system/message",
- "seq": 29,
+ "seq": 32,
"time": 0,
"data": {
"turn": 1,
@@ -717,17 +748,17 @@
}
},
"sourceEventSeqs": [
- 7
+ 9
],
"surfaceOp": {
"op": "replace",
- "startSeq": 7,
- "endSeq": 7
+ "startSeq": 9,
+ "endSeq": 9
}
},
{
"type": "request/header",
- "seq": 30,
+ "seq": 33,
"time": 0,
"data": {
"header": {
@@ -764,17 +795,17 @@
},
{
"type": "session-log-deepseek/delivery-accepted",
- "seq": 31,
+ "seq": 34,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 30
+ "throughSeq": 33
}
},
{
"type": "assistant/message",
- "seq": 32,
+ "seq": 35,
"time": 0,
"data": {
"turn": 1,
@@ -898,7 +929,7 @@
},
{
"type": "tool/call",
- "seq": 33,
+ "seq": 36,
"time": 0,
"data": {
"turn": 1,
@@ -910,7 +941,7 @@
},
{
"type": "tool/ptc-dispatch-start",
- "seq": 34,
+ "seq": 37,
"time": 0,
"data": {
"rootCallId": "advanced-code",
@@ -924,7 +955,7 @@
},
{
"type": "tool/ptc-dispatch",
- "seq": 35,
+ "seq": 38,
"time": 0,
"data": {
"rootCallId": "advanced-code",
@@ -945,7 +976,7 @@
},
{
"type": "tool/result",
- "seq": 36,
+ "seq": 39,
"time": 0,
"data": {
"turn": 1,
@@ -973,13 +1004,13 @@
}
},
"sourceEventSeqs": [
- 33
+ 36
],
"surfaceOp": "append"
},
{
"type": "step/end",
- "seq": 37,
+ "seq": 40,
"time": 0,
"data": {
"turn": 1,
@@ -988,7 +1019,7 @@
},
{
"type": "step/start",
- "seq": 38,
+ "seq": 41,
"time": 0,
"data": {
"turn": 1,
@@ -997,17 +1028,17 @@
},
{
"type": "session-log-deepseek/delivery-accepted",
- "seq": 39,
+ "seq": 42,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 38
+ "throughSeq": 41
}
},
{
"type": "assistant/message",
- "seq": 40,
+ "seq": 43,
"time": 0,
"data": {
"turn": 1,
@@ -1131,7 +1162,7 @@
},
{
"type": "tool/call",
- "seq": 41,
+ "seq": 44,
"time": 0,
"data": {
"turn": 1,
@@ -1143,7 +1174,7 @@
},
{
"type": "tool/result",
- "seq": 42,
+ "seq": 45,
"time": 0,
"data": {
"turn": 1,
@@ -1176,13 +1207,13 @@
}
},
"sourceEventSeqs": [
- 41
+ 44
],
"surfaceOp": "append"
},
{
"type": "step/end",
- "seq": 43,
+ "seq": 46,
"time": 0,
"data": {
"turn": 1,
@@ -1191,7 +1222,7 @@
},
{
"type": "step/start",
- "seq": 44,
+ "seq": 47,
"time": 0,
"data": {
"turn": 1,
@@ -1200,17 +1231,17 @@
},
{
"type": "session-log-deepseek/delivery-accepted",
- "seq": 45,
+ "seq": 48,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 44
+ "throughSeq": 47
}
},
{
"type": "assistant/message",
- "seq": 46,
+ "seq": 49,
"time": 0,
"data": {
"turn": 1,
@@ -1334,7 +1365,7 @@
},
{
"type": "tool/call",
- "seq": 47,
+ "seq": 50,
"time": 0,
"data": {
"turn": 1,
@@ -1346,7 +1377,7 @@
},
{
"type": "tool/ptc-dispatch-start",
- "seq": 48,
+ "seq": 51,
"time": 0,
"data": {
"rootCallId": "advanced-denied-ptc",
@@ -1360,7 +1391,7 @@
},
{
"type": "tool/ptc-dispatch",
- "seq": 49,
+ "seq": 52,
"time": 0,
"data": {
"rootCallId": "advanced-denied-ptc",
@@ -1386,7 +1417,7 @@
},
{
"type": "tool/result",
- "seq": 50,
+ "seq": 53,
"time": 0,
"data": {
"turn": 1,
@@ -1414,13 +1445,13 @@
}
},
"sourceEventSeqs": [
- 47
+ 50
],
"surfaceOp": "append"
},
{
"type": "step/end",
- "seq": 51,
+ "seq": 54,
"time": 0,
"data": {
"turn": 1,
@@ -1429,7 +1460,7 @@
},
{
"type": "step/start",
- "seq": 52,
+ "seq": 55,
"time": 0,
"data": {
"turn": 1,
@@ -1438,17 +1469,17 @@
},
{
"type": "session-log-deepseek/delivery-accepted",
- "seq": 53,
+ "seq": 56,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 52
+ "throughSeq": 55
}
},
{
"type": "assistant/message",
- "seq": 54,
+ "seq": 57,
"time": 0,
"data": {
"turn": 1,
@@ -1572,7 +1603,7 @@
},
{
"type": "tool/call",
- "seq": 55,
+ "seq": 58,
"time": 0,
"data": {
"turn": 1,
@@ -1584,7 +1615,7 @@
},
{
"type": "subagent/catalog",
- "seq": 56,
+ "seq": 59,
"time": 0,
"data": {
"version": 0,
@@ -1596,7 +1627,7 @@
},
{
"type": "tool/result",
- "seq": 57,
+ "seq": 60,
"time": 0,
"data": {
"turn": 1,
@@ -1624,13 +1655,13 @@
}
},
"sourceEventSeqs": [
- 55
+ 58
],
"surfaceOp": "append"
},
{
"type": "step/end",
- "seq": 58,
+ "seq": 61,
"time": 0,
"data": {
"turn": 1,
@@ -1639,7 +1670,7 @@
},
{
"type": "step/start",
- "seq": 59,
+ "seq": 62,
"time": 0,
"data": {
"turn": 1,
@@ -1648,17 +1679,17 @@
},
{
"type": "session-log-deepseek/delivery-accepted",
- "seq": 60,
+ "seq": 63,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 59
+ "throughSeq": 62
}
},
{
"type": "assistant/message",
- "seq": 61,
+ "seq": 64,
"time": 0,
"data": {
"turn": 1,
@@ -1782,7 +1813,7 @@
},
{
"type": "tool/call",
- "seq": 62,
+ "seq": 65,
"time": 0,
"data": {
"turn": 1,
@@ -1794,7 +1825,7 @@
},
{
"type": "tool-workflow/run-start",
- "seq": 63,
+ "seq": 66,
"time": 0,
"data": {
"runId": "{{workflow-run}}",
@@ -1803,7 +1834,7 @@
},
{
"type": "subagent/catalog",
- "seq": 64,
+ "seq": 67,
"time": 0,
"data": {
"version": 0,
@@ -1814,7 +1845,7 @@
},
{
"type": "tool-workflow/agent-start",
- "seq": 65,
+ "seq": 68,
"time": 0,
"data": {
"runId": "{{workflow-run}}",
@@ -1826,7 +1857,7 @@
},
{
"type": "tool-workflow/agent-end",
- "seq": 66,
+ "seq": 69,
"time": 0,
"data": {
"runId": "{{workflow-run}}",
@@ -1836,7 +1867,7 @@
},
{
"type": "tool-workflow/run-end",
- "seq": 67,
+ "seq": 70,
"time": 0,
"data": {
"runId": "{{workflow-run}}",
@@ -1845,7 +1876,7 @@
},
{
"type": "tool/result",
- "seq": 68,
+ "seq": 71,
"time": 0,
"data": {
"turn": 1,
@@ -1873,13 +1904,13 @@
}
},
"sourceEventSeqs": [
- 62
+ 65
],
"surfaceOp": "append"
},
{
"type": "step/end",
- "seq": 69,
+ "seq": 72,
"time": 0,
"data": {
"turn": 1,
@@ -1888,7 +1919,7 @@
},
{
"type": "step/start",
- "seq": 70,
+ "seq": 73,
"time": 0,
"data": {
"turn": 1,
@@ -1897,17 +1928,17 @@
},
{
"type": "session-log-deepseek/delivery-accepted",
- "seq": 71,
+ "seq": 74,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 70
+ "throughSeq": 73
}
},
{
"type": "assistant/message",
- "seq": 72,
+ "seq": 75,
"time": 0,
"data": {
"turn": 1,
@@ -2031,7 +2062,7 @@
},
{
"type": "tool/call",
- "seq": 73,
+ "seq": 76,
"time": 0,
"data": {
"turn": 1,
@@ -2043,7 +2074,7 @@
},
{
"type": "tool/result",
- "seq": 74,
+ "seq": 77,
"time": 0,
"data": {
"turn": 1,
@@ -2071,13 +2102,13 @@
}
},
"sourceEventSeqs": [
- 73
+ 76
],
"surfaceOp": "append"
},
{
"type": "step/end",
- "seq": 75,
+ "seq": 78,
"time": 0,
"data": {
"turn": 1,
@@ -2086,7 +2117,7 @@
},
{
"type": "step/start",
- "seq": 76,
+ "seq": 79,
"time": 0,
"data": {
"turn": 1,
@@ -2095,7 +2126,7 @@
},
{
"type": "system/message",
- "seq": 77,
+ "seq": 80,
"time": 0,
"data": {
"turn": 1,
@@ -2116,17 +2147,17 @@
}
},
"sourceEventSeqs": [
- 29
+ 32
],
"surfaceOp": {
"op": "replace",
- "startSeq": 29,
- "endSeq": 29
+ "startSeq": 32,
+ "endSeq": 32
}
},
{
"type": "request/header",
- "seq": 78,
+ "seq": 81,
"time": 0,
"data": {
"header": {
@@ -2162,17 +2193,17 @@
},
{
"type": "session-log-deepseek/delivery-accepted",
- "seq": 79,
+ "seq": 82,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 78
+ "throughSeq": 81
}
},
{
"type": "assistant/message",
- "seq": 80,
+ "seq": 83,
"time": 0,
"data": {
"turn": 1,
@@ -2280,7 +2311,7 @@
},
{
"type": "step/end",
- "seq": 81,
+ "seq": 84,
"time": 0,
"data": {
"turn": 1,
@@ -2289,7 +2320,7 @@
},
{
"type": "browser-use/stagehand-llm-request",
- "seq": 82,
+ "seq": 85,
"time": 0,
"data": {
"request": {
@@ -2362,10 +2393,10 @@
},
{
"type": "browser-use/stagehand-llm-result",
- "seq": 83,
+ "seq": 86,
"time": 0,
"data": {
- "requestSeq": 82,
+ "requestSeq": 85,
"blocks": [
{
"type": "tool-call",
@@ -2385,7 +2416,7 @@
},
{
"type": "command/run",
- "seq": 84,
+ "seq": 87,
"time": 0,
"data": {
"commandId": "{{command:1}}",
@@ -2397,7 +2428,7 @@
},
{
"type": "feedback/record",
- "seq": 85,
+ "seq": 88,
"time": 0,
"data": {
"text": "The session needs a clearer explanation."
@@ -2405,7 +2436,7 @@
},
{
"type": "command/done",
- "seq": 86,
+ "seq": 89,
"time": 0,
"data": {
"commandId": "{{command:1}}",
@@ -2415,7 +2446,7 @@
},
{
"type": "feedback/record",
- "seq": 87,
+ "seq": 90,
"time": 0,
"data": {
"category": "other"
@@ -2423,7 +2454,7 @@
},
{
"type": "feedback/message-put",
- "seq": 88,
+ "seq": 91,
"time": 0,
"data": {
"sessionId": "{{parent}}",
@@ -2440,7 +2471,7 @@
},
{
"type": "feedback/message-put",
- "seq": 89,
+ "seq": 92,
"time": 0,
"data": {
"sessionId": "{{parent}}",
@@ -2456,7 +2487,7 @@
},
{
"type": "feedback/message-delete",
- "seq": 90,
+ "seq": 93,
"time": 0,
"data": {
"sessionId": "{{parent}}",
@@ -2465,7 +2496,7 @@
},
{
"type": "turn/end",
- "seq": 91,
+ "seq": 94,
"time": 0,
"data": {
"turn": 1,
@@ -2482,7 +2513,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "agent/inbox/spliced",
- "seq": 3,
+ "seq": 4,
"time": 0,
"data": {
"target": "next-turn",
@@ -2519,7 +2550,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "turn/start",
- "seq": 4,
+ "seq": 5,
"time": 0,
"data": {
"turn": 1
@@ -2533,7 +2564,24 @@
"sessionId": "{{parent}}",
"event": {
"type": "agent/inbox/spliced",
- "seq": 5,
+ "seq": 6,
+ "time": 0,
+ "data": {
+ "target": "next-step",
+ "start": 0,
+ "removedCount": 1,
+ "inserted": []
+ }
+ }
+ }
+ },
+ {
+ "method": "session.event",
+ "payload": {
+ "sessionId": "{{parent}}",
+ "event": {
+ "type": "agent/inbox/spliced",
+ "seq": 7,
"time": 0,
"data": {
"target": "next-turn",
@@ -2550,7 +2598,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/start",
- "seq": 6,
+ "seq": 8,
"time": 0,
"data": {
"turn": 1,
@@ -2565,7 +2613,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "system/message",
- "seq": 7,
+ "seq": 9,
"time": 0,
"data": {
"turn": 1,
@@ -2595,7 +2643,33 @@
"sessionId": "{{parent}}",
"event": {
"type": "user/message",
- "seq": 8,
+ "seq": 10,
+ "time": 0,
+ "data": {
+ "id": "{{messageId}}",
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": "Serial agent creation completed."
+ }
+ ],
+ "source": {
+ "kind": "plugin",
+ "plugin": "serial-created-fixture"
+ }
+ },
+ "surfaceOp": "append"
+ }
+ }
+ },
+ {
+ "method": "session.event",
+ "payload": {
+ "sessionId": "{{parent}}",
+ "event": {
+ "type": "user/message",
+ "seq": 11,
"time": 0,
"data": {
"content": [
@@ -2620,7 +2694,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "user/message",
- "seq": 9,
+ "seq": 12,
"time": 0,
"data": {
"content": [
@@ -2657,7 +2731,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "user/message",
- "seq": 10,
+ "seq": 13,
"time": 0,
"data": {
"id": "{{messageId}}",
@@ -2689,7 +2763,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "request/header",
- "seq": 11,
+ "seq": 14,
"time": 0,
"data": {
"header": {
@@ -2730,7 +2804,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "request/context",
- "seq": 12,
+ "seq": 15,
"time": 0,
"data": {
"provider": "deepseek-official",
@@ -2746,12 +2820,12 @@
"sessionId": "{{parent}}",
"event": {
"type": "session/title",
- "seq": 13,
+ "seq": 16,
"time": 0,
"data": {
"title": "Run the advanced packaged-runtime snapsh",
"messageSeqs": [
- 8
+ 11
],
"source": {
"kind": "fallback"
@@ -2766,7 +2840,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "assistant/attempt",
- "seq": 14,
+ "seq": 17,
"time": 0,
"data": {
"turn": 1,
@@ -2798,12 +2872,12 @@
"sessionId": "{{parent}}",
"event": {
"type": "image/offload",
- "seq": 15,
+ "seq": 18,
"time": 0,
"data": {
"targets": [
{
- "seq": 10,
+ "seq": 13,
"imageIndexes": [
0
]
@@ -2819,7 +2893,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "request/header",
- "seq": 16,
+ "seq": 19,
"time": 0,
"data": {
"header": {
@@ -2860,12 +2934,12 @@
"sessionId": "{{parent}}",
"event": {
"type": "session-log-deepseek/delivery-accepted",
- "seq": 17,
+ "seq": 20,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 16
+ "throughSeq": 19
}
}
}
@@ -2876,7 +2950,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "assistant/message",
- "seq": 18,
+ "seq": 21,
"time": 0,
"data": {
"turn": 1,
@@ -3006,7 +3080,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/call",
- "seq": 19,
+ "seq": 22,
"time": 0,
"data": {
"turn": 1,
@@ -3024,7 +3098,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/result",
- "seq": 20,
+ "seq": 23,
"time": 0,
"data": {
"turn": 1,
@@ -3056,7 +3130,7 @@
}
},
"sourceEventSeqs": [
- 19
+ 22
],
"surfaceOp": "append"
}
@@ -3068,7 +3142,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/end",
- "seq": 21,
+ "seq": 24,
"time": 0,
"data": {
"turn": 1,
@@ -3083,7 +3157,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/start",
- "seq": 22,
+ "seq": 25,
"time": 0,
"data": {
"turn": 1,
@@ -3098,12 +3172,12 @@
"sessionId": "{{parent}}",
"event": {
"type": "session-log-deepseek/delivery-accepted",
- "seq": 23,
+ "seq": 26,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 22
+ "throughSeq": 25
}
}
}
@@ -3114,7 +3188,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "assistant/message",
- "seq": 24,
+ "seq": 27,
"time": 0,
"data": {
"turn": 1,
@@ -3244,7 +3318,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/call",
- "seq": 25,
+ "seq": 28,
"time": 0,
"data": {
"turn": 1,
@@ -3262,7 +3336,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/result",
- "seq": 26,
+ "seq": 29,
"time": 0,
"data": {
"turn": 1,
@@ -3295,7 +3369,7 @@
}
},
"sourceEventSeqs": [
- 25
+ 28
],
"surfaceOp": "append"
}
@@ -3307,7 +3381,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/end",
- "seq": 27,
+ "seq": 30,
"time": 0,
"data": {
"turn": 1,
@@ -3322,7 +3396,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/start",
- "seq": 28,
+ "seq": 31,
"time": 0,
"data": {
"turn": 1,
@@ -3337,7 +3411,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "system/message",
- "seq": 29,
+ "seq": 32,
"time": 0,
"data": {
"turn": 1,
@@ -3358,12 +3432,12 @@
}
},
"sourceEventSeqs": [
- 7
+ 9
],
"surfaceOp": {
"op": "replace",
- "startSeq": 7,
- "endSeq": 7
+ "startSeq": 9,
+ "endSeq": 9
}
}
}
@@ -3374,7 +3448,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "request/header",
- "seq": 30,
+ "seq": 33,
"time": 0,
"data": {
"header": {
@@ -3417,12 +3491,12 @@
"sessionId": "{{parent}}",
"event": {
"type": "session-log-deepseek/delivery-accepted",
- "seq": 31,
+ "seq": 34,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 30
+ "throughSeq": 33
}
}
}
@@ -3433,7 +3507,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "assistant/message",
- "seq": 32,
+ "seq": 35,
"time": 0,
"data": {
"turn": 1,
@@ -3563,7 +3637,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/call",
- "seq": 33,
+ "seq": 36,
"time": 0,
"data": {
"turn": 1,
@@ -3581,7 +3655,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/ptc-dispatch-start",
- "seq": 34,
+ "seq": 37,
"time": 0,
"data": {
"rootCallId": "advanced-code",
@@ -3601,7 +3675,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/ptc-dispatch",
- "seq": 35,
+ "seq": 38,
"time": 0,
"data": {
"rootCallId": "advanced-code",
@@ -3628,7 +3702,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/result",
- "seq": 36,
+ "seq": 39,
"time": 0,
"data": {
"turn": 1,
@@ -3656,7 +3730,7 @@
}
},
"sourceEventSeqs": [
- 33
+ 36
],
"surfaceOp": "append"
}
@@ -3668,7 +3742,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/end",
- "seq": 37,
+ "seq": 40,
"time": 0,
"data": {
"turn": 1,
@@ -3683,7 +3757,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/start",
- "seq": 38,
+ "seq": 41,
"time": 0,
"data": {
"turn": 1,
@@ -3698,12 +3772,12 @@
"sessionId": "{{parent}}",
"event": {
"type": "session-log-deepseek/delivery-accepted",
- "seq": 39,
+ "seq": 42,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 38
+ "throughSeq": 41
}
}
}
@@ -3714,7 +3788,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "assistant/message",
- "seq": 40,
+ "seq": 43,
"time": 0,
"data": {
"turn": 1,
@@ -3844,7 +3918,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/call",
- "seq": 41,
+ "seq": 44,
"time": 0,
"data": {
"turn": 1,
@@ -3862,7 +3936,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/result",
- "seq": 42,
+ "seq": 45,
"time": 0,
"data": {
"turn": 1,
@@ -3895,7 +3969,7 @@
}
},
"sourceEventSeqs": [
- 41
+ 44
],
"surfaceOp": "append"
}
@@ -3907,7 +3981,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/end",
- "seq": 43,
+ "seq": 46,
"time": 0,
"data": {
"turn": 1,
@@ -3922,7 +3996,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/start",
- "seq": 44,
+ "seq": 47,
"time": 0,
"data": {
"turn": 1,
@@ -3937,12 +4011,12 @@
"sessionId": "{{parent}}",
"event": {
"type": "session-log-deepseek/delivery-accepted",
- "seq": 45,
+ "seq": 48,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 44
+ "throughSeq": 47
}
}
}
@@ -3953,7 +4027,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "assistant/message",
- "seq": 46,
+ "seq": 49,
"time": 0,
"data": {
"turn": 1,
@@ -4083,7 +4157,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/call",
- "seq": 47,
+ "seq": 50,
"time": 0,
"data": {
"turn": 1,
@@ -4101,7 +4175,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/ptc-dispatch-start",
- "seq": 48,
+ "seq": 51,
"time": 0,
"data": {
"rootCallId": "advanced-denied-ptc",
@@ -4121,7 +4195,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/ptc-dispatch",
- "seq": 49,
+ "seq": 52,
"time": 0,
"data": {
"rootCallId": "advanced-denied-ptc",
@@ -4153,7 +4227,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/result",
- "seq": 50,
+ "seq": 53,
"time": 0,
"data": {
"turn": 1,
@@ -4181,7 +4255,7 @@
}
},
"sourceEventSeqs": [
- 47
+ 50
],
"surfaceOp": "append"
}
@@ -4193,7 +4267,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/end",
- "seq": 51,
+ "seq": 54,
"time": 0,
"data": {
"turn": 1,
@@ -4208,7 +4282,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/start",
- "seq": 52,
+ "seq": 55,
"time": 0,
"data": {
"turn": 1,
@@ -4223,12 +4297,12 @@
"sessionId": "{{parent}}",
"event": {
"type": "session-log-deepseek/delivery-accepted",
- "seq": 53,
+ "seq": 56,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 52
+ "throughSeq": 55
}
}
}
@@ -4239,7 +4313,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "assistant/message",
- "seq": 54,
+ "seq": 57,
"time": 0,
"data": {
"turn": 1,
@@ -4369,7 +4443,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/call",
- "seq": 55,
+ "seq": 58,
"time": 0,
"data": {
"turn": 1,
@@ -4396,6 +4470,37 @@
"type": "agent/inbox/spliced",
"seq": 3,
"time": 0,
+ "data": {
+ "target": "next-step",
+ "start": 0,
+ "inserted": [
+ {
+ "id": "{{messageId}}",
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": "Serial agent creation completed."
+ }
+ ],
+ "source": {
+ "kind": "plugin",
+ "plugin": "serial-created-fixture"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ {
+ "method": "session.event",
+ "payload": {
+ "sessionId": "{{child-1}}",
+ "event": {
+ "type": "agent/inbox/spliced",
+ "seq": 4,
+ "time": 0,
"data": {
"target": "next-turn",
"start": 0,
@@ -4431,7 +4536,7 @@
"sessionId": "{{child-1}}",
"event": {
"type": "turn/start",
- "seq": 4,
+ "seq": 5,
"time": 0,
"data": {
"turn": 1
@@ -4445,7 +4550,24 @@
"sessionId": "{{child-1}}",
"event": {
"type": "agent/inbox/spliced",
- "seq": 5,
+ "seq": 6,
+ "time": 0,
+ "data": {
+ "target": "next-step",
+ "start": 0,
+ "removedCount": 1,
+ "inserted": []
+ }
+ }
+ }
+ },
+ {
+ "method": "session.event",
+ "payload": {
+ "sessionId": "{{child-1}}",
+ "event": {
+ "type": "agent/inbox/spliced",
+ "seq": 7,
"time": 0,
"data": {
"target": "next-turn",
@@ -4462,7 +4584,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "subagent/catalog",
- "seq": 56,
+ "seq": 59,
"time": 0,
"data": {
"version": 0,
@@ -4480,7 +4602,7 @@
"sessionId": "{{child-1}}",
"event": {
"type": "subagent/descriptor",
- "seq": 6,
+ "seq": 8,
"time": 0,
"data": {
"version": 3,
@@ -4497,7 +4619,7 @@
"sessionId": "{{child-1}}",
"event": {
"type": "step/start",
- "seq": 7,
+ "seq": 9,
"time": 0,
"data": {
"turn": 1,
@@ -4512,7 +4634,7 @@
"sessionId": "{{child-1}}",
"event": {
"type": "system/message",
- "seq": 8,
+ "seq": 10,
"time": 0,
"data": {
"turn": 1,
@@ -4542,7 +4664,33 @@
"sessionId": "{{child-1}}",
"event": {
"type": "user/message",
- "seq": 9,
+ "seq": 11,
+ "time": 0,
+ "data": {
+ "id": "{{messageId}}",
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": "Serial agent creation completed."
+ }
+ ],
+ "source": {
+ "kind": "plugin",
+ "plugin": "serial-created-fixture"
+ }
+ },
+ "surfaceOp": "append"
+ }
+ }
+ },
+ {
+ "method": "session.event",
+ "payload": {
+ "sessionId": "{{child-1}}",
+ "event": {
+ "type": "user/message",
+ "seq": 12,
"time": 0,
"data": {
"content": [
@@ -4567,7 +4715,7 @@
"sessionId": "{{child-1}}",
"event": {
"type": "user/message",
- "seq": 10,
+ "seq": 13,
"time": 0,
"data": {
"content": [
@@ -4608,7 +4756,7 @@
"sessionId": "{{child-1}}",
"event": {
"type": "request/header",
- "seq": 11,
+ "seq": 14,
"time": 0,
"data": {
"header": {
@@ -4649,7 +4797,7 @@
"sessionId": "{{child-1}}",
"event": {
"type": "request/context",
- "seq": 12,
+ "seq": 15,
"time": 0,
"data": {
"provider": "deepseek-official",
@@ -4665,12 +4813,12 @@
"sessionId": "{{child-1}}",
"event": {
"type": "session/title",
- "seq": 13,
+ "seq": 16,
"time": 0,
"data": {
"title": "Reply with exactly DIRECT_CHILD_OK and",
"messageSeqs": [
- 9
+ 12
],
"source": {
"kind": "fallback"
@@ -4685,12 +4833,12 @@
"sessionId": "{{child-1}}",
"event": {
"type": "session-log-deepseek/delivery-accepted",
- "seq": 14,
+ "seq": 17,
"time": 0,
"data": {
"sessionId": "{{child-1}}",
"sessionFormatVersion": 3,
- "throughSeq": 13
+ "throughSeq": 16
}
}
}
@@ -4701,7 +4849,7 @@
"sessionId": "{{child-1}}",
"event": {
"type": "assistant/message",
- "seq": 15,
+ "seq": 18,
"time": 0,
"data": {
"turn": 1,
@@ -4815,7 +4963,7 @@
"sessionId": "{{child-1}}",
"event": {
"type": "step/end",
- "seq": 16,
+ "seq": 19,
"time": 0,
"data": {
"turn": 1,
@@ -4830,7 +4978,7 @@
"sessionId": "{{child-1}}",
"event": {
"type": "turn/end",
- "seq": 17,
+ "seq": 20,
"time": 0,
"data": {
"turn": 1,
@@ -4871,7 +5019,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/result",
- "seq": 57,
+ "seq": 60,
"time": 0,
"data": {
"turn": 1,
@@ -4899,7 +5047,7 @@
}
},
"sourceEventSeqs": [
- 55
+ 58
],
"surfaceOp": "append"
}
@@ -4911,7 +5059,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/end",
- "seq": 58,
+ "seq": 61,
"time": 0,
"data": {
"turn": 1,
@@ -4926,7 +5074,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/start",
- "seq": 59,
+ "seq": 62,
"time": 0,
"data": {
"turn": 1,
@@ -4941,12 +5089,12 @@
"sessionId": "{{parent}}",
"event": {
"type": "session-log-deepseek/delivery-accepted",
- "seq": 60,
+ "seq": 63,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 59
+ "throughSeq": 62
}
}
}
@@ -4957,7 +5105,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "assistant/message",
- "seq": 61,
+ "seq": 64,
"time": 0,
"data": {
"turn": 1,
@@ -5087,7 +5235,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/call",
- "seq": 62,
+ "seq": 65,
"time": 0,
"data": {
"turn": 1,
@@ -5105,7 +5253,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool-workflow/run-start",
- "seq": 63,
+ "seq": 66,
"time": 0,
"data": {
"runId": "{{workflow-run}}",
@@ -5129,6 +5277,37 @@
"type": "agent/inbox/spliced",
"seq": 3,
"time": 0,
+ "data": {
+ "target": "next-step",
+ "start": 0,
+ "inserted": [
+ {
+ "id": "{{messageId}}",
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": "Serial agent creation completed."
+ }
+ ],
+ "source": {
+ "kind": "plugin",
+ "plugin": "serial-created-fixture"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ {
+ "method": "session.event",
+ "payload": {
+ "sessionId": "{{child-2}}",
+ "event": {
+ "type": "agent/inbox/spliced",
+ "seq": 4,
+ "time": 0,
"data": {
"target": "next-turn",
"start": 0,
@@ -5164,7 +5343,7 @@
"sessionId": "{{child-2}}",
"event": {
"type": "turn/start",
- "seq": 4,
+ "seq": 5,
"time": 0,
"data": {
"turn": 1
@@ -5178,7 +5357,24 @@
"sessionId": "{{child-2}}",
"event": {
"type": "agent/inbox/spliced",
- "seq": 5,
+ "seq": 6,
+ "time": 0,
+ "data": {
+ "target": "next-step",
+ "start": 0,
+ "removedCount": 1,
+ "inserted": []
+ }
+ }
+ }
+ },
+ {
+ "method": "session.event",
+ "payload": {
+ "sessionId": "{{child-2}}",
+ "event": {
+ "type": "agent/inbox/spliced",
+ "seq": 7,
"time": 0,
"data": {
"target": "next-turn",
@@ -5195,7 +5391,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "subagent/catalog",
- "seq": 64,
+ "seq": 67,
"time": 0,
"data": {
"version": 0,
@@ -5212,7 +5408,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool-workflow/agent-start",
- "seq": 65,
+ "seq": 68,
"time": 0,
"data": {
"runId": "{{workflow-run}}",
@@ -5230,7 +5426,7 @@
"sessionId": "{{child-2}}",
"event": {
"type": "subagent/descriptor",
- "seq": 6,
+ "seq": 8,
"time": 0,
"data": {
"version": 3,
@@ -5246,7 +5442,7 @@
"sessionId": "{{child-2}}",
"event": {
"type": "step/start",
- "seq": 7,
+ "seq": 9,
"time": 0,
"data": {
"turn": 1,
@@ -5261,7 +5457,7 @@
"sessionId": "{{child-2}}",
"event": {
"type": "system/message",
- "seq": 8,
+ "seq": 10,
"time": 0,
"data": {
"turn": 1,
@@ -5291,7 +5487,33 @@
"sessionId": "{{child-2}}",
"event": {
"type": "user/message",
- "seq": 9,
+ "seq": 11,
+ "time": 0,
+ "data": {
+ "id": "{{messageId}}",
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": "Serial agent creation completed."
+ }
+ ],
+ "source": {
+ "kind": "plugin",
+ "plugin": "serial-created-fixture"
+ }
+ },
+ "surfaceOp": "append"
+ }
+ }
+ },
+ {
+ "method": "session.event",
+ "payload": {
+ "sessionId": "{{child-2}}",
+ "event": {
+ "type": "user/message",
+ "seq": 12,
"time": 0,
"data": {
"content": [
@@ -5316,7 +5538,7 @@
"sessionId": "{{child-2}}",
"event": {
"type": "user/message",
- "seq": 10,
+ "seq": 13,
"time": 0,
"data": {
"content": [
@@ -5357,7 +5579,7 @@
"sessionId": "{{child-2}}",
"event": {
"type": "request/header",
- "seq": 11,
+ "seq": 14,
"time": 0,
"data": {
"header": {
@@ -5398,7 +5620,7 @@
"sessionId": "{{child-2}}",
"event": {
"type": "request/context",
- "seq": 12,
+ "seq": 15,
"time": 0,
"data": {
"provider": "deepseek-official",
@@ -5414,12 +5636,12 @@
"sessionId": "{{child-2}}",
"event": {
"type": "session/title",
- "seq": 13,
+ "seq": 16,
"time": 0,
"data": {
"title": "Reply with exactly WORKFLOW_CHILD_OK and",
"messageSeqs": [
- 9
+ 12
],
"source": {
"kind": "fallback"
@@ -5434,12 +5656,12 @@
"sessionId": "{{child-2}}",
"event": {
"type": "session-log-deepseek/delivery-accepted",
- "seq": 14,
+ "seq": 17,
"time": 0,
"data": {
"sessionId": "{{child-2}}",
"sessionFormatVersion": 3,
- "throughSeq": 13
+ "throughSeq": 16
}
}
}
@@ -5450,7 +5672,7 @@
"sessionId": "{{child-2}}",
"event": {
"type": "assistant/message",
- "seq": 15,
+ "seq": 18,
"time": 0,
"data": {
"turn": 1,
@@ -5564,7 +5786,7 @@
"sessionId": "{{child-2}}",
"event": {
"type": "step/end",
- "seq": 16,
+ "seq": 19,
"time": 0,
"data": {
"turn": 1,
@@ -5579,7 +5801,7 @@
"sessionId": "{{child-2}}",
"event": {
"type": "turn/end",
- "seq": 17,
+ "seq": 20,
"time": 0,
"data": {
"turn": 1,
@@ -5620,7 +5842,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool-workflow/agent-end",
- "seq": 66,
+ "seq": 69,
"time": 0,
"data": {
"runId": "{{workflow-run}}",
@@ -5636,7 +5858,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool-workflow/run-end",
- "seq": 67,
+ "seq": 70,
"time": 0,
"data": {
"runId": "{{workflow-run}}",
@@ -5651,7 +5873,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/result",
- "seq": 68,
+ "seq": 71,
"time": 0,
"data": {
"turn": 1,
@@ -5679,7 +5901,7 @@
}
},
"sourceEventSeqs": [
- 62
+ 65
],
"surfaceOp": "append"
}
@@ -5691,7 +5913,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/end",
- "seq": 69,
+ "seq": 72,
"time": 0,
"data": {
"turn": 1,
@@ -5706,7 +5928,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/start",
- "seq": 70,
+ "seq": 73,
"time": 0,
"data": {
"turn": 1,
@@ -5721,12 +5943,12 @@
"sessionId": "{{parent}}",
"event": {
"type": "session-log-deepseek/delivery-accepted",
- "seq": 71,
+ "seq": 74,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 70
+ "throughSeq": 73
}
}
}
@@ -5737,7 +5959,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "assistant/message",
- "seq": 72,
+ "seq": 75,
"time": 0,
"data": {
"turn": 1,
@@ -5867,7 +6089,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/call",
- "seq": 73,
+ "seq": 76,
"time": 0,
"data": {
"turn": 1,
@@ -5885,7 +6107,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "tool/result",
- "seq": 74,
+ "seq": 77,
"time": 0,
"data": {
"turn": 1,
@@ -5913,7 +6135,7 @@
}
},
"sourceEventSeqs": [
- 73
+ 76
],
"surfaceOp": "append"
}
@@ -5925,7 +6147,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/end",
- "seq": 75,
+ "seq": 78,
"time": 0,
"data": {
"turn": 1,
@@ -5940,7 +6162,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/start",
- "seq": 76,
+ "seq": 79,
"time": 0,
"data": {
"turn": 1,
@@ -5955,7 +6177,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "system/message",
- "seq": 77,
+ "seq": 80,
"time": 0,
"data": {
"turn": 1,
@@ -5976,12 +6198,12 @@
}
},
"sourceEventSeqs": [
- 29
+ 32
],
"surfaceOp": {
"op": "replace",
- "startSeq": 29,
- "endSeq": 29
+ "startSeq": 32,
+ "endSeq": 32
}
}
}
@@ -5992,7 +6214,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "request/header",
- "seq": 78,
+ "seq": 81,
"time": 0,
"data": {
"header": {
@@ -6034,12 +6256,12 @@
"sessionId": "{{parent}}",
"event": {
"type": "session-log-deepseek/delivery-accepted",
- "seq": 79,
+ "seq": 82,
"time": 0,
"data": {
"sessionId": "{{parent}}",
"sessionFormatVersion": 3,
- "throughSeq": 78
+ "throughSeq": 81
}
}
}
@@ -6050,7 +6272,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "assistant/message",
- "seq": 80,
+ "seq": 83,
"time": 0,
"data": {
"turn": 1,
@@ -6164,7 +6386,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "step/end",
- "seq": 81,
+ "seq": 84,
"time": 0,
"data": {
"turn": 1,
@@ -6179,7 +6401,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "browser-use/stagehand-llm-request",
- "seq": 82,
+ "seq": 85,
"time": 0,
"data": {
"request": {
@@ -6258,10 +6480,10 @@
"sessionId": "{{parent}}",
"event": {
"type": "browser-use/stagehand-llm-result",
- "seq": 83,
+ "seq": 86,
"time": 0,
"data": {
- "requestSeq": 82,
+ "requestSeq": 85,
"blocks": [
{
"type": "tool-call",
@@ -6287,7 +6509,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "command/run",
- "seq": 84,
+ "seq": 87,
"time": 0,
"data": {
"commandId": "{{command:1}}",
@@ -6305,7 +6527,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "feedback/record",
- "seq": 85,
+ "seq": 88,
"time": 0,
"data": {
"text": "The session needs a clearer explanation."
@@ -6319,7 +6541,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "command/done",
- "seq": 86,
+ "seq": 89,
"time": 0,
"data": {
"commandId": "{{command:1}}",
@@ -6335,7 +6557,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "feedback/record",
- "seq": 87,
+ "seq": 90,
"time": 0,
"data": {
"category": "other"
@@ -6349,7 +6571,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "feedback/message-put",
- "seq": 88,
+ "seq": 91,
"time": 0,
"data": {
"sessionId": "{{parent}}",
@@ -6372,7 +6594,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "feedback/message-put",
- "seq": 89,
+ "seq": 92,
"time": 0,
"data": {
"sessionId": "{{parent}}",
@@ -6394,7 +6616,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "feedback/message-delete",
- "seq": 90,
+ "seq": 93,
"time": 0,
"data": {
"sessionId": "{{parent}}",
@@ -6409,7 +6631,7 @@
"sessionId": "{{parent}}",
"event": {
"type": "turn/end",
- "seq": 91,
+ "seq": 94,
"time": 0,
"data": {
"turn": 1,
diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v3.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v3.jsonl
index 8c5f74fd05..c90c08b7c6 100644
--- a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v3.jsonl
+++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v3.jsonl
@@ -2,18 +2,21 @@
{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}}
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin","plugin":"serial-created-fixture"}}]}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
{"type":"turn/start","data":{"turn":1}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Check direct child"}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"}
+{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin","plugin":"serial-created-fixture"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
-{"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[9],"source":{"kind":"fallback"}}}
-{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-1}}","sessionFormatVersion":3,"throughSeq":13}}
+{"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[12],"source":{"kind":"fallback"}}}
+{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-1}}","sessionFormatVersion":3,"throughSeq":16}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["DIRECT_CHILD_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v3.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v3.jsonl
index 58eed2574e..8080fe9950 100644
--- a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v3.jsonl
+++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v3.jsonl
@@ -2,18 +2,21 @@
{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}}
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin","plugin":"serial-created-fixture"}}]}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
{"type":"turn/start","data":{"turn":1}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn"}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"}
+{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin","plugin":"serial-created-fixture"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
-{"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[9],"source":{"kind":"fallback"}}}
-{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-2}}","sessionFormatVersion":3,"throughSeq":13}}
+{"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[12],"source":{"kind":"fallback"}}}
+{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-2}}","sessionFormatVersion":3,"throughSeq":16}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["WORKFLOW_CHILD_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl
index 7ce90f6331..4ef708d0a5 100644
--- a/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl
+++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl
@@ -2,64 +2,67 @@
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
{"type":"approval/policy","data":{"policy":"never"}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin","plugin":"serial-created-fixture"}}]}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
{"type":"turn/start","data":{"turn":1}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"}
+{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin","plugin":"serial-created-fixture"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}}],"source":{"kind":"plugin","plugin":"python-snapshot-image-offload"}},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
-{"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[8],"source":{"kind":"fallback"}}}
+{"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[11],"source":{"kind":"fallback"}}}
{"type":"assistant/attempt","data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"code":"IMAGE_OFFLOAD_REQUIRED","message":"authored image budget failure","offloadImages":1}}}}]}}
-{"type":"image/offload","data":{"targets":[{"seq":10,"imageIndexes":[0]}]}}
+{"type":"image/offload","data":{"targets":[{"seq":13,"imageIndexes":[0]}]}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"series"}}
-{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":16}}
+{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":19}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n ctx.on('tools/pre-execute', (exec, next) => {\\n if (exec.name !== 'snapshot_double' || exec.arguments.value !== -1) return next()\\n return {\\n kind: 'deny',\\n reason: 'Auto review rejected tool \\\"snapshot_double\\\"; its body was not executed',\\n info: {\\n name: 'AutoReviewDeniedError', code: 'AUTO_REVIEW_DENIED',\\n reason: ' transport raw\\\\r\\\\nreason ',\\n },\\n }\\n })\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-define","name":"cordis_define","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-define","args":["{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n ctx.on('tools/pre-execute', (exec, next) => {\\n if (exec.name !== 'snapshot_double' || exec.arguments.value !== -1) return next()\\n return {\\n kind: 'deny',\\n reason: 'Auto review rejected tool \\\"snapshot_double\\\"; its body was not executed',\\n info: {\\n name: 'AutoReviewDeniedError', code: 'AUTO_REVIEW_DENIED',\\n reason: ' transport raw\\\\r\\\\nreason ',\\n },\\n }\\n })\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n ctx.on('tools/pre-execute', (exec, next) => {\\n if (exec.name !== 'snapshot_double' || exec.arguments.value !== -1) return next()\\n return {\\n kind: 'deny',\\n reason: 'Auto review rejected tool \\\"snapshot_double\\\"; its body was not executed',\\n info: {\\n name: 'AutoReviewDeniedError', code: 'AUTO_REVIEW_DENIED',\\n reason: ' transport raw\\\\r\\\\nreason ',\\n },\\n }\\n })\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n ctx.on('tools/pre-execute', (exec, next) => {\\n if (exec.name !== 'snapshot_double' || exec.arguments.value !== -1) return next()\\n return {\\n kind: 'deny',\\n reason: 'Auto review rejected tool \\\"snapshot_double\\\"; its body was not executed',\\n info: {\\n name: 'AutoReviewDeniedError', code: 'AUTO_REVIEW_DENIED',\\n reason: ' transport raw\\\\r\\\\nreason ',\\n },\\n }\\n })\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}
-{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[19],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[22],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"step/start","data":{"turn":1,"step":2}}
-{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":22}}
+{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":25}}
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-run","name":"cordis_run","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-run","args":["{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}
-{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[28],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":2}}
{"type":"step/start","data":{"turn":1,"step":3}}
-{"type":"system/message","data":{"turn":1,"step":3,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{messageId}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}}
+{"type":"system/message","data":{"turn":1,"step":3,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{messageId}}"}},"sourceEventSeqs":[9],"surfaceOp":{"op":"replace","startSeq":9,"endSeq":9}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"change","startsSeries":true}}
-{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":30}}
+{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":33}}
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-code","args":["{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}
{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"snapshot_double","arguments":{"value":21}}}
{"type":"tool/ptc-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}}
-{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[33],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[36],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":3}}
{"type":"step/start","data":{"turn":1,"step":4}}
-{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":38}}
+{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":41}}
{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-denied-native","name":"snapshot_double","arguments":"{\"value\": -1}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-native","name":"snapshot_double","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-native","args":["{\"value\": -1}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-denied-native","name":"snapshot_double","arguments":"{\"value\": -1}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-denied-native","name":"snapshot_double","arguments":"{\"value\": -1}"}}
-{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-denied-native"},"content":[{"type":"tool-result","toolCallId":"advanced-denied-native","content":[{"type":"text","text":"Error: Auto review rejected tool \"snapshot_double\"; its body was not executed"}],"isError":true}],"role":"user","id":"{{messageId}}"},"error":{"name":"AutoReviewDeniedError","code":"AUTO_REVIEW_DENIED","reason":" transport raw\r\nreason "}},"sourceEventSeqs":[41],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-denied-native"},"content":[{"type":"tool-result","toolCallId":"advanced-denied-native","content":[{"type":"text","text":"Error: Auto review rejected tool \"snapshot_double\"; its body was not executed"}],"isError":true}],"role":"user","id":"{{messageId}}"},"error":{"name":"AutoReviewDeniedError","code":"AUTO_REVIEW_DENIED","reason":" transport raw\r\nreason "}},"sourceEventSeqs":[44],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":4}}
{"type":"step/start","data":{"turn":1,"step":5}}
-{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":44}}
+{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":47}}
{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-denied-ptc","name":"run_code","arguments":"{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-ptc","name":"run_code","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-denied-ptc","args":["{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-denied-ptc","name":"run_code","arguments":"{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-denied-ptc","name":"run_code","arguments":"{\"code\": \"try { await tools.snapshot_double({ value: -1 }) } catch (error) { return error.message }\", \"description\": \"Catch a structured inner tool denial\"}"}}
{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-denied-ptc","parentCallId":"advanced-denied-ptc","subCallId":"advanced-denied-ptc:ptc:1","name":"snapshot_double","arguments":{"value":-1}}}
{"type":"tool/ptc-dispatch","data":{"rootCallId":"advanced-denied-ptc","parentCallId":"advanced-denied-ptc","subCallId":"advanced-denied-ptc:ptc:1","name":"snapshot_double","arguments":{"value":-1},"isError":true,"error":{"name":"AutoReviewDeniedError","code":"AUTO_REVIEW_DENIED","reason":" transport raw\r\nreason "},"content":[{"type":"text","text":"Error: Auto review rejected tool \"snapshot_double\"; its body was not executed"}]}}
-{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-denied-ptc"},"content":[{"type":"tool-result","toolCallId":"advanced-denied-ptc","content":[{"type":"text","text":"Auto review rejected tool \"snapshot_double\"; its body was not executed"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[47],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-denied-ptc"},"content":[{"type":"tool-result","toolCallId":"advanced-denied-ptc","content":[{"type":"text","text":"Auto review rejected tool \"snapshot_double\"; its body was not executed"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[50],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":5}}
{"type":"step/start","data":{"turn":1,"step":6}}
-{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":52}}
+{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":55}}
{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-direct-child","args":["{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":6,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}
{"type":"subagent/catalog","data":{"version":0,"childId":"{{child-1}}","childCreatedAt":0,"mode":"one-shot","label":"Check direct child"}}
-{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[58],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":6}}
{"type":"step/start","data":{"turn":1,"step":7}}
-{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":59}}
+{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":62}}
{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-workflow","name":"workflow","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-workflow","args":["{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":7,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}
{"type":"tool-workflow/run-start","data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}}
@@ -67,22 +70,22 @@
{"type":"tool-workflow/agent-start","data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}}
{"type":"tool-workflow/agent-end","data":{"runId":"{{workflow-run}}","seq":1,"outcome":"completed"}}
{"type":"tool-workflow/run-end","data":{"runId":"{{workflow-run}}","stopReason":"completed"}}
-{"type":"tool/result","data":{"turn":1,"step":7,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[62],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":7,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[65],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":7}}
{"type":"step/start","data":{"turn":1,"step":8}}
-{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":70}}
+{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":73}}
{"type":"assistant/message","data":{"turn":1,"step":8,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-undefine","name":"cordis_undefine","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-undefine","args":["{\"pluginId\": \"snap-1\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":8,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}
-{"type":"tool/result","data":{"turn":1,"step":8,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[73],"surfaceOp":"append"}
+{"type":"tool/result","data":{"turn":1,"step":8,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[76],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":8}}
{"type":"step/start","data":{"turn":1,"step":9}}
-{"type":"system/message","data":{"turn":1,"step":9,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{messageId}}"}},"sourceEventSeqs":[29],"surfaceOp":{"op":"replace","startSeq":29,"endSeq":29}}
+{"type":"system/message","data":{"turn":1,"step":9,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{messageId}}"}},"sourceEventSeqs":[32],"surfaceOp":{"op":"replace","startSeq":32,"endSeq":32}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"change","startsSeries":true}}
-{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":78}}
+{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":81}}
{"type":"assistant/message","data":{"turn":1,"step":9,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}},"id":"{{message:1}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["ADVANCED_EXECUTABLE_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":9}}
{"type":"browser-use/stagehand-llm-request","data":{"request":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp","maxTokens":4096,"sessionId":"{{parent}}","system":"Extract the heading\n\nReturn the requested structured result by calling stagehand_result exactly once. Put the result in its result argument. Do not emit a text answer or call another tool.","messages":[{"role":"user","source":{"kind":"plugin","plugin":"browser-use-stagehand-native"},"content":[{"type":"text","text":"Page heading: Fixture heading"}],"id":"{{messageId}}"}],"tools":[{"name":"stagehand_result","description":"Return the structured result requested by Stagehand.","parameters":{"type":"object","properties":{"result":{"type":"object","properties":{"extraction":{"type":"string"}},"required":["extraction"],"additionalProperties":false}},"required":["result"],"additionalProperties":false}}]},"responseFormat":{"type":"json_schema","name":"extract","schema":{"type":"object","properties":{"extraction":{"type":"string"}},"required":["extraction"],"additionalProperties":false}}}}
-{"type":"browser-use/stagehand-llm-result","data":{"requestSeq":82,"blocks":[{"type":"tool-call","id":"structured-result","name":"stagehand_result","arguments":"{\"result\":{\"extraction\":\"Fixture heading\"}}"}],"finish":{"kind":"tool-calls"},"usage":{"inputTokens":3,"outputTokens":3}}}
+{"type":"browser-use/stagehand-llm-result","data":{"requestSeq":85,"blocks":[{"type":"tool-call","id":"structured-result","name":"stagehand_result","arguments":"{\"result\":{\"extraction\":\"Fixture heading\"}}"}],"finish":{"kind":"tool-calls"},"usage":{"inputTokens":3,"outputTokens":3}}}
{"type":"command/run","data":{"commandId":"{{command:1}}","name":"feedback","source":{"kind":"user"}}}
{"type":"feedback/record","data":{"text":"The session needs a clearer explanation."}}
{"type":"command/done","data":{"commandId":"{{command:1}}","kind":"success","text":"Feedback recorded for session {{parent}}\nAnonymous user: {{anonymous-user}}."}}
diff --git a/snapshots/sdk/serial-created/cordis.snapshot.yml b/snapshots/sdk/serial-created/cordis.snapshot.yml
new file mode 100644
index 0000000000..c5741704cc
--- /dev/null
+++ b/snapshots/sdk/serial-created/cordis.snapshot.yml
@@ -0,0 +1,20 @@
+# Keyless TypeScript SDK replay patch over the live profile patch.
+
+- id: llm-deepseek
+ name: '@deepseek-ai/dsh-llm-deepseek'
+ disabled: true
+
+- insert:
+ - id: llm-replay
+ name: '@deepseek-ai/dsh-llm-replay'
+ config:
+ providers:
+ - id: deepseek-official
+ name: DeepSeek
+ models:
+ - id: deepseek-v4-flash
+
+- id: session-log-deepseek
+ name: '@deepseek-ai/dsh-session-log-deepseek'
+ config:
+ enabled: true
diff --git a/snapshots/sdk/serial-created/cordis.yml b/snapshots/sdk/serial-created/cordis.yml
new file mode 100644
index 0000000000..1065247b4b
--- /dev/null
+++ b/snapshots/sdk/serial-created/cordis.yml
@@ -0,0 +1,38 @@
+# TypeScript SDK snapshot-record patch over `dsh --profile sdk`.
+
+# The fixture corpus must not depend on the developer's personal skill roots.
+# Ordinary product launches retain the sdk profile's default skill discovery.
+- id: skill-filesystem
+ name: '@deepseek-ai/dsh-skill-filesystem'
+ config:
+ includeDefaultRoots: false
+
+- id: llm-deepseek
+ name: '@deepseek-ai/dsh-llm-deepseek'
+ config:
+ thinking: enabled
+ reasoningEffort: max
+
+- id: session-persistence-jsonl
+ name: '@deepseek-ai/dsh-session-persistence-jsonl'
+ config:
+ root: !!js dshHomePath('sessions')
+ compression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
+
+- id: tool-subagent
+ name: '@deepseek-ai/dsh-tool-subagent'
+ config:
+ provider: spawn
+ toolName: subagent
+ backgroundMode: one-shot
+ enableRunInBackground: false
+ maxDepth: 1
+
+- id: session-log-deepseek
+ name: '@deepseek-ai/dsh-session-log-deepseek'
+ config:
+ enabled: true
+
+- insert:
+ - id: serial-created-fixture
+ name: '../../../packages/core/agent-loop/tests/fixtures/serial-created.mjs'
diff --git a/snapshots/sdk/serial-created/session.jsonl b/snapshots/sdk/serial-created/session.jsonl
new file mode 100644
index 0000000000..ff28de89da
--- /dev/null
+++ b/snapshots/sdk/serial-created/session.jsonl
@@ -0,0 +1,25 @@
+{"type":"session","version":0,"id":"{{session:1}}","createdAt":1785097381464,"cwd":"{{cwd}}","delegationDepth":0}
+{"type":"permission/preset","data":{"preset":"workspace-write"}}
+{"type":"sandbox/mode","data":{"mode":"workspace-write"}}
+{"type":"approval/policy","data":{"policy":"ask"}}
+{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly: SDK snapshot OK"}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}}
+{"type":"turn/start","data":{"turn":1}}
+{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","data":{"turn":1,"step":1}}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly: SDK snapshot OK"}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:2}}"},"surfaceOp":"append"}
+{"type":"session/title","data":{"title":"Reply with exactly: SDK snapshot","messageSeqs":[7],"source":{"kind":"fallback"}}}
+{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","throughSeq":11}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
+{"type":"reasoning-chunks","data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," \"","SD","K"," snapshot"," OK","\"."," Let"," me"," do"," that","."]}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
+{"type":"text-chunks","data":{"turn":1,"step":1,"index":1,"dt":[0,0,0],"texts":["SD","K"," snapshot"," OK"]}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly \"SDK snapshot OK\". Let me do that."}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"SDK snapshot OK"}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1769,"outputTokens":24,"cacheReadTokens":0,"reasoningTokens":19}}}}
+{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly \"SDK snapshot OK\". Let me do that."},{"type":"text","text":"SDK snapshot OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":1769,"outputTokens":24,"cacheReadTokens":0,"reasoningTokens":19}},"sourceEventSeqs":[13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41],"surfaceOp":"append"}
+{"type":"step/end","data":{"turn":1,"step":1}}
+{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/snapshots/sdk/serial-created/session.v3.jsonl b/snapshots/sdk/serial-created/session.v3.jsonl
new file mode 100644
index 0000000000..e022f3bf75
--- /dev/null
+++ b/snapshots/sdk/serial-created/session.v3.jsonl
@@ -0,0 +1,21 @@
+{"type":"session","version":3,"id":"{{session:1}}","createdAt":1785097381464,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0}
+{"type":"permission/preset","data":{"preset":"workspace-write"}}
+{"type":"sandbox/mode","data":{"mode":"workspace-write"}}
+{"type":"approval/policy","data":{"policy":"ask"}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{message:1}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin","plugin":"serial-created-fixture"}}]}}
+{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly: SDK snapshot OK"}],"source":{"kind":"user"},"role":"user","id":"{{message:2}}"}]}}
+{"type":"turn/start","data":{"turn":1}}
+{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
+{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","data":{"turn":1,"step":1}}
+{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{message:3}}"}},"surfaceOp":"append"}
+{"type":"user/message","data":{"id":"{{message:1}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin","plugin":"serial-created-fixture"}},"surfaceOp":"append"}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly: SDK snapshot OK"}],"source":{"kind":"user"},"role":"user","id":"{{message:2}}"},"surfaceOp":"append"}
+{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:4}}"},"surfaceOp":"append"}
+{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
+{"type":"session/title","data":{"title":"Reply with exactly: SDK snapshot","messageSeqs":[11],"source":{"kind":"fallback"}}}
+{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":3,"throughSeq":15}}
+{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly \"SDK snapshot OK\". Let me do that."},{"type":"text","text":"SDK snapshot OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":1769,"outputTokens":24,"cacheReadTokens":0,"reasoningTokens":19},"stream":[{"type":"chunk","time":1788943826008,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788943826008,"index":0,"dt":[0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," \"","SD","K"," snapshot"," OK","\"."," Let"," me"," do"," that","."]},{"type":"chunk","time":1788943826009,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788943826009,"index":1,"dt":[0,0,0],"texts":["SD","K"," snapshot"," OK"]},{"type":"chunk","time":1788943826009,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly \"SDK snapshot OK\". Let me do that."}}},{"type":"chunk","time":1788943826009,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"SDK snapshot OK"}}},{"type":"chunk","time":1788943826009,"chunk":{"type":"usage","usage":{"inputTokens":1769,"outputTokens":24,"cacheReadTokens":0,"reasoningTokens":19}}},{"type":"chunk","time":1788943826009,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
+{"type":"step/end","data":{"turn":1,"step":1}}
+{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/snapshots/sdk/serial-created/snapshot.yml b/snapshots/sdk/serial-created/snapshot.yml
new file mode 100644
index 0000000000..cdcd9729d1
--- /dev/null
+++ b/snapshots/sdk/serial-created/snapshot.yml
@@ -0,0 +1,10 @@
+version: 1
+scenario: serial-created
+profile: sdk
+composition: sdk-serial-created
+recording: authored
+header:
+ class: sdk-serial-created
+ pin: true
+ systemPromptSource: text-turn
+ toolSchemasSource: text-turn