@deepseek-ai/dsh-api-session-controller 0.2.1-alpha.1 → 0.2.1-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -15,8 +15,8 @@
15
15
  en: 1fdf707322fb6996
16
16
  zh: 0204f7e9d4aa3cce
17
17
  /session-controller/use-this-package:
18
- en: 77552723167e5d8e
19
- zh: 1d77a657c707190a
18
+ en: f9e4042fae88f801
19
+ zh: f379f78966d57c2e
20
20
  /session-controller/client-references:
21
21
  en: 41bdda1ac32df558
22
22
  zh: aa5d0e325924066b
package/README.md CHANGED
@@ -31,13 +31,17 @@ History pages and follow opening snapshots carry one `{ type: 'event', event: Se
31
31
 
32
32
  The Client journal validates current Session event envelopes before publishing follow snapshots, live entries, or history pages. It reuses the browser-safe Session validators for required surface markers, exact replacement endpoints, earlier unique source seqs, embedded Assistant provider metadata, request-header omissions, and tool-error consistency. Invalid records fail without field stripping or normalization; range membership and source existence remain durable-log checks on the Host.
33
33
 
34
- Each endpoint states its activation policy. List reads only stored headers and projection-cache rows: it never calls per-session stat or opens a cold Session body. A current-format cache identity may supply every list hint; a lifecycle-matching predecessor cache may supply only its version-compatible title as a stale display fact, never as an authoritative fold seed. Search, attachment, history pages, log following, skill discovery, and workspace-path opening can inspect persistence without activating an Agent; `canOpenWorkspacePath()` reports native-opening availability without addressing a Session. Cancellation requires live state; queue mutation, model, rename, prompt, and file-reference operations may resolve or resume an ordinary Session. Prompt rejects content with neither non-whitespace text nor an attachment before resolving the Agent or appending Session events; queue edits accept only non-empty text content. Prompt admission consumes opaque receipts from the injected [`fileUploads`](../../client/file-upload/README.md) Host service and resolves every same-Agent receipt before sending the complete ordered content list through `ctx.attachments`. Prompt retries whose `requestId` is already queued or logged return the original acceptance without inserting another message. Create and fork are the only operations that create a new Agent directly. The service applies one preset-aware resume policy and subagent ownership fence to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces. Queue mutation has one narrow exception: a live child whose current projected identity is continuable and comes from its own non-seed suffix accepts the ordinary Edit, Remove, and QueueDock Steer actions across both inbox destinations. One-shot, missing, unknown, corrupt, seed-only, or cold children remain rejected without resume. The skill catalog uses a live Agent when present or the recorded preset's standing scope when cold, so listing never starts an Agent. The authenticated delivery routes use `workspaceDesktop()` for the serving Host name and file-manager behavior. `openWorkspacePath({ path, action: "reveal" })` delegates file-manager navigation to the native adapter; omitting `action` opens the file-type association, including HTML and SVG. Both operations require the composed filesystem to map the requested Host path to the same canonical process path; unmapped remote paths are refused before a native command runs. `session.projections` reads one complete baseline through a live-preferred Session observation without activating an Agent. It returns null for a missing Session and serves any registered projection keys. The Client exposes shared values and explicit-read state through `projectionsBySession`; domains select their own keys. Session-list summaries carry `agentAvailable`, updated through existing summary and status events independently of durable projections. Initial reads and live projection frames use the same sequence ordering.
34
+ Each endpoint states its activation policy. List reads only stored headers and projection-cache rows: it never calls per-session stat or opens a cold Session body. A current-format cache identity may supply every list hint; a lifecycle-matching predecessor cache may supply only its version-compatible title as a stale display fact, never as an authoritative fold seed. Search, attachment, history pages, log following, skill discovery, and workspace-path opening can inspect persistence without activating an Agent; `canOpenWorkspacePath()` reports native-opening availability without addressing a Session. Cancellation requires live state; queue mutation, model, rename, prompt, and file-reference operations may resolve or resume an ordinary Session. Prompt rejects content with neither non-whitespace text nor an attachment before resolving the Agent or appending Session events; queue edits accept only non-empty text content. Prompt admission consumes opaque receipts from the injected [`fileUploads`](../../client/file-upload/README.md) Host service and resolves every same-Agent receipt before sending the complete ordered content list through `ctx.attachments`. Prompt retries whose `requestId` is already queued or logged return the original acceptance without inserting another message. Create and fork are the only operations that create a new Agent directly. The service applies one preset-aware resume policy and subagent ownership fence to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces. Queue mutation has one narrow exception: a live child whose current projected identity is continuable and comes from its own non-seed suffix accepts the ordinary Edit, Remove, and QueueDock Steer actions across both inbox destinations. One-shot, missing, unknown, corrupt, seed-only, or cold children remain rejected without resume. The skill catalog uses a live Agent's effective working directory and scoped registry when present. Cold listing uses the projected current directory, falling back to the recorded origin and then the configured directory default. It retains the preset standing scope without activating an Agent. The authenticated delivery routes use `workspaceDesktop()` for the serving Host name and file-manager behavior. `openWorkspacePath({ path, action: "reveal" })` delegates file-manager navigation to the native adapter; omitting `action` opens the file-type association, including HTML and SVG. Both operations require the composed filesystem to map the requested Host path to the same canonical process path; unmapped remote paths are refused before a native command runs. `session.projections` normally reads an exact baseline, including current-format cold logs, and returns `kind: sequenced`. Only a read requiring migration returns `kind: migration-required` hints without `asOfSeq`; missing keys remain absent. It returns null for a missing Session. Fork callers may set `allowMigration: false` to receive `session/migration-required` instead of starting source migration; omission retains automatic migration. The Client exposes shared values and explicit-read state through `projectionsBySession`; domains select their own keys. Session-list summaries carry `agentAvailable`, updated through existing summary and status events independently of durable projections. Sequenced reads and live projection frames use the same sequence ordering; cached reads fill hints without replacing sequenced values.
35
+
36
+ Every Remote endpoint is access-controlled: a Peer whose principal is not the owner reaches an endpoint only when, on every resource the call touches, a binding of that principal kind's policy in [`dsh-api-remote-access`](../remote-access/README.md) grants a role listing the action ([Peer principals Agent Note](../../../.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.md)). The controller injects `remoteAccess` and registers the `session` resource kind with the default field `sessionId` through `typert.resources`. `session/page` and `session/follow` declare a `session` reader in `@Remote({ resources })` that reads `request.address.sessionId` for an ordinary Session address and `request.address.childSessionId` for a subagent address; whether access to the parent Session covers its children is the policy's rule. `session/projections` reads the Session resource from `request.sessionId`, so a reconnecting reader reloads only the projections of Sessions it may read. `session/list`, `session/search`, and `session/control` read no resource, so their calls touch the `global` resource and need a `global` role; for a non-owner caller, `session/list` returns only the rows, `session/search` only the hits, and `session/control` only the baseline projections and projection frames, whose Session `ctx.remoteAccess.allows()` admits for the same action, so that action also belongs to a `session` role; owner results are unfiltered. `session/prompt` builds the durable `user-rpc` message source in its Remote method and records `principal` only for a non-owner caller, read through `principalOf(this.ctx.invocation)`; a source without `principal` belongs to the owner.
37
+
38
+ List rows carry `formatStatus` when available, so Clients can identify migration-required Sessions without requesting their projections.
35
39
 
36
40
  Host list generation yields between complete rows only when its configured work slice is exhausted; cheap remaining work returns without a forced yield. Request cancellation is checked per row and after each yield and rejects without partial results. Each row is synchronous; the list does not promise one cross-Session snapshot. A Session already queued as cold remains a cached row if it attaches during a yield: Client in-flight mutation replay corrects its availability, and sequenced projections supersede cached hints.
37
41
 
38
42
  Client list rows and resident Sessions use the current `sessionListMetadata` projection to reject stale blank-session hints; recency is the later of the summary timestamp and the projected last user prompt. When SessionManager creates an instance before its list row arrives, it reconciles blankness with metadata already retained for that Session. An opened conversation is therefore not reused by New Session even when an older list response still marks it blank.
39
43
 
40
- Explicit-id `session.create` adopts a live Session or resumes a persisted Session while retaining its writer lock. Writer contention returns `session/writer-held`; callers may try another blank without suppressing unrelated failures. `session.list` includes persisted blanks using cached metadata, without opening cold log bodies.
44
+ `session.create` returns a Session for a new conversation, and its response names the Session actually used. A named `sessionId` is reused while it is blank (no turn has started), belongs to the requested directory, is an unarchived member of the requested Workspace, and is not written by another owner; a live Session is reused in place, and a persisted one is resumed while retaining its writer lock. The reused Session runs the requested `agentPreset`, or the configured default when the request names none: its recorded preset is not resolved, and a change is logged as `agent-preset/selected` before the Agent is published. A named Session that has started, fails the Workspace placement, or is written by another owner stays unchanged, and a new Session is created instead. A `sessionId` with no stored Session becomes the new Session's identity. A named Session in another directory fails with `session/conflict`, and one owned by subagent routing with `session/agent-busy`; both checks precede preset resolution. Creations naming one identity run in arrival order, including their preset resolution, and each response reports the preset its own request left the Session running. `session.list` includes persisted blanks using cached metadata, without opening cold log bodies.
41
45
 
42
46
  Client list refreshes retain unchanged row objects and reuse the items array when order and values match. Each row's `retainedBy` contains positive local reference-source counts; Host metadata refreshes cannot overwrite them. Cache membership checks use a per-refresh ID set, so reconciliation grows linearly with the current list and retained cache sizes. Host summary updates replace running and Agent availability; local create/fork responses only fill missing metadata on existing rows. Removed ordinary Sessions retain projection stores only when their catalogs contain children.
43
47
 
package/README.zh.md CHANGED
@@ -31,13 +31,17 @@ kind: "package-reference"
31
31
 
32
32
  Client journal 在发布 follow 快照、live entry 或历史页之前验证当前 Session 事件 envelope。它复用浏览器安全的 Session validator,检查必需的 surface marker、精确的 replacement endpoint、更早且唯一的 source seq、内嵌 Assistant 提供方元数据、request header 可选字段的省略规则以及工具错误一致性。无效 record 直接失败,不删除字段或归一化;范围成员与来源存在性仍由 Host 的持久日志检查。
33
33
 
34
- 每个 endpoint 都声明自己的激活策略。列表只读取持久化 header 与 projection cache row,绝不调用逐 Session stat 或打开冷 Session body。当前格式 cache identity 可以提供全部列表 hint;生命周期匹配的 predecessor cache 只能提供版本兼容的 title,作为可能过时的展示事实,绝不能作为权威 fold seed。搜索、附件、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;`canOpenWorkspacePath()` 无需指定 Session 即可报告原生打开能力。取消要求 live 状态;queue 变更、模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。提示词会在解析 Agent 或追加 Session 事件前,拒绝既没有非空白文本也没有附件的 content;queue edit 只接受非空文本 content。prompt 准入从注入的 [`fileUploads`](../../client/file-upload/README.zh.md) Host 服务取得不透明凭证,在把完整有序内容列表交给 `ctx.attachments` 前解析每个属于同一 Agent 的凭证。`requestId` 已进入 queue 或日志时,prompt 重试直接返回原来的接受结果,不会重复插入消息。只有 create 与 fork 会直接创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。Queue 变更只有一个狭窄例外:当前 projection identity 为 continuable 且来自自身非 seed suffix 的在线 child,可以在两个 inbox 目标上使用普通 Edit、Remove 与 QueueDock Steer action。One-shot、缺失、未知、损坏、仅含 seed identity 或冷 child 继续被拒绝,且不会恢复。skill 目录优先使用已有 live Agent,否则使用所记录 preset 的常驻 scope,因此列表查询绝不会启动 Agent。经过鉴权的文件交付路由通过 `workspaceDesktop()` 获取提供服务的 Host 名称和文件管理器行为。`openWorkspacePath({ path, action: "reveal" })` 将文件管理器导航委托给原生适配器;省略 `action` 时按文件类型关联打开,包括 HTML 和 SVG。两种操作都要求当前文件系统将请求的 Host 路径映射到同一个规范进程路径;无法映射的远端路径会在执行原生命令之前被拒绝。 `session.projections` 通过一次 live-preferred Session observation 读取完整基线,不激活 Agent。Session 不存在时返回 null,并可提供任意已注册的 projection key。Client 通过 `projectionsBySession` 暴露共享值和显式读取状态,由领域选择自身的 key。Session 列表摘要携带 `agentAvailable`,通过已有摘要与状态事件更新,与持久化 projection 相互独立。初始读取与实时 projection 帧使用相同的序号排序规则。
34
+ 每个 endpoint 都声明自己的激活策略。列表只读取持久化 header 与 projection cache row,绝不调用逐 Session stat 或打开冷 Session body。当前格式 cache identity 可以提供全部列表 hint;生命周期匹配的 predecessor cache 只能提供版本兼容的 title,作为可能过时的展示事实,绝不能作为权威 fold seed。搜索、附件、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;`canOpenWorkspacePath()` 无需指定 Session 即可报告原生打开能力。取消要求 live 状态;queue 变更、模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。提示词会在解析 Agent 或追加 Session 事件前,拒绝既没有非空白文本也没有附件的 content;queue edit 只接受非空文本 content。prompt 准入从注入的 [`fileUploads`](../../client/file-upload/README.zh.md) Host 服务取得不透明凭证,在把完整有序内容列表交给 `ctx.attachments` 前解析每个属于同一 Agent 的凭证。`requestId` 已进入 queue 或日志时,prompt 重试直接返回原来的接受结果,不会重复插入消息。只有 create 与 fork 会直接创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。Queue 变更只有一个狭窄例外:当前 projection identity 为 continuable 且来自自身非 seed suffix 的在线 child,可以在两个 inbox 目标上使用普通 Edit、Remove 与 QueueDock Steer action。One-shot、缺失、未知、损坏、仅含 seed identity 或冷 child 继续被拒绝,且不会恢复。skill 目录优先使用已有 live Agent 的有效工作目录与限定范围内的注册表。冷态列表使用投影中的当前目录,依次回退到记录的起始目录与配置的默认目录。它保留 preset 常驻 scope,不会激活 Agent。经过鉴权的文件交付路由通过 `workspaceDesktop()` 获取提供服务的 Host 名称和文件管理器行为。`openWorkspacePath({ path, action: "reveal" })` 将文件管理器导航委托给原生适配器;省略 `action` 时按文件类型关联打开,包括 HTML 和 SVG。两种操作都要求当前文件系统将请求的 Host 路径映射到同一个规范进程路径;无法映射的远端路径会在执行原生命令之前被拒绝。 `session.projections` 正常读取准确基线,包括当前格式的冷日志,返回 `kind: sequenced`。只有需要迁移时才返回不带 `asOfSeq` 的 `kind: migration-required` 提示,缺少的键保持缺席。Session 不存在时返回 null。Fork 调用者可设置 `allowMigration: false`,在源历史需要迁移时收到 `session/migration-required`;省略则保持自动迁移。Client 通过 `projectionsBySession` 暴露共享值和显式读取状态,由领域选择自身的 key。Session 列表摘要携带 `agentAvailable`,通过已有摘要与状态事件更新,与持久化 projection 相互独立。有序读取与实时 projection 帧使用相同的序号排序规则;缓存读取只补充提示,不替换有序值。
35
+
36
+ 每个 Remote 端点都受访问控制:principal 不是 owner 的 Peer 只有在本次调用访问的每个资源上,该 principal kind 在 [`dsh-api-remote-access`](../remote-access/README.zh.md) 中的 policy 都有某个 binding 授予列出该 action 的 role 时,才能调用该端点([Peer 身份属性 Agent Note](../../../.agents/notes/implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md))。控制器注入 `remoteAccess`,并经 `typert.resources` 以默认字段 `sessionId` 登记 `session` 资源种类。`session/page` 与 `session/follow` 在 `@Remote({ resources })` 中声明 `session` 读取函数:对普通 Session 地址读 `request.address.sessionId`,对 subagent 地址读 `request.address.childSessionId`;父会话的访问权是否覆盖其子会话由 policy 决定。`session/projections` 从 `request.sessionId` 读取会话资源,因此重连的读者只会重新读取它可读的会话的 projections。`session/list`、`session/search` 与 `session/control` 不读取资源,其调用访问 `global` 资源,需要 `global` role;对非 owner 调用方,`session/list` 只返回其 Session 被 `ctx.remoteAccess.allows()` 按同一 action 允许的行,`session/search` 只返回这类 Session 的命中,`session/control` 也只返回这类 Session 的 baseline projection 与 projection 帧,因此该 action 也要列在 `session` role 中;owner 的结果不过滤。`session/prompt` 在其 Remote 方法内构造持久的 `user-rpc` 消息 source,且只为非 owner 调用方记录经 `principalOf(this.ctx.invocation)` 读取的 `principal`;不带 `principal` 的 source 属于 owner。
37
+
38
+ 列表行在状态可用时携带 `formatStatus`,Client 无需请求其投影即可识别需要迁移的 Session。
35
39
 
36
40
  Host 仅在配置的工作时间片耗尽时,于完整条目之间让出事件循环;剩余工作较少时直接返回,不强制让出。每行及每次让出之后检查请求取消,取消时拒绝返回部分结果。每行同步计算;列表不承诺跨 Session 的统一快照。已经进入冷态队列的 Session 若在让出期间接入,仍返回缓存行:Client 对进行中请求的变更重放会纠正其可用性,带序号的投影会覆盖缓存提示。
37
41
 
38
42
  Client 列表行和驻留 Session 使用当前 `sessionListMetadata` 投影纠正过期的空白会话提示;最近活动时间取摘要时间戳与投影中最后一次用户提示词时间的较晚值。当 SessionManager 在列表行到达前创建实例时,会使用已保留的该 Session 元数据对账空白状态。因此,即使旧列表响应仍将已有对话标为空白,新会话操作也不会复用已经打开过的对话。
39
43
 
40
- 显式 ID 的 `session.create` 会收养活动 Session,或恢复持久化 Session 并持续持有其写锁。写锁争用返回 `session/writer-held`;调用方可以尝试其他空白会话,同时保留其他失败。`session.list` 根据缓存元数据列出持久化空白会话,不打开冷日志正文。
44
+ `session.create` 返回一个用于新对话的 Session,响应指明实际使用的 Session。请求指明的 `sessionId` 在满足以下条件时被复用:仍为空白(尚未开始任何轮次)、属于请求的目录、是请求的工作区中未归档的成员,且未被其他写者占用;活动 Session 原地复用,持久化 Session 被恢复并持续持有其写锁。被复用的 Session 运行请求的 `agentPreset`,请求未指明时运行配置的默认 preset:不解析它记录的 preset,变更在 Agent 发布前记录为 `agent-preset/selected`。指明的 Session 若已开始、不满足工作区归属,或被其他写者占用,则保持不变,改为新建 Session。没有对应已存储 Session 的 `sessionId` 成为新 Session 的标识。指明其他目录中的 Session 返回 `session/conflict`,指明由 subagent 路由拥有的 Session 返回 `session/agent-busy`;这两项检查先于 preset 解析。指明同一标识的创建请求按到达顺序执行,preset 解析也包含在内,每个响应报告其自身请求完成时该 Session 运行的 preset。`session.list` 根据缓存元数据列出持久化空白会话,不打开冷日志正文。
41
45
 
42
46
  Client 列表刷新保留未变化的行对象,并在顺序和值均相同时复用条目数组。每行的 `retainedBy` 包含正数的本地引用来源计数,Host 元数据刷新不能覆盖它们。缓存成员检查使用每次刷新构建的 ID 集合,因此对账成本随当前列表和保留缓存的规模线性增长。 Host 摘要更新会替换运行状态与 Agent 可用性;本地 create/fork 响应只补充已有行缺失的元数据。普通 Session 被移除后,仅在目录仍有子项时保留其投影 store。
43
47
 
package/lib/client.js CHANGED
@@ -157,6 +157,8 @@ window.__ModuleLoader__.load({
157
157
  "hook/invoked",
158
158
  "hook/result",
159
159
  "image/offload",
160
+ "job/registered",
161
+ "job/settled",
160
162
  "llm/retry",
161
163
  "llm/retry-started",
162
164
  "model/selection",
@@ -193,6 +195,7 @@ window.__ModuleLoader__.load({
193
195
  "turn/start",
194
196
  "user/message",
195
197
  "web/deepseek-search-llm-request",
198
+ "working-directory/change",
196
199
  "workspace/changes"
197
200
  ]);
198
201
  //#endregion
@@ -1616,7 +1619,7 @@ window.__ModuleLoader__.load({
1616
1619
  this.changed(key);
1617
1620
  }
1618
1621
  /**
1619
- * Fill keys from a session-list block the Host labeled `cached`: a zero-I/O
1622
+ * Fill keys from a Host block labeled `cached`: a zero-I/O
1620
1623
  * view of the persisted checkpoint. A cached value lands only where no
1621
1624
  * sequenced row exists: a connected Session has already answered for such
1622
1625
  * a key, and the list's view of the persisted checkpoint cannot be newer
@@ -2358,7 +2361,7 @@ window.__ModuleLoader__.load({
2358
2361
  else {
2359
2362
  const routedContent = content;
2360
2363
  const routed = await this.remote.subagents.prompt({
2361
- requestId: randomUUID(),
2364
+ requestId: requestId ?? randomUUID(),
2362
2365
  parentSessionId: this.address.parentSessionId,
2363
2366
  childSessionId: this.address.childSessionId,
2364
2367
  mode: "continuable",
@@ -3011,20 +3014,28 @@ window.__ModuleLoader__.load({
3011
3014
  }
3012
3015
  /**
3013
3016
  * Resolve an address for breadcrumb navigation without retaining transport authority.
3014
- * @param sessionId - possible child id in an already-loaded catalog.
3015
- * @returns A retained or catalog-derived direct-parent address.
3017
+ * @param sessionId - possible child id in received Session metadata.
3018
+ * @returns A retained address, then a catalog address, then an unknown-mode address
3019
+ * from a listed subagent header; undefined without that evidence.
3016
3020
  */
3017
3021
  subagentAddress(sessionId) {
3018
3022
  const retained = this.addresses.get(sessionId);
3019
3023
  if (retained !== void 0) return retained;
3020
3024
  for (const parentSessionId of this.projectionStores.keys()) {
3021
3025
  const child = this.projectionStores.get(parentSessionId)?.values().subagentCatalog?.find((entry) => entry.id === sessionId);
3022
- if (child !== void 0) return {
3026
+ if (child !== void 0 && child.mode !== "external") return {
3023
3027
  parentSessionId,
3024
3028
  childSessionId: sessionId,
3025
3029
  mode: child.mode
3026
3030
  };
3027
3031
  }
3032
+ const summary = this.summaries.find((summary) => summary.sessionId === sessionId);
3033
+ if (summary?.origin !== "subagent" || summary.parentSessionId === void 0) return void 0;
3034
+ return {
3035
+ parentSessionId: summary.parentSessionId,
3036
+ childSessionId: sessionId,
3037
+ mode: "unknown"
3038
+ };
3028
3039
  }
3029
3040
  /**
3030
3041
  * Withdraw an exact Client instance before running its teardown callbacks.
@@ -3090,7 +3101,8 @@ window.__ModuleLoader__.load({
3090
3101
  session.handleRunning(summary.running);
3091
3102
  } else {
3092
3103
  const address = this.addresses.get(sessionId);
3093
- if ((address === void 0 ? void 0 : this.projectionStores.get(address.parentSessionId)?.values().subagentCatalog?.find((entry) => entry.id === sessionId)) !== void 0) {
3104
+ const child = address === void 0 ? void 0 : this.projectionStores.get(address.parentSessionId)?.values().subagentCatalog?.find((entry) => entry.id === sessionId);
3105
+ if (child !== void 0 && child.mode !== "external") {
3094
3106
  session.handleBlank(false);
3095
3107
  session.handleRunning(false);
3096
3108
  } else session.handleBlank(true);
@@ -3158,7 +3170,7 @@ window.__ModuleLoader__.load({
3158
3170
  return store;
3159
3171
  }
3160
3172
  /**
3161
- * Load a complete projection baseline once per connection; retry unsuccessful reads.
3173
+ * Read projections without starting migration; retry failed or migration-deferred reads.
3162
3174
  * @param sessionId - Session to inspect without opening its conversation.
3163
3175
  * @returns completion of the current or newly started read.
3164
3176
  */
@@ -3179,13 +3191,22 @@ window.__ModuleLoader__.load({
3179
3191
  const result = await this.remote.session.projections({ sessionId }, controller.signal);
3180
3192
  if (controller.signal.aborted) return;
3181
3193
  if (result.ok) {
3182
- if (result.value !== null) store.seed({
3183
- ...result.value,
3184
- asOfSeq: sessionSeqCursor(result.value.asOfSeq)
3185
- });
3194
+ const value = result.value;
3195
+ if (value !== null) switch (value.kind) {
3196
+ case "sequenced":
3197
+ store.seed({
3198
+ values: value.values,
3199
+ asOfSeq: sessionSeqCursor(value.asOfSeq)
3200
+ });
3201
+ break;
3202
+ case "migration-required":
3203
+ store.applyCached(value.values);
3204
+ break;
3205
+ default: assertNever(value, "session projections result");
3206
+ }
3186
3207
  else if (store.values() === initialValues) store.clear();
3187
3208
  this.projectionLoads.set(sessionId, {
3188
- state: "ready",
3209
+ state: value?.kind === "migration-required" ? "migration-required" : "ready",
3189
3210
  error: null
3190
3211
  });
3191
3212
  } else this.projectionLoads.set(sessionId, {
@@ -3294,11 +3315,14 @@ window.__ModuleLoader__.load({
3294
3315
  * Contract session.create; on success merge into summaries immediately (no
3295
3316
  * wait for the next refresh). A created session is blank by definition
3296
3317
  * (entity birth precedes the first message).
3297
- * @param opts - target workspace or working directory, plus an optional caller-owned id.
3298
- * @returns the create result.
3318
+ * @param opts - target workspace or working directory, an optional blank Session to reuse, and an optional Agent preset.
3319
+ * @returns the create result, naming the Session the Host created or reused.
3299
3320
  */
3300
3321
  async create(opts = {}) {
3301
- const shared = opts.sessionId === void 0 ? {} : { sessionId: opts.sessionId };
3322
+ const shared = {
3323
+ ...opts.sessionId === void 0 ? {} : { sessionId: opts.sessionId },
3324
+ ...opts.agentPreset === void 0 ? {} : { agentPreset: opts.agentPreset }
3325
+ };
3302
3326
  const payload = opts.workspaceId !== void 0 ? {
3303
3327
  workspaceId: opts.workspaceId,
3304
3328
  ...shared
@@ -3340,14 +3364,15 @@ window.__ModuleLoader__.load({
3340
3364
  * preserve it or lower it after an exact cut before the first `turn/start`;
3341
3365
  * lineage rides parentSessionId. A child published before Workspace
3342
3366
  * attachment fails is also reconciled into the list.
3343
- * @param opts - source session and the optional exact inclusive boundary seq.
3367
+ * @param opts - source session, optional exact inclusive boundary seq, and permission to start migration.
3344
3368
  * @returns the fork result (the child session id).
3345
3369
  */
3346
3370
  async fork(opts) {
3347
3371
  const source = this.summaries.find((s) => s.sessionId === opts.sessionId);
3348
3372
  const result = await this.remote.session.fork({
3349
3373
  sessionId: opts.sessionId,
3350
- ...opts.atSeq === void 0 ? {} : { atSeq: opts.atSeq }
3374
+ ...opts.atSeq === void 0 ? {} : { atSeq: opts.atSeq },
3375
+ ...opts.allowMigration === void 0 ? {} : { allowMigration: opts.allowMigration }
3351
3376
  });
3352
3377
  const childId = result.ok ? result.value.sessionId : workspaceAttachSessionId(result.error);
3353
3378
  if (childId !== void 0) this.recordMutation({
@@ -3572,7 +3597,7 @@ window.__ModuleLoader__.load({
3572
3597
  };
3573
3598
  })).map((entry) => {
3574
3599
  const prev = this.entryCache.get(entry.sessionId);
3575
- if (prev !== void 0 && prev.updatedAt === entry.updatedAt && prev.running === entry.running && prev.blank === entry.blank && prev.parentSessionId === entry.parentSessionId && prev.cwd === entry.cwd && prev.origin === entry.origin && prev.title === entry.title && prev.depth === entry.depth && prev.projectionValues === entry.projectionValues) return prev;
3600
+ if (prev !== void 0 && prev.updatedAt === entry.updatedAt && prev.running === entry.running && prev.blank === entry.blank && prev.parentSessionId === entry.parentSessionId && prev.cwd === entry.cwd && prev.origin === entry.origin && prev.title === entry.title && prev.depth === entry.depth && prev.formatStatus === entry.formatStatus && prev.projectionValues === entry.projectionValues) return prev;
3576
3601
  this.entryCache.set(entry.sessionId, entry);
3577
3602
  return entry;
3578
3603
  });
@@ -3605,13 +3630,14 @@ window.__ModuleLoader__.load({
3605
3630
  blank: existing.blank && mutation.summary.blank,
3606
3631
  ...mutation.kind === "upsert" ? {
3607
3632
  agentAvailable: mutation.summary.agentAvailable,
3608
- running: mutation.summary.running
3633
+ running: mutation.summary.running,
3634
+ ...mutation.summary.formatStatus === void 0 ? {} : { formatStatus: mutation.summary.formatStatus }
3609
3635
  } : {},
3610
3636
  ...existing.cwd === void 0 && mutation.summary.cwd !== void 0 ? { cwd: mutation.summary.cwd } : {},
3611
3637
  ...existing.parentSessionId === void 0 && mutation.summary.parentSessionId !== void 0 ? { parentSessionId: mutation.summary.parentSessionId } : {},
3612
3638
  ...existing.origin === void 0 && mutation.summary.origin !== void 0 ? { origin: mutation.summary.origin } : {}
3613
3639
  };
3614
- if (filled.cwd === existing.cwd && filled.parentSessionId === existing.parentSessionId && filled.origin === existing.origin && filled.blank === existing.blank && filled.agentAvailable === existing.agentAvailable && filled.running === existing.running) return [...summaries];
3640
+ if (filled.cwd === existing.cwd && filled.parentSessionId === existing.parentSessionId && filled.origin === existing.origin && filled.blank === existing.blank && filled.agentAvailable === existing.agentAvailable && filled.running === existing.running && filled.formatStatus === existing.formatStatus) return [...summaries];
3615
3641
  return summaries.map((summary) => summary.sessionId === mutation.summary.sessionId ? filled : summary);
3616
3642
  }
3617
3643
  case "remove": return summaries.filter((summary) => summary.sessionId !== mutation.sessionId);
@@ -3861,13 +3887,13 @@ window.__ModuleLoader__.load({
3861
3887
  * Resolve an already discovered direct-parent address without opening it.
3862
3888
  * Feature plugins use this to avoid Agent-bound RPCs in persisted child views.
3863
3889
  * @param id - possible addressed child id.
3864
- * @returns A retained or loaded-catalog address, without retaining a new selection or scope.
3890
+ * @returns A retained, catalog-derived, or unknown-mode listed address, without retaining a new selection or scope.
3865
3891
  */
3866
3892
  subagentAddress(id) {
3867
3893
  return this.manager.subagentAddress(id);
3868
3894
  }
3869
3895
  /**
3870
- * Load all Session projections once per connection; retry an unsuccessful initial read.
3896
+ * Read projections without starting migration; retry failed or migration-deferred reads.
3871
3897
  * @param sessionId - Session to inspect without opening its conversation.
3872
3898
  */
3873
3899
  refreshProjections(sessionId) {
@@ -3939,8 +3965,8 @@ window.__ModuleLoader__.load({
3939
3965
  /**
3940
3966
  * Create a Host Session and publish its catalog row before resolving.
3941
3967
  * Callers retain the returned identity before borrowing its binding.
3942
- * @param opts - target workspace or directory and an optional preallocated id.
3943
- * @returns the new session id.
3968
+ * @param opts - target workspace or directory, an optional blank Session to reuse, and an optional Agent preset.
3969
+ * @returns the created or reused session id.
3944
3970
  * @throws {SessionCreateError} with the requested id.
3945
3971
  */
3946
3972
  async create(opts = {}) {
@@ -3957,7 +3983,7 @@ window.__ModuleLoader__.load({
3957
3983
  * seq (a real event seq the caller already knows; a cut inside an open
3958
3984
  * turn is balanced Host-side with synthetic closers, and omission selects
3959
3985
  * the latest completed-turn prefix), and whether to increment an
3960
- * inherited durable title before resolving.
3986
+ * inherited durable title before resolving, plus permission to start source migration.
3961
3987
  * @returns the child session id.
3962
3988
  * @throws {SessionForkError} with the source id.
3963
3989
  * @throws {Error} when a requested child-title rename fails after creation.
@@ -3966,7 +3992,8 @@ window.__ModuleLoader__.load({
3966
3992
  const sourceTitle = opts.increaseTitle ? this.list.getSnapshot().byId[opts.sessionId]?.title : void 0;
3967
3993
  const result = await this.manager.fork({
3968
3994
  sessionId: opts.sessionId,
3969
- ...opts.atSeq === void 0 ? {} : { atSeq: SessionSeq(opts.atSeq) }
3995
+ ...opts.atSeq === void 0 ? {} : { atSeq: SessionSeq(opts.atSeq) },
3996
+ ...opts.allowMigration === void 0 ? {} : { allowMigration: opts.allowMigration }
3970
3997
  });
3971
3998
  if (!result.ok) throw new SessionForkError(result.error, opts.sessionId);
3972
3999
  this.projectList();
@@ -4130,6 +4157,7 @@ window.__ModuleLoader__.load({
4130
4157
  retainedBy: this.retentionSnapshot(entry.sessionId).retainedBy,
4131
4158
  blank: entry.blank,
4132
4159
  updatedAt: entry.updatedAt,
4160
+ formatStatus: entry.formatStatus,
4133
4161
  ...entry.projectionValues === void 0 ? {} : { projectionValues: entry.projectionValues },
4134
4162
  ...entry.title !== void 0 ? { title: entry.title } : {},
4135
4163
  ...entry.cwd !== void 0 ? { cwd: entry.cwd } : {},
@@ -4138,6 +4166,7 @@ window.__ModuleLoader__.load({
4138
4166
  };
4139
4167
  }
4140
4168
  for (const [parentId, projection] of Object.entries(projectionsBySession)) for (const child of projection.values.subagentCatalog ?? []) {
4169
+ if (child.mode === "external") continue;
4141
4170
  const childId = child.id;
4142
4171
  const summary = byId[childId];
4143
4172
  const projectionValues = summary?.projectionValues ?? this.manager.projectionValues(childId);