From e63f22f2f1c63d2118c7f6f6eb59dd1d4e70be77 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Fri, 24 Jul 2026 10:27:05 +0800 Subject: [PATCH] temp commit --- ...subagent-catalog-and-list-agents.i18n.yaml | 6 +- ...urable-subagent-catalog-and-list-agents.md | 78 ++++++++++++------- ...ble-subagent-catalog-and-list-agents.zh.md | 78 ++++++++++++------- 3 files changed, 99 insertions(+), 63 deletions(-) diff --git a/.agents/notes/proposed/feature/2026-07-22-durable-subagent-catalog-and-list-agents.i18n.yaml b/.agents/notes/proposed/feature/2026-07-22-durable-subagent-catalog-and-list-agents.i18n.yaml index fee41ed170..73624514e5 100644 --- a/.agents/notes/proposed/feature/2026-07-22-durable-subagent-catalog-and-list-agents.i18n.yaml +++ b/.agents/notes/proposed/feature/2026-07-22-durable-subagent-catalog-and-list-agents.i18n.yaml @@ -1,6 +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 -2026-07-22-durable-subagent-catalog-and-list-agents.md: 47c99ee6171bbb64416eeb497146a8aa11ea6869 -2026-07-22-durable-subagent-catalog-and-list-agents.zh.md: e80e35eddefc050fa49c26cef88df56520eb58f2 +# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md +2026-07-22-durable-subagent-catalog-and-list-agents.md: b8bca3208541fa8154c3587db4c0520d6a2e3d04 +2026-07-22-durable-subagent-catalog-and-list-agents.zh.md: 6ee737212da52df21fd30c4fe78d680dbeaf8a5f diff --git a/.agents/notes/proposed/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md b/.agents/notes/proposed/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md index 47c99ee617..b8bca32085 100644 --- a/.agents/notes/proposed/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md +++ b/.agents/notes/proposed/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md @@ -6,39 +6,49 @@ English | [中文](2026-07-22-durable-subagent-catalog-and-list-agents.zh.md) ## Problem -Continuable background subagents expose a stable child id and persist the reconstruction descriptor in that child's session, so `send_message` can resume a known child without any listing operation. `list_agents` has a different requirement: after parent restart, enumerate only that parent's direct continuable children even when the caller no longer knows their ids. The durable child-handle and activation design is owned by [continuable background subagents](2026-07-21-continuable-background-subagents.md); this note owns enumeration and its model-facing query. +Continuable background subagents expose a stable child id and persist the reconstruction descriptor in that child's session, so `send_message` can resume a known child without any listing operation. `list_agents` has a different requirement: after parent restart, enumerate only that parent's direct continuable children even when the caller no longer knows their ids. The durable Session and Activation design is owned by [continuable subagents](../../implemented/feature/2026-07-28-continuable-subagent-conversations.md); this note owns enumeration and its model-facing query. -Enumeration must cross-check immutable session lineage, descriptor validity, and process-local activation state without loading or resuming an Agent merely to display it. It must also define how missing, corrupt, deleted, or unsupported children affect the list and whether repeatedly loading many child logs needs an index. +Enumeration must cross-check immutable session lineage, descriptor validity, and the live-preferred session corpus without loading or resuming an Agent merely to display it. It must also define how missing, corrupt, deleted, or unsupported children affect the list and whether repeatedly loading many child logs needs an index. ## Proposal -Treat parent-to-child enumeration and `list_agents` as one separately reviewed feature built on the durable child-handle contract. `SubagentControlService.listChildren(parent)` must: +Treat parent-to-child enumeration and `list_agents` as one separately reviewed feature built on the durable child-session contract. `SubagentService.listChildren(parent)` must: -- find materialized session headers whose `parentSession` identifies the caller's session; -- load and validate each candidate's `subagent/descriptor` event without activating the child; -- union those durable candidates with the parent's process-local Task associations, including active children that have not materialized yet; +- use `ctx.sessionQuery.traceSession(parent.session.id)` to obtain the caller's direct live-preferred child sessions; +- read and validate each candidate's `subagent/descriptor` event without activating the child; - omit one-shot children without a diagnostic, and omit a candidate that becomes unavailable or has a corrupt or unsupported descriptor with a per-child diagnostic; -- expose an inactive child as resumable only when its descriptor is valid and its provider is currently registered with `resume?()`; +- expose only children whose descriptor carries a durable creation `label`; +- report a live child as `running` and a persisted-only child as `complete`; - return every resulting child in stable `createdAt` ascending, child-id ascending order. -Descriptor format, persistence, by-id lookup, direct-parent authorization, and cold resume remain owned by the activation proposal. Listing consumes those facts but cannot weaken them or invent a second descriptor representation. +Descriptor persistence, by-id lookup, direct-parent authorization, and provider-independent cold resume remain owned by the implemented Activation contract. This proposal extends the descriptor with a durable `label` and requires listing to diagnose duplicate descriptor events; it cannot weaken the existing facts or invent a second descriptor representation. ### Enumeration decision -The first implementation uses `SessionPersistence.list()` to obtain materialized headers, filters on `SessionHeader.parentSession`, and unions those ids with Task associations owned by the parent. An associated child is resolved from the live association and is never passed to `SessionPersistence.load()`; only inactive direct-child candidates are loaded to fold their descriptors. The activation contract calls a preallocated id without a durable header and descriptor an **unmaterialized child**: by-id control reports an inactive instance as unavailable, but an active association still appears in `list_agents` as `running`. Once that Task becomes terminal, the child remains discoverable only if its durable descriptor validates. A materialized one-shot child lacks the descriptor and is excluded. This path requires no parent-session catalog event or new persistence backend. +The first implementation consumes `ctx.sessionQuery.traceSession(parent.session.id)` and considers only the trace's first-level descendants. Session query already merges `ctx.sessions` with `ctx.sessionPersistence` using live precedence, preserves immutable-header consistency, derives direct-child lineage from `SessionHeader.parentSession`, and sorts siblings by `createdAt` ascending and child id ascending. `listChildren()` does not reproduce that corpus logic or inspect the continuation manager's process-local Activation map. -This O(number of direct children) load path is the correctness baseline. If measured scale later requires an index, that index is derived state: session headers and child descriptors remain authoritative, and rebuilding or corruption fallback must reproduce the same results. An index cannot become a second authorization source or make an unmaterialized child visible. +Corpus construction precedes per-child descriptor inspection. A failure while building the initial trace, including persistence listing failure, a live/persisted header conflict anywhere in the observed corpus, or invalid target lineage, fails the whole `list_agents` call because no trustworthy candidate set exists. Only failures after a successful trace are isolated to one candidate; "corrupt child" in that per-child contract therefore means corrupt loaded event surface or descriptor data, not a corpus-level header conflict. -`SessionPersistence.load()` may durably repair an interrupted child log by appending synthetic closing events. The first version accepts this existing persistence side effect: `listChildren()` creates no Agent and appends no catalog or descriptor event itself, but it is not a storage-read-only operation. It reads the model-hidden descriptor retained in the child log by the activation contract, so compacted and uncompacted children must enumerate identically. +Session lineage is broader than subagent identity: an ordinary `ctx.sessions.fork()` and a one-shot subagent also create direct children. Each candidate must therefore contain exactly one valid `subagent/descriptor` event. The activation contract writes it only during initial creation and cold resume appends no further descriptor; a second event is corruption rather than evidence of another activation. The event distinguishes a continuable background subagent from an ordinary fork or one-shot child; its short creation `label` comes from the delegation's `description`, while its continuation fields remain the reconstruction input for provider-independent cold resume. A candidate without the event is omitted without a diagnostic. + +The published logical record is also the status source: `SessionRecord.live` means `running`, while `live: false, persisted: true` means `complete`. `complete` means that no Activation is live; it encodes neither successful completion nor a permanently closed child, and `send_message` may materialize another Activation. Conversely, `running` says only that the session is live: a live Agent outside the continuation manager's matching Activation still appears as `running`, but `send_message` rejects rather than adopting it. A child is not visible before its session is published, and no process-local Activation entry is added as a second candidate or status source. Listing is a snapshot that may race publication, disposal, or a later message; `send_message` remains the authoritative delivery-time operation. + +The subagent service keeps `sessionQuery` optional so start and follow-up remain available without it. Its public `listChildren()` method resolves the optional service when called and throws `SubagentError` with stable code `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` before doing any work when it is absent. `@deepseek-ai/dsh-tool-subagent-control` exports separately loadable tool plugins: the `send_message` adapter requires only `subagents`, while the `list_agents` adapter requires both `subagents` and `sessionQuery` at load. A deployment may therefore use `send_message` without loading session query; the list tool catches misconfiguration at plugin load, while another direct service consumer receives the same explicit call-time contract. + +This descriptor-read path is the correctness baseline, not a claim that work is linear only in the number of direct children. Let D be the number of direct-child candidates, C the number of persisted sessions scanned by each persistence listing, and L_i the size of candidate i's full log. One corpus trace is followed by two exact reads per candidate. `listChildren()` uses `sessionQuery.listEvents(childId)` to locate the sole descriptor event and `sessionQuery.readEvent({ sessionId: childId, seq })` to read it, and each operation independently loads the logical session. In the persisted-only worst case, every exact read repeats `persistence.list()`, loads the full child log, and clones its events, for O(D × C + Σ L_i) work up to constant factors; a live child instead takes two detached in-memory snapshots of its full log. The persisted path may durably repair an interrupted child log by appending synthetic closing events. The first version accepts the repeated reads as the no-index correctness baseline, but deployments must treat total corpus and child-log size—not only direct-child count—as the capacity constraint. Listing creates no Agent and appends no catalog or descriptor event itself, but it is not a storage-read-only operation. The model-hidden descriptor remains outside the conversation surface and survives compaction, so compacted and uncompacted children must enumerate identically. + +If measured scale later requires an index, that index is derived state: session headers and child descriptors remain authoritative, and rebuilding or corruption fallback must reproduce the same results. An index cannot become a second authorization source or make an unpublished child visible. ### `list_agents` contract -`SubagentControlService.listChildren(parent)` returns all direct continuable children in the union of durable candidates and active Task associations, plus non-fatal diagnostics for inactive candidates it could not load, validate, or resume. An association records its creation time when the control service allocates the child id; a materialized child uses `SessionHeader.createdAt`. Children are sorted by that `createdAt` ascending, then child id ascending. Diagnostics follow their candidate's same key. The model-facing `list_agents` tool takes no arguments and is a thin adapter in `@deepseek-ai/dsh-tool-subagent-control`; it renders the complete sorted children and diagnostics together, and reports two operational child states: +`SubagentService.listChildren(parent)` returns every direct continuable child found in the session trace plus non-fatal diagnostics for candidates it could not read or validate. Each child carries its session id, descriptor `label`, and one of two snapshot states: -- `running`: a non-terminal Task-backed activation exists, including startup before materialization and settlement before Task terminal publication; -- `resumable`: no activation is associated, a valid durable descriptor exists, and the named provider is currently registered with `resume?()`. +- `running`: the logical session record is live in `ctx.sessions`; +- `complete`: the logical session record exists only in persistence and may be resumed by `send_message`. -These values are not `AgentStatus`. A plain Agent registry entry without a Task association is an ownership conflict, not a third list state. Inactive candidates use three diagnostic reasons: `corrupt` for malformed committed data or descriptor content, `unsupported` for an unknown descriptor version, and `unavailable` when the candidate disappears, another child-specific load fails, or its provider is absent or lacks `resume?()`. Each diagnostic identifies the child id and reason without exposing model-hidden descriptor content; the candidate is omitted while healthy siblings remain visible. Failure of the initial `SessionPersistence.list()` operation fails the whole call because no candidate set exists. Headers whose `parentSession` names another parent are filtered before descriptor loading and produce no diagnostic. +These values are neither `AgentStatus` nor the manager's internal Activation state. Children are sorted by `SessionHeader.createdAt` ascending, then child id ascending; diagnostics follow their candidate's same key. The model-facing `list_agents` tool takes no arguments and is a thin adapter in `@deepseek-ai/dsh-tool-subagent-control` that renders the complete sorted children and diagnostics together. + +Diagnostics use three fixed reasons. Malformed event surfaces, conflicting headers discovered during an exact child load, malformed descriptor content, and multiple descriptor events map to `corrupt`. An unknown descriptor version maps to `unsupported`. `SESSION_QUERY_SESSION_NOT_FOUND`, `SESSION_QUERY_EVENT_NOT_FOUND`, and `SESSION_QUERY_PERSISTENCE_FAILED` from a per-child read map to `unavailable`. This phase boundary is intentional: a persistence outage during the initial trace fails the operation, while the same outage beginning during candidate reads may produce one identical `unavailable` diagnostic per affected child; v1 neither coalesces those diagnostics nor promotes them to a global failure. A missing descriptor is instead a one-shot exclusion without a diagnostic. Configuration/window errors and unrecognized failures are not child diagnostics and propagate as operation failures. Each diagnostic identifies the child id and reason without exposing model-hidden descriptor content; the candidate is omitted while healthy siblings remain visible. Sessions outside the trace's direct descendants are never read and produce no diagnostic. The first version has no child deletion operation. If later product behavior deletes child sessions, persistence listing naturally drops a deleted child; any future derived index must remove or tombstone the same entry so `list_agents` cannot retain stale state. @@ -46,9 +56,13 @@ The first version has no child deletion operation. If later product behavior del **Fold listing into the activation RFC.** Descriptor-by-id persistence and cold resume do not require parent-to-child enumeration. Keeping the query separate lets `send_message` land without taking on listing states, scanning performance, or deletion behavior. -**List every persisted session whose header names the parent.** `parentSession` proves lineage but does not prove that the child is continuable. Listing must also load and validate the descriptor. +**Rebuild lineage directly from `SessionPersistence.list()`.** This duplicates session query's live-preferred corpus merge, immutable-header consistency checks, direct-child tracing, and deterministic ordering. Listing uses the existing trusted query service and adds only subagent-specific descriptor validation and rendering. -**Use the live Agent registry as the catalog.** Runs are deliberately disposed after every Task, and registry state disappears on restart. It cannot support durable discovery. +**List every traced child session.** `parentSession` proves lineage but does not prove that the child is a continuable subagent: ordinary session forks and one-shot subagents share that header field. Listing must also read and validate the descriptor. + +**Use the live Agent registry as the catalog.** Activations are deliberately disposed after settlement, and registry state disappears on restart. It cannot support durable discovery. + +**Use the process-local Activation map as a second catalog.** This exposes manager residency but couples a session-discovery query to materialization and settlement, introduces another ordering clock, and makes the same child change candidate source during its lifetime. The first version lists published logical sessions only and treats `SessionRecord.live` as its snapshot status. **Persist a parent-session catalog event.** Direct-child headers already provide the durable enumeration seed, and the child descriptor is the reconstruction authority. A second parent log duplicates state and creates cross-session ordering and stale-entry behavior without helping by-id resume. @@ -60,20 +74,24 @@ The first version has no child deletion operation. If later product behavior del ## Acceptance criteria -- Durable enumeration uses materialized session headers as candidates, validates `parentSession`, and includes only inactive children whose persisted descriptor satisfies the durable child-handle contract; the final result unions those children with parent-owned active associations. -- Listing loads no Agent and appends no catalog or descriptor event itself, but may trigger `SessionPersistence.load()` interrupted-tail repair for inactive children; an already-associated child is never loaded, and compacted and uncompacted logs return the same children. -- `list_agents` takes no arguments and returns all valid direct continuable children plus per-child diagnostics, sorted by `createdAt` ascending and child id ascending. -- Active Task associations appear as `running` even before durable materialization; after Task terminal, the child appears as `resumable` only when its descriptor validates and its currently registered provider implements `resume?()`. -- `list_agents` reports no pass-through runtime status, uses only `corrupt`, `unsupported`, or `unavailable` diagnostic reasons, and never exposes descriptor contents in a diagnostic. -- Parent resume does not activate children; listing reads durable state and overlays only already-associated process-local Tasks. -- A preallocated-but-unmaterialized child id, one-shot child, corrupt descriptor, unsupported descriptor version, and stale derived-index entry are never advertised as resumable; non-child headers are filtered before load. -- A corrupt, unsupported, disappeared, or unloadable candidate cannot hide healthy siblings: it is omitted with an id-and-reason diagnostic, while failure of the initial persistence listing fails the whole call. -- Keyless tests cover fresh and compacted discovery, active unmaterialized children, transition from running association to durable resume, provider absence, stable ordering, restart, parent-header prefiltering, isolated child diagnostics, load repair, scan behavior, and stale-index fallback. The model-facing complete-list-plus-diagnostics result has runnable snapshot coverage. +- Enumeration uses `ctx.sessionQuery.traceSession(parent.session.id)`, considers only direct descendants, and does not duplicate corpus merging, lineage reconstruction, or sibling ordering. +- Listing loads no Agent, materializes no Activation, and appends no catalog or descriptor event itself. After the initial trace it performs two independent exact session-query reads per candidate; persisted reads may trigger interrupted-tail repair, and compacted and uncompacted logs return the same children. +- A valid descriptor includes the delegation's durable `label`; ordinary session forks and one-shot children lack that descriptor and are omitted without a diagnostic. Provider registration does not affect discovery or provider-independent cold resume. +- Initial creation writes exactly one descriptor event, cold resume writes none, and a candidate with more than one descriptor event is diagnosed as `corrupt`. +- `list_agents` takes no arguments and returns every valid direct continuable child with its id, label, and `running` or `complete` snapshot state, plus per-child diagnostics, sorted by `createdAt` ascending and child id ascending. +- A live logical session is `running`; a persisted-only logical session is `complete` and remains eligible for a later `send_message`. The result does not consult the process-local Activation map. +- Parent resume does not activate children. A child is absent until its session is published, and listing may race publication, disposal, or later delivery without weakening `send_message`'s execution-time checks. +- `list_agents` uses only `corrupt`, `unsupported`, or `unavailable` diagnostic reasons and never exposes descriptor contents in a diagnostic. +- After a successful initial trace, a corrupt, unsupported, disappeared, or unreadable descriptor candidate cannot hide healthy siblings: it is omitted with an id-and-reason diagnostic. Corpus-level persistence, header-consistency, or lineage failure during that initial trace fails the whole call. +- Per-child session-query failures map deterministically: invalid surfaces and exact-load source conflicts are `corrupt`; missing sessions or events and persistence failures are `unavailable`; unknown descriptor versions are `unsupported`; and missing descriptors are omitted as one-shot children. +- The list tool requires `sessionQuery` at plugin load; a direct `listChildren()` call without it fails before enumeration with `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE`, while by-id `send_message` remains usable without that service. +- Keyless tests cover fresh and compacted discovery, ordinary fork and one-shot exclusion, live-to-complete transition, unmanaged-live-session snapshots, provider-independent discovery, durable `label` values, stable ordering, restart, direct-child tracing, duplicate descriptor rejection, isolated child diagnostics, phase-dependent persistence failure, load repair, snapshot races, and scan behavior. The model-facing complete-list-plus-diagnostics result has runnable snapshot coverage. ## Risks -- Listing performs one header scan and may load every direct-child log; a later derived index must preserve the same authorization, per-child diagnostic, and fallback behavior. -- Listing may repair interrupted child logs and persist synthetic closing events even though it creates no Agent. This is the existing `SessionPersistence.load()` contract, not a hidden catalog write. -- The first version has no deletion operation, so persisted children remain listed for as long as their sessions remain in persistence even though live Agent resources remain bounded by active Tasks. +- Session tracing observes the complete logical corpus, then descriptor validation reads each direct-child log twice. In the persisted-only worst case, work is O(D × C + Σ L_i), not merely O(D), because each exact read rescans persistence and loads and clones the full candidate log. A later derived index must preserve the same authorization, per-child diagnostic, and fallback behavior. +- Corpus construction is an all-or-nothing trust boundary: one live/persisted header conflict can fail the initial trace and hide otherwise healthy siblings. Per-child isolation begins only after that trace succeeds. +- Session-query reads may repair interrupted child logs and persist synthetic closing events even though listing creates no Agent. This is the existing persistence-load contract, not a hidden catalog write. +- The first version has no deletion operation, so persisted children remain listed for as long as their sessions remain in persistence even though live Agent resources remain bounded by resident Activations. - The no-argument tool returns every direct continuable child and diagnostic. Stable ordering makes the result deterministic but does not bound model-context growth; pagination or deletion remains a later product decision. -- Task associations exist only in one runtime. Another process can report a durable child as `resumable` while work for that child is active elsewhere unless the deployment adds a shared lease. +- `running` and `complete` are process-local corpus snapshots, not delivery promises. Another process may activate a persisted child while this process reports it as `complete`; cross-process accuracy requires a shared lease. diff --git a/.agents/notes/proposed/feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md b/.agents/notes/proposed/feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md index e80e35edde..6ee737212d 100644 --- a/.agents/notes/proposed/feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md +++ b/.agents/notes/proposed/feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md @@ -6,39 +6,49 @@ Status: proposed ## 问题 -可继续的后台 subagent 会公开稳定的 child id,并将重建描述符持久化在该 child 的会话中,因此 `send_message` 无需任何列表查询操作即可恢复已知 child。`list_agents` 的要求不同:parent 重启后,即使调用方不再知道各 child id,也要只枚举该 parent 的直接可继续 child。[可继续的后台 subagent](2026-07-21-continuable-background-subagents.md)负责持久化 child handle 与激活设计;本记录负责枚举及其面向模型的查询。 +可继续的后台 subagent 会公开稳定的 child id,并将重建描述符持久化在该 child 的会话中,因此 `send_message` 无需任何列表查询操作即可恢复已知 child。`list_agents` 的要求不同:parent 重启后,即使调用方不再知道各 child id,也要只枚举该 parent 的直接可继续 child。[可继续 subagent](../../implemented/feature/2026-07-28-continuable-subagent-conversations.md)负责持久化 Session 与 Activation 设计;本记录负责枚举及其面向模型的查询。 -枚举必须交叉核对不可变的会话谱系、描述符有效性与进程内激活状态,而不能仅为展示就加载或恢复 Agent。它还必须定义缺失、损坏、已删除或不受支持的 child 如何影响列表,以及反复加载大量 child 日志是否需要索引。 +枚举必须交叉核对不可变的会话谱系、描述符有效性与实时优先的会话语料,而不能仅为展示就加载或恢复 Agent。它还必须定义缺失、损坏、已删除或不受支持的 child 如何影响列表,以及反复加载大量 child 日志是否需要索引。 ## 提案 -将 parent 到 child 的枚举与 `list_agents` 作为一个基于持久化 child handle 契约、单独评审的功能。`SubagentControlService.listChildren(parent)` 必须: +将 parent 到 child 的枚举与 `list_agents` 作为一个基于持久化 child Session 契约、单独评审的功能。`SubagentService.listChildren(parent)` 必须: -- 查找 `parentSession` 将调用方会话标识为 parent 的已实际落盘会话 header; -- 加载并校验每个候选会话的 `subagent/descriptor` 事件,但不激活 child; -- 将这些持久化候选与 parent 的进程内 Task 关联合并,包括尚未实际落盘的活跃 child; +- 使用 `ctx.sessionQuery.traceSession(parent.session.id)` 获取调用方直接且实时优先的 child 会话; +- 读取并校验每个候选会话的 `subagent/descriptor` 事件,但不激活 child; - 排除一次性 child 且不产生 diagnostic;如果候选在枚举后变得不可用,或其描述符损坏或版本不受支持,则排除该候选并产生对应 child 的 diagnostic; -- 仅当非活跃 child 的描述符有效,且其提供方当前已注册并实现 `resume?()` 时,才将它对外标记为 `resumable`; +- 只公开描述符带有持久化创建 `label` 的 child; +- 将存活 child 报告为 `running`,只存在于持久化存储中的 child 报告为 `complete`; - 按 `createdAt` 升序、再按 child id 升序稳定返回所有结果 child。 -描述符格式、持久化、按 id 查找、直接 parent 鉴权与从持久化存储恢复仍由激活提案负责。列表查询消费这些事实,但不能削弱它们,也不能另行发明第二种描述符表示。 +描述符持久化、按 id 查找、直接 parent 鉴权和不依赖提供方的冷恢复仍归已实现的 Activation 契约负责。本提案会为描述符增加持久化 `label`,并要求列表查询诊断重复的描述符事件;它不能削弱现有事实,也不能发明第二种描述符表示。 ### 枚举决策 -第一版使用 `SessionPersistence.list()` 获取已实际落盘的 header,按 `SessionHeader.parentSession` 过滤,再将这些 id 与 parent 拥有的 Task 关联合并。已关联的 child 直接从存活关联中解析,绝不会传给 `SessionPersistence.load()`;只有非活跃的直接 child 候选才会被加载以归并其描述符。激活契约将已预分配 id、却没有持久化 header 和描述符的 child 称为 **unmaterialized child**:按 id 的控制操作会报告非活跃实例不可用,但活跃关联仍会在 `list_agents` 中显示为 `running`。该 Task 进入终态后,只有在持久化描述符通过校验时,这个 child 才会继续可被发现。已实际落盘的一次性 child 没有描述符,因此会被排除。这条路径无需 parent 会话目录事件或新的持久化后端。 +第一版消费 `ctx.sessionQuery.traceSession(parent.session.id)`,并且只考虑追踪结果的第一层后代。会话查询已经使用实时优先规则合并 `ctx.sessions` 与 `ctx.sessionPersistence`,保持不可变 header 一致性,根据 `SessionHeader.parentSession` 推导直接 child 谱系,并按 `createdAt` 升序、child id 升序排列 sibling。`listChildren()` 不会重复实现这套语料逻辑,也不会检查继续执行管理器的进程内 Activation map。 -这条 O(直接 child 数量)加载路径是正确性基线。如果实测规模日后需要索引,该索引属于派生状态:会话 header 和 child 描述符仍是权威信息,重建或损坏回退必须复现相同结果。索引不能成为第二个鉴权来源,也不能让尚未实际落盘的 child 变得可见。 +语料构建先于逐 child 描述符检查。构建初始追踪时如果发生持久化列表查询失败、所观测语料中任意位置的存活/持久化 header 冲突或目标谱系无效,整个 `list_agents` 调用都会失败,因为此时不存在可信的候选集。只有初始追踪成功后的失败才会被隔离到单个候选;因此,这项逐 child 契约中的“损坏 child”是指已加载的事件 surface 或描述符数据损坏,而不是语料级 header 冲突。 -`SessionPersistence.load()` 可能通过追加合成的结束事件,持久修复中断的 child 日志。第一版接受这项现有的持久化副作用:`listChildren()` 不会创建 Agent,也不会自行追加目录或描述符事件,但它并非严格的存储只读操作。它读取激活契约保留在 child 日志中、对模型隐藏的描述符,因此经过压缩和未经压缩的 child 必须枚举出相同结果。 +会话谱系涵盖的范围比 subagent 身份更广:普通 `ctx.sessions.fork()` 和一次性 subagent 也会创建直接 child。因此,每个候选都必须恰好包含一个有效的 `subagent/descriptor` 事件。激活契约只在初始创建期间写入该事件,从持久化存储恢复时不会追加其他描述符;第二个事件属于损坏,而不是另一次激活的证据。该事件用于区分可继续的后台 subagent 与普通 fork 或一次性 child;其简短创建 `label` 来自委派的 `description`,其余继续执行字段仍是不依赖提供方的冷恢复所使用的重建输入。缺少该事件的候选会被排除,且不产生 diagnostic。 + +已发布的逻辑记录同时也是状态来源:`SessionRecord.live` 表示 `running`,而 `live: false, persisted: true` 表示 `complete`。`complete` 表示当前没有存活的 Activation,既不表示执行成功,也不表示 child 已永久关闭;`send_message` 仍可物化另一次 Activation。反过来,`running` 只表示会话存活:位于继续执行管理器对应 Activation 之外的存活 Agent 仍会显示为 `running`,但 `send_message` 会拒绝,而不会接管它。child 会话发布前不可见,也不会添加进程内 Activation 条目作为第二个候选来源或状态来源。列表查询是一份快照,可能与发布、dispose 或后续消息发生竞态;`send_message` 仍是消息送达时的权威操作。 + +subagent 服务将 `sessionQuery` 保持为可选依赖,因此没有该服务时仍可执行 start 和 follow-up。其公开的 `listChildren()` 方法在调用时解析这个可选服务;如果服务缺失,该方法会在执行任何工作前抛出 `SubagentError`,并携带稳定错误码 `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE`。`@deepseek-ai/dsh-tool-subagent-control` 导出可分别加载的工具插件:`send_message` 适配器只要求 `subagents`,而 `list_agents` 适配器在加载时同时要求 `subagents` 和 `sessionQuery`。因此,部署可以在不加载会话查询的情况下使用 `send_message`;列表工具会在插件加载时捕获配置错误,而其他直接服务消费方会收到同一项明确的调用时契约。 + +这条描述符读取路径是正确性基线,并不声称工作量只与直接 child 数量呈线性关系。令 D 为直接 child 候选数量,C 为每次持久化列表查询所扫描的持久化会话数量,L_i 为候选 i 的完整日志大小。一次语料追踪后,每个候选会执行两次精确读取。`listChildren()` 使用 `sessionQuery.listEvents(childId)` 定位唯一的描述符事件,并使用 `sessionQuery.readEvent({ sessionId: childId, seq })` 读取该事件;每项操作都会独立加载逻辑会话。对于只存在于持久化存储中的最坏情况,每次精确读取都会重复执行 `persistence.list()`、加载完整 child 日志并克隆其中的事件,因此忽略常数因子后的工作量为 O(D × C + Σ L_i);存活 child 则会对其完整日志取得两份分离的内存快照。持久化路径可能通过追加合成的结束事件,持久修复中断的 child 日志。第一版接受这些重复读取,将其作为无索引的正确性基线,但部署必须将语料总量和 child 日志大小,而不仅是直接 child 数量,视为容量约束。列表查询不会创建 Agent,也不会自行追加目录或描述符事件,但它并非严格的存储只读操作。对模型隐藏的描述符始终位于对话 surface 之外,并且会在压缩后保留,因此经过压缩和未经压缩的 child 必须枚举出相同结果。 + +如果实测规模日后需要索引,该索引属于派生状态:会话 header 和 child 描述符仍是权威信息,重建或损坏回退必须复现相同结果。索引不能成为第二个鉴权来源,也不能让尚未发布的 child 变得可见。 ### `list_agents` 契约 -`SubagentControlService.listChildren(parent)` 返回持久化候选与活跃 Task 关联并集中的所有直接可继续 child,以及无法加载、校验或恢复非活跃候选时产生的非致命 diagnostic。控制服务分配 child id 时,关联会记录其创建时间;已实际落盘的 child 则使用 `SessionHeader.createdAt`。这些 child 先按该 `createdAt` 升序、再按 child id 升序排序,diagnostic 使用其候选的同一排序键。面向模型的 `list_agents` 工具不接受参数,它是 `@deepseek-ai/dsh-tool-subagent-control` 中的轻量适配器;它会一并渲染完整的已排序 child 和 diagnostic,并报告两种 child 操作状态: +`SubagentService.listChildren(parent)` 返回会话追踪中找到的每个直接可继续 child,以及无法读取或校验候选时产生的非致命 diagnostic。每个 child 都携带自己的 session id、描述符 `label`,以及两种快照状态之一: -- `running`:存在由非终态 Task 支撑的激活,包括实际落盘前的启动阶段和 Task 终态发布前的结算阶段; -- `resumable`:没有关联任何激活,存在有效的持久化描述符,且其指定的提供方当前已注册并实现 `resume?()`。 +- `running`:逻辑会话记录在 `ctx.sessions` 中存活; +- `complete`:逻辑会话记录只存在于持久化存储中,并且可以由 `send_message` 恢复。 -这些值并非 `AgentStatus`。普通 Agent 注册表中没有 Task 关联的条目属于所有权冲突,而不是第三种列表状态。非活跃候选使用三种固定的 diagnostic 原因:格式错误的已提交数据或描述符内容使用 `corrupt`,未知描述符版本使用 `unsupported`,候选消失、出现其他逐 child 加载失败、其提供方缺失或未实现 `resume?()` 时使用 `unavailable`。每条 diagnostic 都标识 child id 及原因,不暴露对模型隐藏的描述符内容;系统会排除该候选,而其他健康的 sibling 仍然可见。如果初始 `SessionPersistence.list()` 操作失败,因为系统无法获得候选集,整次调用都会失败。`parentSession` 指向其他 parent 的 header 会在加载描述符前被过滤,且不产生 diagnostic。 +这些值既不是 `AgentStatus`,也不是管理器内部的 Activation 状态。child 按 `SessionHeader.createdAt` 升序、再按 child id 升序排序;diagnostic 使用其候选的同一排序键。面向模型的 `list_agents` 工具不接受参数,它是 `@deepseek-ai/dsh-tool-subagent-control` 中的轻量适配器,会一并渲染完整的已排序 child 和 diagnostic。 + +diagnostic 使用三种固定原因。格式错误的事件 surface、精确加载 child 时发现的 header 冲突、格式错误的描述符内容和多个描述符事件映射为 `corrupt`。未知描述符版本映射为 `unsupported`。逐 child 读取产生的 `SESSION_QUERY_SESSION_NOT_FOUND`、`SESSION_QUERY_EVENT_NOT_FOUND` 和 `SESSION_QUERY_PERSISTENCE_FAILED` 映射为 `unavailable`。这项阶段边界是有意为之:初始追踪期间发生持久化故障会让操作失败,而同一故障如果始于候选读取期间,可能会让每个受影响的 child 分别产生一条相同的 `unavailable` diagnostic;第一版既不合并这些 diagnostic,也不会把它们提升为全局失败。缺少描述符则视为一次性 child,直接排除且不产生 diagnostic。配置错误、窗口错误和未识别的失败不属于 child diagnostic,会作为操作失败继续向上传播。每条 diagnostic 都标识 child id 及原因,不暴露对模型隐藏的描述符内容;系统会排除该候选,而其他健康的 sibling 仍然可见。系统绝不会读取不属于追踪结果直接后代的会话,也不会为它们产生 diagnostic。 第一版不提供 child 删除操作。如果后续产品行为会删除 child 会话,持久化列表会自然移除已删除的 child;任何未来的派生索引都必须移除或 tombstone 同一条目,避免 `list_agents` 保留陈旧状态。 @@ -46,9 +56,13 @@ Status: proposed **将列表查询并入激活 RFC。** 按 id 持久化描述符和从持久化存储恢复无需 parent 到 child 的枚举。保持查询独立,可让 `send_message` 落地时不必同时承担列表状态、扫描性能或删除行为。 -**枚举 header 中以该 parent 为 parent 的每个持久化会话。** `parentSession` 能证明谱系,却不能证明 child 可继续。列表查询还必须加载并校验描述符。 +**直接通过 `SessionPersistence.list()` 重建谱系。** 这种做法会重复实现会话查询中的实时优先语料合并、不可变 header 一致性检查、直接 child 追踪和确定性排序。列表查询应使用现有可信查询服务,只增加 subagent 特有的描述符校验与渲染。 -**使用存活的 Agent 注册表作为目录。** 系统会在每个 Task 结束后有意 dispose 对应 run,而且注册表状态会在重启时消失,因此无法支持持久化发现。 +**列出每个已追踪的 child 会话。** `parentSession` 能证明谱系,却不能证明 child 是可继续的 subagent:普通会话 fork 和一次性 subagent 也使用这个 header 字段。列表查询还必须读取并校验描述符。 + +**使用存活的 Agent 注册表作为目录。** 系统会在 Activation 结算后有意 dispose 它,而且注册表状态会在重启时消失,因此无法支持持久化发现。 + +**使用进程内 Activation map 作为第二个目录。** 这种做法能公开管理器驻留状态,却会让会话发现查询与物化及结算耦合,引入另一套排序时钟,并让同一个 child 在其生命周期内改变候选来源。第一版只列出已经发布的逻辑会话,并将 `SessionRecord.live` 视为其快照状态。 **持久化 parent 会话目录事件。** 直接 child header 已经提供持久化枚举种子,child 描述符则是重建的权威信息。第二份 parent 日志会重复状态,并造成跨会话顺序和陈旧条目行为,却无助于按 id 恢复。 @@ -60,20 +74,24 @@ Status: proposed ## 验收标准 -- 持久化枚举使用已实际落盘的会话 header 作为候选,校验 `parentSession`,并且只包含持久化描述符满足持久化 child handle 契约的非活跃 child;最终结果会将这些 child 与 parent 拥有的活跃关联合并。 -- 列表查询不加载 Agent,也不会自行追加目录或描述符事件,但可能对非活跃 child 触发 `SessionPersistence.load()` 的中断尾部修复;已关联的 child 绝不会被加载,且经过压缩和未经压缩的日志会返回相同的 child。 -- `list_agents` 不接受参数,返回所有有效的直接可继续 child 及逐 child diagnostic,并按 `createdAt` 升序、child id 升序排序。 -- 活跃 Task 关联即使尚未实际落盘,也会显示为 `running`;Task 进入终态后,只有在描述符校验通过,且当前注册的提供方实现 `resume?()` 时,child 才会显示为 `resumable`。 -- `list_agents` 不直接透传运行时状态,只使用 `corrupt`、`unsupported` 或 `unavailable` 作为 diagnostic 原因,且绝不在 diagnostic 中暴露描述符内容。 -- 恢复 parent 不会激活 child;列表查询读取持久化状态,并且只叠加已经关联的进程内 Task。 -- 已预分配但尚未实际落盘的 child id、一次性 child、损坏描述符、不受支持的描述符版本和陈旧的派生索引条目绝不会被标记为可恢复;非 child header 会在加载前被过滤。 -- 损坏、不受支持、已消失或无法加载的候选不能隐藏健康的 sibling:系统会排除该候选,并生成一条含 id 和原因的 diagnostic;只有初始持久化列表查询失败时,整次调用才会失败。 -- 无密钥测试覆盖压缩前后的发现、活跃的尚未实际落盘 child、从正在运行的关联转换为持久化恢复、提供方缺失、稳定排序、重启、parent header 预过滤、单个 child diagnostic 隔离、加载修复、扫描行为和陈旧索引回退。面向模型的完整列表加 diagnostic 结果具有可运行的快照覆盖。 +- 枚举使用 `ctx.sessionQuery.traceSession(parent.session.id)`,只考虑直接后代,并且不重复实现语料合并、谱系重建或 sibling 排序。 +- 列表查询不会加载 Agent、物化 Activation,也不会自行追加目录或描述符事件。初始追踪完成后,它会对每个候选执行两次相互独立的会话查询精确读取;持久化读取可能触发中断尾部修复,且经过压缩和未经压缩的日志会返回相同的 child。 +- 有效描述符包含委派的持久化 `label`;普通会话 fork 和一次性 child 缺少该描述符,因此会被排除且不产生 diagnostic。提供方注册状态不影响发现,也不影响不依赖提供方的冷恢复。 +- 初始创建恰好写入一个描述符事件,从持久化存储恢复时不写入任何描述符;如果候选包含多个描述符事件,则将其诊断为 `corrupt`。 +- `list_agents` 不接受参数,返回每个有效的直接可继续 child 及其 id、label 和 `running` 或 `complete` 快照状态,并返回逐 child diagnostic;结果按 `createdAt` 升序、child id 升序排序。 +- 存活的逻辑会话为 `running`;只存在于持久化存储中的逻辑会话为 `complete`,并且仍可在之后通过 `send_message` 恢复。结果不查询进程内 Activation map。 +- 恢复 parent 不会激活 child。child 会话发布前不会出现,列表查询可能与发布、dispose 或后续消息送达发生竞态,但不会削弱 `send_message` 在执行时进行的检查。 +- `list_agents` 只使用 `corrupt`、`unsupported` 或 `unavailable` 作为 diagnostic 原因,且绝不在 diagnostic 中暴露描述符内容。 +- 初始追踪成功后,描述符损坏、不受支持、已消失或无法读取的候选不能隐藏健康的 sibling:系统会排除该候选,并生成一条含 id 和原因的 diagnostic。初始追踪期间发生的语料级持久化、header 一致性或谱系失败会让整次调用失败。 +- 逐 child 会话查询失败采用固定映射:无效 surface 和精确加载时的来源冲突映射为 `corrupt`;会话或事件缺失以及持久化失败映射为 `unavailable`;未知描述符版本映射为 `unsupported`;缺少描述符则作为一次性 child 排除。 +- 列表工具在插件加载时要求 `sessionQuery`;直接调用 `listChildren()` 时如果缺少该服务,则会在枚举前以 `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` 失败,而按 id 的 `send_message` 在没有该服务时仍可使用。 +- 无密钥测试覆盖压缩前后的发现、排除普通 fork 和一次性 child、从存活到 complete 的转换、未受管理的存活会话快照、不依赖提供方的发现、持久化 `label` 值、稳定排序、重启、直接 child 追踪、重复描述符拒绝、单个 child diagnostic 隔离、依阶段而异的持久化失败、加载修复、快照竞态和扫描行为。面向模型的完整列表加 diagnostic 结果具有可运行的快照覆盖。 ## 风险 -- 列表查询会扫描一次 header,并且可能加载每个直接 child 的日志;后续的派生索引必须保持相同的鉴权、逐 child diagnostic 和回退行为。 -- 列表查询可能修复中断的 child 日志并持久化合成的结束事件,即使它不创建 Agent。这是 `SessionPersistence.load()` 的现有契约,而非隐藏的目录写入。 -- 第一版没有删除操作,因此只要 child 会话仍保留在持久化存储中,它们就会继续出现在列表里,但存活 Agent 资源仍由活跃 Task 数量限制。 +- 会话追踪会观察完整的逻辑语料,随后描述符校验会读取每个直接 child 的日志两次。对于只存在于持久化存储中的最坏情况,工作量为 O(D × C + Σ L_i),而不只是 O(D),因为每次精确读取都会重新扫描持久化存储,并加载和克隆候选的完整日志。后续的派生索引必须保持相同的鉴权、逐 child diagnostic 和回退行为。 +- 语料构建是一个全有或全无的信任边界:一处存活/持久化 header 冲突就可能导致初始追踪失败,并隐藏原本健康的 sibling。只有初始追踪成功后,逐 child 隔离才会生效。 +- 会话查询读取可能修复中断的 child 日志并持久化合成的结束事件,即使列表查询不创建 Agent。这是现有的持久化加载契约,而非隐藏的目录写入。 +- 第一版没有删除操作,因此只要 child 会话仍保留在持久化存储中,它们就会继续出现在列表里,但存活 Agent 资源仍由驻留 Activation 数量限制。 - 无参数工具会返回每个直接可继续 child 和 diagnostic。稳定排序可使结果确定,但不会限制模型上下文的增长;分页或删除仍是后续的产品决策。 -- Task 关联仅存在于一个运行时中。除非部署添加共享租约,否则当另一个进程正在处理某个持久化 child 时,当前进程仍可能将其报告为 `resumable`。 +- `running` 和 `complete` 是进程内语料快照,而非消息送达承诺。另一个进程可能在当前进程将某个持久化 child 报告为 `complete` 时激活它;跨进程准确性需要共享租约。