From 3bf6d738f13dc0e7860551caaadb340b20c68e7e Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Wed, 23 Sep 2026 14:16:57 +0800 Subject: [PATCH] fix(agent-team): keep late activation on normal reconnect path --- .../feature/2026-08-05-agent-teams.i18n.yaml | 4 +- .../feature/2026-08-05-agent-teams.md | 2 +- .../feature/2026-08-05-agent-teams.zh.md | 2 +- apps/web/tests/plugin-manager.e2e.ts | 3 ++ docs/event-producer-consumer.i18n.yaml | 4 +- docs/event-producer-consumer.md | 1 - docs/event-producer-consumer.zh.md | 1 - docs/subsystems/session-projection.i18n.yaml | 4 +- docs/subsystems/session-projection.md | 20 -------- docs/subsystems/session-projection.zh.md | 20 -------- .../api/session-controller/README.i18n.yaml | 4 +- packages/api/session-controller/README.md | 2 - packages/api/session-controller/README.zh.md | 2 - .../api/session-controller/src/control.ts | 3 -- .../tests/control-queue.host.spec.ts | 51 ------------------- .../client-ui-agent-team/README.i18n.yaml | 4 +- .../client-ui-agent-team/README.md | 1 + .../client-ui-agent-team/README.zh.md | 1 + .../extensions/tool-cordis/src/api-catalog.ts | 8 --- .../session-projection/README.i18n.yaml | 4 +- packages/session/session-projection/README.md | 2 - .../session/session-projection/README.zh.md | 2 - .../session/session-projection/src/index.ts | 13 +---- scripts/gen-cordis-catalog.ts | 1 - 24 files changed, 20 insertions(+), 139 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml b/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml index b17a826f3a..4eb9df8cb0 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.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-08-05-agent-teams.md -2026-08-05-agent-teams.md: 6f271b18fbf7498dd60098ca4f1ef610a398700a -2026-08-05-agent-teams.zh.md: 6e9a36b07167e661382b129079b6a98d88c0aa53 +2026-08-05-agent-teams.md: 6333e511ba1e71823cfa55c27ba46b2665d1ef15 +2026-08-05-agent-teams.zh.md: 3fed1302146406f71d63c7ff2df8f031a91c9804 diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.md b/.agents/notes/implemented/feature/2026-08-05-agent-teams.md index 6f271b18fb..6333e511ba 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.md +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.md @@ -56,7 +56,7 @@ Worktree isolation is not a harness runtime behavior. A deployment or prompt may ## Web projection -The Web panel reads the Lead Session's `agentTeam` wire projection because the existing projection stream keeps browser state current without a separate Team read API. Both Lead and teammate conversations select values from the shared cross-Session store. The panel owns no projection reads: Session loading, cached list values, and control baselines supply the data, and changes to registered wire keys replace control baselines for connected clients. +The Web panel reads the Lead Session's `agentTeam` wire projection because the existing projection stream keeps browser state current without a separate Team read API. Both Lead and teammate conversations select values from the shared cross-Session store. The panel owns no projection reads: Session loading, cached list values, and control baselines supply the data. The wire view carries durable roster phases, non-deleted task views with shared readiness and overlap derivation, and `failure` beside the last valid state. Live activity comes from Session status. Model selection has its own durable projection; the panel displays the shared store's available values without opening member conversations. A missing cached value remains absent until Session loading or a live update supplies it. diff --git a/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md b/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md index 6e9a36b071..3fed130214 100644 --- a/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md +++ b/.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md @@ -56,7 +56,7 @@ Worktree isolation 不是 harness runtime 行为。deployment 或 prompt 可以 ## Web projection -Web panel 读取 Lead Session 的 `agentTeam` 传输投影,因为现有投影流能持续更新浏览器状态,无需独立的 Team 读取 API。Lead 与 teammate 会话均从共享的跨 Session store 选择值。面板不负责投影读取:Session 加载、列表缓存值与控制基线提供数据,已注册传输 key 的变化会为已连接客户端替换控制基线。 +Web panel 读取 Lead Session 的 `agentTeam` 传输投影,因为现有投影流能持续更新浏览器状态,无需独立的 Team 读取 API。Lead 与 teammate 会话均从共享的跨 Session store 选择值。面板不负责投影读取:Session 加载、列表缓存值与控制基线提供数据。 传输视图携带持久 roster phase、使用共享就绪与重叠派生逻辑的未删除任务视图,以及最后有效状态旁的 `failure`。实时活动来自 Session 状态。模型选择有自己的持久投影;面板展示共享 store 中可用的值,不打开成员会话。缺少的缓存值保持缺失,直到 Session 加载或实时更新提供它。 diff --git a/apps/web/tests/plugin-manager.e2e.ts b/apps/web/tests/plugin-manager.e2e.ts index 19a9c96e4d..05ff6010ab 100644 --- a/apps/web/tests/plugin-manager.e2e.ts +++ b/apps/web/tests/plugin-manager.e2e.ts @@ -280,6 +280,9 @@ describe('web e2e: plugin manager', () => { await action.waitFor({ timeout: 20_000 }) await action.getByRole('button', { name: /Agent Team/iu }).click() const teamPanel = teamPage.getByRole('dialog', { name: 'Agent Team', exact: true }) + await teamPanel.getByText('Team 暂不可用', { exact: true }).waitFor() + await teamPage.reload({ waitUntil: 'load' }) + await action.getByRole('button', { name: /Agent Team/iu }).click() await teamPanel.getByText('还没有共享任务').waitFor() await teamPanel.getByText('lead', { exact: true }).waitFor() const manifest = JSON.parse(await homeFile('profiles', 'scaffold', 'package.json')) as { diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index 295ce3768e..a6e1582653 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: 3b259e4c32d5de56e6f97b214b5fd50ca3af8e6f -event-producer-consumer.zh.md: 3f2d1527919b819aec8d61a8e932876196caa673 +event-producer-consumer.md: 1d844013f1230d6dbe535c3942279683f03738ce +event-producer-consumer.zh.md: 73438c3f19ef6850674a27bf8f490fe4cf52b420 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 3b259e4c32..1d844013f1 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -55,7 +55,6 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:228`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:234`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:241`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `session-projection/definitions-changed` | `emit` | [`packages/session/session-projection/src/index.ts:39`](../packages/session/session-projection/src/index.ts) | [`session-projection`](../packages/session/session-projection) (`emit`) | `session-controller` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:53`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 3f2d152791..73438c3f19 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -57,7 +57,6 @@ | `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:228`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:234`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:241`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `session-projection/definitions-changed` | `emit` | [`packages/session/session-projection/src/index.ts:39`](../packages/session/session-projection/src/index.ts) | [`session-projection`](../packages/session/session-projection) (`emit`) | `session-controller` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:53`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | diff --git a/docs/subsystems/session-projection.i18n.yaml b/docs/subsystems/session-projection.i18n.yaml index 699f3162a0..cc738c4fcb 100644 --- a/docs/subsystems/session-projection.i18n.yaml +++ b/docs/subsystems/session-projection.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/session-projection.md -session-projection.md: 4cc68aab0e94d155b5f6736f61fdc2394e7bf622 -session-projection.zh.md: fd6e0428456a4893bcf15e7899baec4ea01f8295 +session-projection.md: 8580b94c4d7dd36f942ff10d77e13d2dc70b7e8b +session-projection.zh.md: b98d348a36c51aab973bc3397ddbf744860245a4 diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md index 4cc68aab0e..8580b94c4d 100644 --- a/docs/subsystems/session-projection.md +++ b/docs/subsystems/session-projection.md @@ -352,25 +352,5 @@ hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly Se Types: [Session](session.md) · [SessionEvent](session.md) · [SessionHeader](persistence.md) · [SessionLogOffset](session.md) -Source: [`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts) - - - -### `session-projection/*` events - - - -#### `session-projection/definitions-changed` — emit - -The registered client-visible keys changed; carriers must replace their baselines. - -```ts cordis-catalog -/** - * The registered client-visible keys changed; carriers must replace their baselines. - * @mode emit - */ -'session-projection/definitions-changed'(): void -``` - Source: [`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts) diff --git a/docs/subsystems/session-projection.zh.md b/docs/subsystems/session-projection.zh.md index fd6e042845..b98d348a36 100644 --- a/docs/subsystems/session-projection.zh.md +++ b/docs/subsystems/session-projection.zh.md @@ -352,25 +352,5 @@ hydrate( session: Session, checkpoint: ProjectionCheckpoint, events: readonly Se Types: [Session](session.zh.md) · [SessionEvent](session.zh.md) · [SessionHeader](persistence.zh.md) · [SessionLogOffset](session.zh.md) -Source: [`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts) - - - -### `session-projection/*` events - - - -#### `session-projection/definitions-changed` — emit - -The registered client-visible keys changed; carriers must replace their baselines. - -```ts cordis-catalog -/** - * The registered client-visible keys changed; carriers must replace their baselines. - * @mode emit - */ -'session-projection/definitions-changed'(): void -``` - Source: [`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts) diff --git a/packages/api/session-controller/README.i18n.yaml b/packages/api/session-controller/README.i18n.yaml index 3a1d8a748c..c283462c19 100644 --- a/packages/api/session-controller/README.i18n.yaml +++ b/packages/api/session-controller/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/api/session-controller/README.md -README.md: 08c68da01ab88e500970186cf799b33f9926289b -README.zh.md: a9f5878dc8bcf7134dd0d658ec477fe21ecbcf1e +README.md: fa2a251e94cfa2855790d7d71457ac4bdf2ff752 +README.zh.md: d1a73ed5843fddadfc2f401fee1a86eece4e5272 diff --git a/packages/api/session-controller/README.md b/packages/api/session-controller/README.md index 08c68da01a..fa2a251e94 100644 --- a/packages/api/session-controller/README.md +++ b/packages/api/session-controller/README.md @@ -45,8 +45,6 @@ Accepted prompts and observed running keep their Client display conversion acros The Client adapter exposes `SessionEventStream`, a Gateway `RemoteJournalStream` bound to one ordinary or direct-subagent address. It opens follow before the initial page, publishes only contiguous `replace`, `prepend`, `append`, and `settle-assistant` changes, and repairs reconnect or sequence gaps through a tail page. Backwards paging has two verbs: `loadOlder()` pulls one Turn-aligned page, and `loadThrough(seq)` — the turn-jump loader — loops Turn-aligned pages until the window covers the target seq, lowering a shared target on repeated calls, stopping on a page that makes no progress, and reporting busy through the same `loadingOlder` snapshot bit. The Web adapter explicitly opts into cursorless Assistant frames: each opening carries the active attempt's `startedAfterSeq`, `nextIndex`, and compact stream, and every stream member becomes a Client-only `assistant/live-chunk` entry ordered between durable cursors. The Host captures a follower-local arrival ordinal with that baseline and suppresses buffered frames at or before the cut; a replacement Agent may restart frame revision at one. A durable `assistant/message` or `assistant/attempt` arriving after an active opening stays staged only when its seq follows `startedAfterSeq` and its Turn and Step match; the matching end type, seq, and index publishes one named settlement delta that retires the attempt's transient rows and adds the durable entry while earlier same-step retries remain visible. Revision, dense-index, or settlement gaps for a known attempt reopen follow, while a controller that missed the start ignores unknown-attempt frames and publishes their durable settlement normally. An abandoned end publishes a settlement delta without a durable entry so its transient rows retire immediately. A durable gap-repair page has no Assistant baseline, so its held notification reopens follow once for a paired page and baseline. Every history record covers exactly its event seq. A business, persistence, or unresolved continuity failure terminates the stream, while only physical carrier loss selects automatic resumption. `SessionControlStream` is a Gateway `RemoteSnapshotStream`; every generation opens with a complete process-local baseline, so reconnect replaces projection state instead of treating transient values as durable events. At each ready Host generation, a synchronous Client subscription clears retained projection values and watermarks before refreshing queries and restarting the control stream, including Sessions absent from the control baseline. The first control stream waits for generation readiness, so its opening values cannot precede invalidation. Outstanding list responses from the previous generation cannot republish those values. Within a generation, a delayed control baseline cannot overwrite or clear newer sequenced values, whether they arrived through a live Session's list block, a history page, or a frame; a cached list block, viewed from the projection cache for a cold Session, yields to the connected Session's baseline whatever watermark it carries. The durable `inbox` projection carries both pending lists through the same cold-read and reconnect path as other projections. Client Agent contexts provide the identity used by the independent [`fileUpload`](../../client/file-upload/README.md) service; Session objects expose lifecycle, prompt, queue, and history operations rather than file transfer. -The control stream also sends a complete baseline when the registered client-visible projection keys change. Already-connected browsers receive late capability registration and removal without issuing a projection read. - The Session object also carries local submission echoes: `session.beginSubmission` inserts one into `SessionSnapshot.pendingSubmissions` synchronously, before the caller serializes and prompts, so a conversation UI can show the message on the submit click's own frame. The echo stores ordered image previews and durable file references. Session derives its `transcript`, `queued`, or `steering` placement from the current running state and requested delivery mode, then retains that placement until the display handoff. The prompt's `requestId` is the correlation identity: the Host echoes it as the durable user source's `rpcId`, including pending messages in the `inbox` projection. Queued echoes retire one animation frame after queue acceptance; Chat echoes follow the admission rule below. Identified failures and abandonment retire unadmitted echoes immediately. Disposal preserves observed admission and fails other unsettled submissions. Each retirement fires `onRetire` exactly once; an observed retirement includes the ordered durable attachment references so the composer can release successful cards while preserving failed drafts. Echoes are Client memory only; reload and reconnect rebuild the conversation from durable events alone. diff --git a/packages/api/session-controller/README.zh.md b/packages/api/session-controller/README.zh.md index a9f5878dc8..d1a73ed584 100644 --- a/packages/api/session-controller/README.zh.md +++ b/packages/api/session-controller/README.zh.md @@ -45,8 +45,6 @@ Client 列表刷新保留未变化的行对象,并在顺序和值均相同时 Client 适配器提供 `SessionEventStream`,即绑定到一个普通 Session 或 direct subagent address 的 Gateway `RemoteJournalStream`。它在读取首个 page 前打开 follow,只发布连续的 `replace`、`prepend`、`append` 与 `settle-assistant` 变更,并通过 tail page 修复重连或 seq 缺口。向后分页有两个动词:`loadOlder()` 拉一页按轮次开头对齐的历史,而 `loadThrough(seq)`——轮次跳转加载器——按轮次开头对齐页面循环拉取直到窗口覆盖目标 seq,重复调用会下调共享目标,遇到无进展的页即停止,忙碌状态复用同一个 `loadingOlder` 快照位。Web 适配器显式选择接收无 cursor 的 Assistant frame:每个 opening 携带活跃 attempt 的 `startedAfterSeq`、`nextIndex` 与紧凑 stream,每个 stream member 都成为排在持久 cursor 之间的 Client-only `assistant/live-chunk` 条目。Host 会随该 baseline 捕获 follower 本地到达序号,并抑制该 cut 及之前的 buffered frame;replacement Agent 可以从 revision 一重新开始。活跃 opening 之后到达的持久 `assistant/message` 或 `assistant/attempt` 只有在其 seq 晚于 `startedAfterSeq` 且轮次与步骤匹配时才会保持暂存;匹配的 end type、seq 与 index 会发布一个具名 settlement delta,删除该 attempt 的瞬态 row、加入持久条目,并保留同一步骤中更早的 retry。已知 attempt 的 revision、密集 index 或 settlement 缺口会重新打开 follow;若 controller 错过 start,则忽略 unknown-attempt frame,并正常发布其持久 settlement。Abandoned end 会发布不含持久条目的 settlement delta,使瞬态 row 立即退出。持久缺口修复 page 不携带 Assistant baseline,因此 held notification 会重新打开 follow 一次,以取得配对的 page 与 baseline。每条历史 record 只覆盖自身的事件 seq。业务、persistence 或无法恢复的连续性错误会终止 stream,只有物理载体断开才触发自动恢复。`SessionControlStream` 是 Gateway `RemoteSnapshotStream`;每代都以完整的进程本地 baseline 开始,因此重连会替换 projection 状态,而不会把瞬态值当作 durable event。每次 Host generation 就绪时,同步的 Client 订阅会先清除保留的投影值及其水位,再刷新查询并重新打开 control stream,其中也包括 control baseline 中没有列出的 Session。首次 control stream 会等待 generation 就绪,确保其 opening 值不会先于旧状态清理到达。上一代尚未完成的 list 响应无法重新发布这些值。同一 generation 内,延迟到达的 control baseline 不能覆盖或清除较新的 sequenced 值,无论它们来自活 Session 的 list block、history page 还是 frame;冷 Session 从 projection cache 看出来的 cached list block 则不论水位都让位于建连 Session 的 baseline。持久 `inbox` 投影通过与其他投影相同的冷读取和重连路径传输两份待处理列表。Client Agent 上下文提供独立 [`fileUpload`](../../client/file-upload/README.zh.md) 服务使用的身份;Session 对象提供生命周期、prompt、queue 与历史操作,不提供文件传输。 -客户端可见投影 key 的注册集合变化时,控制流也发送完整基线。已连接浏览器无需发起投影读取,即可收到能力的晚注册与移除。 - Session 对象还承载本地提交回显:`session.beginSubmission` 在调用方序列化与提示词之前,同步把一条回显写入 `SessionSnapshot.pendingSubmissions`,会话 UI 因此能在点击提交的当帧显示消息。回显按顺序存放图片预览与持久文件引用。Session 根据当前运行状态与请求的投递模式推导其 `transcript`、`queued` 或 `steering` 位置,并保留该位置直到展示接管。提示词的 `requestId` 是关联标识:Host 把它回显为 durable user source 的 `rpcId`,`inbox` 投影中的待处理消息也保留同一 source。排队回显在队列接受后延迟一个动画帧退休;Chat 回显遵循下述入档规则。尚未入档的回显在带标识的提示词失败或被放弃时立即退休。销毁时保留已观察到的入档结果,其余未结算提交按 failed 退休。每次退休恰好触发一次 `onRetire`;observed 退休还会携带有序的持久附件引用,让 composer 释放成功卡片并保留失败草稿。回显只存在于 Client 内存;刷新与重连只从持久事件重建会话。 diff --git a/packages/api/session-controller/src/control.ts b/packages/api/session-controller/src/control.ts index 48132e1e22..8d27396d29 100644 --- a/packages/api/session-controller/src/control.ts +++ b/packages/api/session-controller/src/control.ts @@ -19,9 +19,6 @@ export class SessionControlController { /** @param ctx - Host context carrying live Agent and projection services. */ constructor(private readonly ctx: Context) { - ctx.on('session-projection/definitions-changed', () => { - if (this.streams.size > 0) this.broadcast({ type: 'baseline', value: this.baseline() }) - }) ctx.sessionProjections.onChanged((session, key, value, seq) => { this.broadcast({ type: 'projection', diff --git a/packages/api/session-controller/tests/control-queue.host.spec.ts b/packages/api/session-controller/tests/control-queue.host.spec.ts index 60c0a19dcb..ad24ad94fb 100644 --- a/packages/api/session-controller/tests/control-queue.host.spec.ts +++ b/packages/api/session-controller/tests/control-queue.host.spec.ts @@ -1,4 +1,3 @@ -import { z } from 'zod' import { Context } from '@deepseek-ai/cordis' import type { Agent, Inbox, InboxState } from '@deepseek-ai/dsh-agent' import { createUserMessage } from '@deepseek-ai/dsh-llm' @@ -18,15 +17,6 @@ declare module '@deepseek-ai/dsh-llm' { } } -declare module '@deepseek-ai/dsh-session-projection/types' { - interface SessionProjectionMap { - 'test/late-capability': number - } - interface SessionProjectionStateMap { - 'test/late-capability': number - } -} - const ownedContexts = new Set() afterEach(async () => { await Promise.all([...ownedContexts].map(ctx => ctx.fiber.dispose())) @@ -66,47 +56,6 @@ describe('Session control Inbox projection', () => { } } - it('updates connected clients when a wire capability is registered, shared, and removed', async () => { - const { ctx, control, agent } = await harness() - const empty = ctx.sessions.create(SessionId('empty-late-capability')) - const abort = new AbortController() - const iterator = control.control(abort.signal)[Symbol.asyncIterator]() - try { - await iterator.next() - const unit = { - key: 'test/late-capability' as const, - stateSchema: z.number(), - init: () => 7, - apply: (state: number) => state, - wire: { viewSchema: z.number(), view: (state: number) => state }, - stateVersion: 1, - } - const removeFirst = ctx.sessionProjections.register(unit) - const removeSecond = ctx.sessionProjections.register(unit) - removeFirst() - await expect(iterator.next()).resolves.toMatchObject({ value: { - type: 'baseline', value: { projections: { - [agent.session.id]: { values: { 'test/late-capability': 7 } }, - [empty.id]: { asOfSeq: -1, values: { 'test/late-capability': 7 } }, - } }, - } }) - removeSecond() - const removed = await iterator.next() - if (removed.done || removed.value.type !== 'baseline') throw new Error('missing removal baseline') - expect(removed.value.value.projections[agent.session.id]?.values).not.toHaveProperty('test/late-capability') - ctx.sessionProjections.register(unit) - await expect(iterator.next()).resolves.toMatchObject({ value: { - type: 'baseline', value: { projections: { - [agent.session.id]: { values: { 'test/late-capability': 7 } }, - [empty.id]: { asOfSeq: -1, values: { 'test/late-capability': 7 } }, - } }, - } }) - } finally { - abort.abort() - await iterator.return?.() - } - }) - it('projects both pending lists in baselines and live replacement frames', async () => { const { control, inbox } = await harness() const queued = message('queued') diff --git a/packages/experimental/client-ui-agent-team/README.i18n.yaml b/packages/experimental/client-ui-agent-team/README.i18n.yaml index 4895ad308e..c819bea01e 100644 --- a/packages/experimental/client-ui-agent-team/README.i18n.yaml +++ b/packages/experimental/client-ui-agent-team/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/experimental/client-ui-agent-team/README.md -README.md: c17f358332d3c61d1f6fa4dc5f4e4941d2dedec9 -README.zh.md: 66da22cd25a24aec1d9df72a2a7f85d126f379f3 +README.md: 364388796442ed71e128ee97b08a39b41982b41b +README.zh.md: 71ad88a6f8f8ecb203af733cd66b5f41ed863b03 diff --git a/packages/experimental/client-ui-agent-team/README.md b/packages/experimental/client-ui-agent-team/README.md index c17f358332..3643887964 100644 --- a/packages/experimental/client-ui-agent-team/README.md +++ b/packages/experimental/client-ui-agent-team/README.md @@ -86,6 +86,7 @@ No direct effect; the Team tools and ordinary conversation submission own any la - **No mailbox timeline** — the projection view carries roster and tasks only; peer messages are not shown. +- **Late plugin activation** — after enabling Agent Teams in an already-open conversation, reload the page to receive its Team projection. - **Model availability** — a model appears only when the shared store has a durable selection or request for that member. Missing cold-cache values stay absent until normal Session loading or a live update supplies them. - **Ordinary child continuation** — a human message sent after navigation uses the stable addressed-subagent prompt path, not the Team peer mailbox. - **No lifecycle or workspace controls** — the panel cannot spawn, rename, delete, or interrupt teammates, and write scopes remain advisory metadata. diff --git a/packages/experimental/client-ui-agent-team/README.zh.md b/packages/experimental/client-ui-agent-team/README.zh.md index 66da22cd25..71ad88a6f8 100644 --- a/packages/experimental/client-ui-agent-team/README.zh.md +++ b/packages/experimental/client-ui-agent-team/README.zh.md @@ -86,6 +86,7 @@ Client export 通过 Cordis effect 注册 locale dictionary 与一个 conversati - **没有 mailbox timeline**——投影视图只承载 roster 与任务;不显示 peer 消息。 +- **晚启用插件** — 在已经打开的会话中启用 Agent Teams 后,需要刷新页面才能接收其 Team 投影。 - **模型可用性** — 仅当共享 store 包含该成员的持久选择或请求时才显示模型。冷缓存中缺少的值会保持缺失,直到正常 Session 加载或实时更新提供它们。 - **普通 child continuation**——导航后发送的人类消息使用稳定 addressed-subagent 提示词路径,而不是 Team peer mailbox。 - **没有 lifecycle 或 workspace control**——panel 不能 spawn、rename、delete 或 interrupt teammate,write scope 仍只是提示性 metadata。 diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 9510f83378..2245f9ef68 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -3979,14 +3979,6 @@ export const EVENT_API: readonly EventApiEntry[] = [ description: 'An installation moved between its Host phases. `installing` is announced once per registry the installation asks, with the attempt\'s registry and position; `cancelling` and `applying` once.', parameters: [{ name: 'progress', description: 'the installation\'s request id and phase, with the attempt while installing.' }], }, - { - name: 'session-projection/definitions-changed', - mode: 'emit', - signature: '\'session-projection/definitions-changed\'(): void', - summary: 'The registered client-visible keys changed; carriers must replace their baselines.', - description: 'The registered client-visible keys changed; carriers must replace their baselines.', - parameters: [], - }, { name: 'session-telemetry/record', mode: 'waterfall', diff --git a/packages/session/session-projection/README.i18n.yaml b/packages/session/session-projection/README.i18n.yaml index 586532b276..0a5ccf8611 100644 --- a/packages/session/session-projection/README.i18n.yaml +++ b/packages/session/session-projection/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/session/session-projection/README.md -README.md: d8262b498ac1ddcb6443d3e9db35c24ed0634ce9 -README.zh.md: 6be8a50733457a123aaa4436da7dde8008410cea +README.md: 2fa7ddc096e64e622c814f216a6c606de6ba1689 +README.zh.md: 565b331cd7e0f5190b6da4e54ed659d9da6d96fc diff --git a/packages/session/session-projection/README.md b/packages/session/session-projection/README.md index d8262b498a..2fa7ddc096 100644 --- a/packages/session/session-projection/README.md +++ b/packages/session/session-projection/README.md @@ -57,8 +57,6 @@ const definition = { `register(definition)` installs the unit; registrants with the same key and `stateVersion` share its cells, while an incompatible version or invalid `stateVersion` throws. Registration is an effect on the calling fiber, so the last unload removes the key and its cached cells. Carriers read a consistent synchronous cut over every client-visible unit with `snapshot(session)` — `{ asOfSeq, values }`, where `asOfSeq` is the seq of the last event every value reflects — and subscribe to per-change notifications with `onChanged(listener)`. `stateOf(session, key)` reads one unit's live read-only host state without computing unrelated views. -The registry emits `session-projection/definitions-changed` after a client-visible key is first registered or its last registration is removed. Carriers replace their baselines so connected clients observe capability changes without waiting for another Session event. Duplicate registrations and host-only units do not trigger this notification. - ```text const dispose = ctx.sessionProjections.register(definition) const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) diff --git a/packages/session/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md index 6be8a50733..565b331cd7 100644 --- a/packages/session/session-projection/README.zh.md +++ b/packages/session/session-projection/README.zh.md @@ -57,8 +57,6 @@ const definition = { `register(definition)` 安装单元;具有相同 key 和 `stateVersion` 的注册方共享其 cell,版本不兼容或 `stateVersion` 非法时会 throw。注册是挂在调用方 fiber 上的 effect,因此最后一个注册方卸载后会移除 key 及其缓存 cell。载体用 `snapshot(session)` 对每个客户端可见单元读取一致的同步切面——`{ asOfSeq, values }`,其中 `asOfSeq` 是所有值共同反映到的最后一个事件的 seq——并用 `onChanged(listener)` 订阅逐变更通知。`stateOf(session, key)` 读取一个单元的实时只读 host 状态,不计算无关视图。 -客户端可见 key 首次注册或最后一次注册被移除后,注册表发出 `session-projection/definitions-changed`。传输层替换基线,使已连接客户端无需等待下一个 Session 事件即可观察能力变化。重复注册与仅 Host 使用的投影单元不触发此通知。 - ```text const dispose = ctx.sessionProjections.register(definition) const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) diff --git a/packages/session/session-projection/src/index.ts b/packages/session/session-projection/src/index.ts index d7e813425a..6d28631ddc 100644 --- a/packages/session/session-projection/src/index.ts +++ b/packages/session/session-projection/src/index.ts @@ -31,13 +31,6 @@ declare module '@deepseek-ai/cordis' { interface Context { sessionProjections: SessionProjectionRegistry } - interface Events { - /** - * The registered client-visible keys changed; carriers must replace their baselines. - * @mode emit - */ - 'session-projection/definitions-changed'(): void - } } import type { SessionProjectionMap, SessionProjectionStateMap } from './types.ts' @@ -293,12 +286,8 @@ export class SessionProjectionRegistry extends Service { /* v8 ignore next -- the disposer runs once per successful registration, so the entry it counted is still here */ if (live === undefined) return live.refs -= 1 - if (live.refs === 0) { - this.registrations.delete(key) - if (live.def.wire !== undefined) this.ctx.emit('session-projection/definitions-changed') - } + if (live.refs === 0) this.registrations.delete(key) } - if (existing === undefined && erased.wire !== undefined) this.ctx.emit('session-projection/definitions-changed') }.bind(this), 'sessionProjections.register()') return () => void dispose() } diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 0c9317d931..644f7073b2 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -210,7 +210,6 @@ export const SERVICE_WALK_EXEMPTIONS: Record = { * {@link EVENT_WALK_EXEMPTIONS} names each one with its documentation owner. */ export const EVENT_SCOPE_PAGE: Record = { - 'session-projection': 'session-projection.md', 'app-boot': 'boot.md', hmr: 'boot.md', 'plugin-manager': 'boot.md',