dsh-llm-fallbacks 0.6.4 → 0.6.6

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.md CHANGED
@@ -7,7 +7,7 @@
7
7
  ![node](https://img.shields.io/badge/node-%3E%3D22-339933.svg)
8
8
  ![pnpm](https://img.shields.io/badge/pnpm-%3E%3D10-f69220.svg)
9
9
  ![dsh tui](https://img.shields.io/badge/dsh%20tui-compatible-4B32C3.svg)
10
- ![dsh](https://img.shields.io/badge/DSH-0.2.0--rc.1-4B32C3.svg)
10
+ ![dsh](https://img.shields.io/badge/DSH-0.2.1--rc.2-4B32C3.svg)
11
11
  [![dshfind](https://dshfind.com/api/badge/omdsh-dev/dsh-llm-fallbacks?lang=en)](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
12
12
 
13
13
  Automatic provider/model fallback chains for dsh (DeepSeek Harness): when an agent's LLM requests keep failing — retries exhausted, auth errors, quota exceeded, rate limiting (429) — the plugin switches provider/model along the fallback chain for the current role, and the current step/turn continues on the target model: tasks are not interrupted by model problems.
@@ -94,7 +94,7 @@ Build the section up in four steps:
94
94
 
95
95
  **2. Set the all-day `rootChain`.** Leading entries are the fallback chain, walked first when a request fails; the **last** entry is the Default model.
96
96
 
97
- > **Conformance**: the last entry must be exactly one official model — `deepseek-official/deepseek-flash` or `deepseek-official/deepseek-v4-pro` (XOR). The settings card and gateway reject any other tail on save; a legacy non-official tail warns at startup and keeps working as a fallback-only walk, but cannot be saved as-is. The retired `deepseek-v4-flash` id is no longer a legal tail — a saved tail with it now warns, goes inert (slot rows + virtual picker), and blocks save until a legal tail is picked. The official Pro tail is catalog-served as `deepseek-official/deepseek-v4-pro` (dsh 0.2.0-rc.1; the claim re-verified against the rc.2 default catalog) and selectable in the card; a hand-written legacy `deepseek-official/deepseek-pro` tail — never catalog-served — reads back as non-conforming (no migration; re-pick the tail). The plugin does not probe catalog availability — a chain containing `deepseek-official/deepseek-v4-pro` dispatches to it like any other exact entry, and on the virtual route the `stream()` delegate serves the first dispatchable exact head of the effective chain, so a chain with a working entry before Pro still routes to that earlier entry.
97
+ > **Conformance**: the last entry must be exactly one official model — `deepseek-official/deepseek-flash` or `deepseek-official/deepseek-v4-pro` (XOR). The settings card and gateway reject any other tail on save; a legacy non-official tail warns at startup and keeps working as a fallback-only walk, but cannot be saved as-is. The retired `deepseek-v4-flash` id is no longer a legal tail — a saved tail with it now warns, goes inert (slot rows + virtual picker), and blocks save until a legal tail is picked. The official Pro tail is catalog-served as `deepseek-official/deepseek-v4-pro` (dsh 0.2.0-rc.2) and selectable in the card; a hand-written legacy `deepseek-official/deepseek-pro` tail — never catalog-served — reads back as non-conforming (no migration; re-pick the tail). The plugin does not probe catalog availability — a chain containing `deepseek-official/deepseek-v4-pro` dispatches to it like any other exact entry, and on the virtual route the `stream()` delegate serves the first dispatchable exact head of the effective chain, so a chain with a working entry before Pro still routes to that earlier entry.
98
98
 
99
99
  **3. Add `timeSlots` (optional).** Rows rotate the effective root chain by wall-clock windows. Preset rows use frozen UTC+8 windows (only their chain is editable; while a preset row exists, `tz` locks to `Asia/Shanghai`); custom rows take `start`/`end` (may wrap midnight) and an optional `days` list. The first row whose window contains the current moment wins; no match → the all-day `rootChain`. Rotation is a routing seed — it applies on the next root request and consumes no cooldown (see [Time slots](#time-slots)).
100
100
 
@@ -187,7 +187,7 @@ The durable fix belongs upstream at the migration edges (the frozen V0→V1 edge
187
187
  - **Chain as root primary from the picker**: whenever the plugin has content configured, the host model picker (web and TUI alike) shows a virtual `FallbacksChain` / `Auto` row — selecting it uses the configured chain as the root primary (a conforming all-day head is required for the delegation to succeed); selecting a real model keeps fallback-only (see [FallbacksChain in the model picker](#fallbackschain-in-the-model-picker)).
188
188
  - **Time slots**: optional `fallbacks.timeSlots` rows rotate the effective root chain by wall-clock windows in the config-level `tz` timezone (default `Asia/Shanghai`) — four frozen UTC+8 presets (`liang-peak` / `liang-valley` / `glm-peak` / `glm-valley`, windows are code constants, models-only edits) or custom `start`/`end`/`days` windows. The first matching row wins; the all-day row is always last. A slot change applies on the **next** root request and is logged as a **time-slot switch** — a routing seed, never a failure decision: it consumes no cooldown and does not count against `maxSwitchesPerStep`. Failure walks keep the **fallback switch** copy (see [Time-slot presets](#time-slot-presets)).
189
189
  - **Dispatch-time role resolution**: on a subagent's first request its role is resolved in three stages — explicit (`agentPreset` matches a declared role id) → deterministic rules (matching either the recorded pair or the served head — see [FallbacksChain in the model picker](#fallbackschain-in-the-model-picker)) → LLM auto-match from the declared role taxonomy (`fallbacks.roleAutoMatch`, default `true`). The resolved role's chain-head model is injected into the first request and recorded via an explicit `role → model` log line (no durable `fallbacks/switch` event is written — issue #52 stop-write); set `roleAutoMatch: false` to disable the LLM auto-match stage (the explicit `agentPreset` stage still applies — with no explicit role this reproduces the previous rules-only behavior). The settings card always renders an **Enable role auto-match** switch (default `true`) to toggle it — the schema default applies even to legacy configs that never declared the key.
190
- - **Role persona on the child (chain-independent)**: when a subagent's dispatch declares a role this plugin knows (the `**Execute as**: <id>` field of its Assignment), that role's `persona` is installed on the child as its own persona — the role is the child's identity, not only a routing decision, and delivery does not depend on routing: a role with an empty `chain` gets its persona exactly like a chained one (the chain only picks the model). A persona the caller already set is never overwritten; a provider that cannot carry a persona skips it and the dispatch runs natively.
190
+ - **Role persona on the child (chain-independent)**: when a subagent's dispatch declares a role this plugin knows (the `**Execute as**: <id>` field of its Assignment), that role's `persona` is installed on the child as a scoped system-prompt section at the `deployment:persona-prefix` slot — the same slot the deployment persona uses, so the role persona **replaces** the deployment prefix for that child instead of stacking under it. The role is the child's identity, not only a routing decision, and delivery does not depend on routing: a role with an empty `chain` gets its persona exactly like a chained one (the chain only picks the model). That section lives on the child's own context, so the persona is **not durable**: a role child resumed after a host restart runs without its role persona (the resumed descriptor cannot carry one). A same-named section the child's scope already holds (a persisted descriptor) counts as delivered; any other registration failure is contained — the child runs unchanged and its notice row carries the ` (persona not applied)` suffix. Role resolution and the catalog record still apply, but two accepted boundaries get no persona delivery at all: an external (acp) child creates no local agent, so it gets **no persona, no notice row and no badge**, and a host that never mounts the `systemPrompt` service runs the child with no persona and **no** ` (persona not applied)` suffix (issue `I-000059`).
191
191
  - **Role announced inside the subagent's own session**: a child dispatched with a declared role gets exactly one plugin notice row in its own session, naming the role it was dispatched as — `[role: <id>]`, with ` (persona not applied)` when the role declares a persona that was skipped — written once at the child's first non-empty pre-step, never for a root session and never for `inherit`/unresolved dispatches. The row is a durable transcript row, so the role stays readable in the child's session after it settles.
192
192
  - **Subagent role badge**: when a subagent is **dispatched with a declared role** — the `**Execute as**: <id>` field of its Assignment, the same declaration that carries the persona, and the badge's ONLY source (policy on or off) — its session shows a compact role badge next to the session's agent-preset (mode) chip in the web session header; hover shows `role → latest request route` (the route of the newest recorded request, not the dispatch-time route). Only the DECLARED header is read, so a child whose role came from another stage (`agentPreset` match, a role rule, or the LLM auto-match) but which declares no Assignment header is still routed as that role yet shows **no** badge — the badge never reports a role inferred after the fact. `inherit`/unresolved sessions render no badge. The badge is durable: it is read from the child's own session record, so it is still shown for a **settled** subagent session and **after a host restart**.
193
193
  - **Context-window-aware fallback**: `triggerCodes` accepts any dsh failure code, including `CONTEXT_WINDOW_EXCEEDED` — a request too big for the current model fails over to a candidate with a **larger** context window (candidates that cannot fit either are skipped), and because the route itself is healthy the switch is request-scoped: no cooldown, no half-open probe burned (see [Fallback triggers](#fallback-triggers-triggercodes)).
package/README.zh-CN.md CHANGED
@@ -7,7 +7,7 @@
7
7
  ![node](https://img.shields.io/badge/node-%3E%3D22-339933.svg)
8
8
  ![pnpm](https://img.shields.io/badge/pnpm-%3E%3D10-f69220.svg)
9
9
  ![dsh tui](https://img.shields.io/badge/dsh%20tui-compatible-4B32C3.svg)
10
- ![dsh](https://img.shields.io/badge/DSH-0.1.7--rc.1-4B32C3.svg)
10
+ ![dsh](https://img.shields.io/badge/DSH-0.2.1--rc.2-4B32C3.svg)
11
11
  [![dshfind](https://dshfind.com/api/badge/omdsh-dev/dsh-llm-fallbacks?lang=zh)](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
12
12
 
13
13
  dsh(DeepSeek Harness)的自动模型降级插件:当 root agent 或 subagent 的模型请求持续失败(重试耗尽、权限、配额超限、限流 429)时,按角色/模型 fallback 链自动切换 provider/model,当前 step/turn 在目标模型上继续完成——任务不因模型问题中断。
@@ -187,7 +187,7 @@ pnpm repair:session-logs -- --drop-legacy-events --apply --backup # 此处必
187
187
  - **选择器里把链当主模型**:只要插件配置了内容,宿主模型选择器(web 与 TUI 一致)即出现虚拟 `FallbacksChain` / `Auto` 行——选中它即以配置的链作为 root 主模型(需要 all-day 链头合规才能成功委托);选真实模型则保持 fallback-only(见 [模型选择器中的 FallbacksChain](#模型选择器中的-fallbackschain))。
188
188
  - **峰谷无忧(分时切换)**:可选的 `fallbacks.timeSlots` 行按墙钟窗口(配置级 `tz` 时区,默认 `Asia/Shanghai`)轮换 root 生效链——四个冻结的 UTC+8 预设(`liang-peak` / `liang-valley` / `glm-peak` / `glm-valley`,窗口为代码常量、仅模型链可编辑),或自定义 `start`/`end`/`days` 窗口。第一条命中的行生效;全时段行固定最后。时段切换在**下一个** root 请求生效,日志记为**分时切换**——路由种子而非失败决策:不消耗冷却、不计入 `maxSwitchesPerStep`。失败降级保留**降级切换**文案(见 [分时槽预设(分时切换)](#分时槽预设分时切换))。
189
189
  - **派发时角色解析**:在 subagent 的首次请求上,其角色按三个阶段解析——显式(`agentPreset` 匹配已声明角色 id)→ 确定性规则(匹配记录 pair 或实际服务的链头——见 [模型选择器中的 FallbacksChain](#模型选择器中的-fallbackschain))→ LLM 自动匹配(从已声明角色体系中选择,`fallbacks.roleAutoMatch` 默认 `true`)。解析出的角色的链头模型注入首次请求,并以显式 `role → model` 日志行记录(不写 durable `fallbacks/switch` 事件——issue #52 停写);设 `roleAutoMatch: false` 仅关闭 LLM 自动匹配阶段(显式 `agentPreset` 阶段仍生效——无显式角色时即复现原有仅规则行为)。设置卡总是渲染「启用角色自动匹配」开关(默认 `true`)以切换之——即使是从未声明过该键的旧配置,schema 默认值同样生效。
190
- - **角色人格注入到子代理(与链无关)**:当 subagent 的派发声明了本插件已知的角色(Assignment 的 `**Execute as**: <id>` 字段)时,该角色的 `persona` 会安装为子代理自身的人格——角色就是子代理的身份,而不只是路由决策;注入与路由无关:`chain` 为空的角色与带链角色的人格注入完全一致(链只负责选模型)。调用方已设置的人格永不覆盖;宿主 provider 无法承载人格时跳过注入,派发照常原生运行。
190
+ - **角色人格注入到子代理(与链无关)**:当 subagent 的派发声明了本插件已知的角色(Assignment 的 `**Execute as**: <id>` 字段)时,该角色的 `persona` 会以作用域化的 system-prompt section 安装到子代理上,位置是 `deployment:persona-prefix` 槽位——与部署人格同一个槽位,因此角色人格**替换**该子代理的部署前缀,而不是叠加在其下。角色就是子代理的身份,而不只是路由决策;注入与路由无关:`chain` 为空的角色与带链角色的人格注入完全一致(链只负责选模型)。该 section 只存在于子代理自身的上下文中,因此人格**不持久**:宿主重启后恢复的角色子代理将不带其角色人格运行(恢复的 descriptor 无法承载人格)。子代理作用域中已有同名 section(持久化的 descriptor)时视为已投递;其他注册失败都会被兜住——子代理照常运行,其 notice 行会带上 ` (persona not applied)` 后缀。角色解析与 catalog 记录仍然生效,但两类已接受的边界完全没有人格投递:external(acp)子代理不创建本地 agent,因此**没有人格、没有 notice 行、也没有徽标**;宿主若从未挂载 `systemPrompt` 服务,子代理将不带人格运行,且**不会**带上 ` (persona not applied)` 后缀(issue `I-000059`)。
191
191
  - **角色在子代理自己的会话中声明**:声明了角色的派发会让子代理自身的会话得到恰好一条插件 notice 行,写明它被派发为哪个角色——`[role: <id>]`,当角色声明了人格但被跳过时追加 ` (persona not applied)`——在子代理首个非空 pre-step 写入一次,root 会话以及 `inherit`/未解析角色的派发永不写入。该行是持久轨迹行,子代理结束后角色仍可在其会话中读到。
192
192
  - **子代理角色徽标**:当 subagent 被**声明了角色地派发**——即其 Assignment 中的 `**Execute as**: <id>` 字段,也就是承载人格的同一处声明,并且是徽标的**唯一**来源(策略开或关皆同)——时,其会话在 Web 会话头部的标题旁显示一个紧凑的角色徽标;悬停显示 `role → 最近一次请求的路由`(最近一条已记录请求的路由,而非派发时的路由)。由于只读取被声明的头部字段,通过其他阶段解析出角色(`agentPreset` 匹配、role 规则或 LLM 自动匹配)但**未**在 Assignment 头部声明角色的子代理仍会按该角色路由,却**不**显示徽标——徽标绝不报告事后推断的角色。`inherit`/未解析角色的会话不显示徽标。徽标是持久的:它读取子代理自身的会话记录,因此**已结束的子代理会话**与**宿主重启之后**仍然显示。
193
193
  - **上下文窗口感知降级**:`triggerCodes` 接受任意 dsh 失败码,包括 `CONTEXT_WINDOW_EXCEEDED`——请求超出当前模型上下文时,降级到上下文窗口**更大**的候选(装不下的候选被跳过);由于路由本身健康,该切换是请求级的:不冷却、不浪费半开探针(见 [降级触发码](#降级触发码triggercodes))。
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { createRequire } from "node:module";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { LlmAdapter, LlmError, boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
4
+ import { randomUUID } from "node:crypto";
5
+ import { PERSONA_PREFIX_SECTION } from "@deepseek-ai/dsh-system-prompt";
4
6
  import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
5
7
  //#region .build/host/selectors.js
6
8
  var SelectorError = class extends Error {
@@ -828,7 +830,7 @@ function buildRoleNotice(role, personaNotApplied) {
828
830
  }
829
831
  });
830
832
  }
831
- function errorMessage$2(error) {
833
+ function errorMessage$3(error) {
832
834
  return error instanceof Error ? error.message : String(error);
833
835
  }
834
836
  function installRoleNotice(ctx, options) {
@@ -855,7 +857,7 @@ function installRoleNotice(ctx, options) {
855
857
  messages: [...decision.messages, notice]
856
858
  };
857
859
  } catch (error) {
858
- debug(`llm-fallbacks: role notice for agent "${agent.id}" skipped (contained, the step is unchanged): ${errorMessage$2(error)}`);
860
+ debug(`llm-fallbacks: role notice for agent "${agent.id}" skipped (contained, the step is unchanged): ${errorMessage$3(error)}`);
859
861
  return decision;
860
862
  }
861
863
  }, { prepend: true });
@@ -942,7 +944,7 @@ const roleProjectionUnit = {
942
944
  view: (state) => state === null ? null : state.role ?? null
943
945
  }
944
946
  };
945
- function errorMessage$1(error) {
947
+ function errorMessage$2(error) {
946
948
  return error instanceof Error ? error.message : String(error);
947
949
  }
948
950
  function installRoleProjection(ctx, options = {}) {
@@ -962,12 +964,12 @@ function installRoleProjection(ctx, options = {}) {
962
964
  }
963
965
  return registry.register(roleProjectionUnit);
964
966
  } catch (error) {
965
- debug(`llm-fallbacks: role projection not registered (contained): ${errorMessage$1(error)}`);
967
+ debug(`llm-fallbacks: role projection not registered (contained): ${errorMessage$2(error)}`);
966
968
  return () => {};
967
969
  }
968
970
  });
969
971
  } catch (error) {
970
- debug(`llm-fallbacks: role projection install skipped (contained): ${errorMessage$1(error)}`);
972
+ debug(`llm-fallbacks: role projection install skipped (contained): ${errorMessage$2(error)}`);
971
973
  }
972
974
  return () => {
973
975
  try {
@@ -1066,7 +1068,7 @@ function parentSessionIdOf(request) {
1066
1068
  function correlationKey(parentSessionId, label) {
1067
1069
  return `${parentSessionId}\u0000${label}`;
1068
1070
  }
1069
- function errorMessage(error) {
1071
+ function errorMessage$1(error) {
1070
1072
  return error instanceof Error ? error.message : String(error);
1071
1073
  }
1072
1074
  const subagentSeamStores = new WeakMap();
@@ -1100,20 +1102,20 @@ function installSubagentSeam(ctx, options) {
1100
1102
  } catch {}
1101
1103
  };
1102
1104
  const reportDegrade = (error) => {
1103
- debug(`llm-fallbacks: subagent role seam degraded to the native path (start unaffected): ${errorMessage(error)}`);
1105
+ debug(`llm-fallbacks: subagent role seam degraded to the native path (start unaffected): ${errorMessage$1(error)}`);
1104
1106
  };
1105
- const recordChild = (childSessionId, role, at, personaNotApplied) => {
1106
- const existing = records.get(childSessionId);
1107
- records.set(childSessionId, {
1107
+ const recordChild = (childId, role, at, personaMissed) => {
1108
+ const existing = records.get(childId);
1109
+ records.set(childId, {
1108
1110
  role,
1109
1111
  at,
1110
1112
  firstNoticePending: existing?.firstNoticePending ?? true,
1111
- ...personaNotApplied || existing?.personaNotApplied === true ? { personaNotApplied: true } : {}
1113
+ ...existing?.personaNotApplied === true || personaMissed === true ? { personaNotApplied: true } : {}
1112
1114
  });
1113
1115
  };
1114
- const beginCorrelation = (request, role, personaNotApplied) => {
1115
- const parentSessionId = parentSessionIdOf(request);
1116
- const label = typeof request?.label === "string" ? request.label : void 0;
1116
+ const beginCorrelation = (spec, role, at) => {
1117
+ const parentSessionId = parentSessionIdOf(spec?.request);
1118
+ const label = typeof spec?.label === "string" ? spec.label : void 0;
1117
1119
  if (parentSessionId === void 0 || label === void 0 || label === "") return void 0;
1118
1120
  const key = correlationKey(parentSessionId, label);
1119
1121
  if (!pending.has(key) && pending.size >= 64) {
@@ -1123,8 +1125,7 @@ function installSubagentSeam(ctx, options) {
1123
1125
  pending.delete(key);
1124
1126
  pending.set(key, {
1125
1127
  role,
1126
- at: Date.now(),
1127
- personaNotApplied
1128
+ at
1128
1129
  });
1129
1130
  return key;
1130
1131
  };
@@ -1168,68 +1169,13 @@ function installSubagentSeam(ctx, options) {
1168
1169
  }
1169
1170
  onValue(result);
1170
1171
  };
1171
- const personaGate = (service, providerName, surface) => {
1172
- if (typeof service.getProvider !== "function") return "unavailable";
1173
- const provider = service.getProvider(providerName);
1174
- if (provider === void 0) return "unknown";
1175
- if (surface === "one-shot") return provider.capabilities?.persona === true ? "ok" : "unsupported";
1176
- return typeof provider.prepareContinuable === "function" ? "ok" : "unsupported";
1177
- };
1178
- const withRolePersona = (service, providerName, request, role, surface) => {
1179
- if (request.persona !== void 0) return {
1180
- request,
1181
- personaNotApplied: false
1182
- };
1183
- const readRoles = options.roles;
1184
- if (readRoles === void 0) return {
1185
- request,
1186
- personaNotApplied: false
1187
- };
1188
- const persona = personaForRole(readRoles(), role);
1189
- if (persona === void 0) return {
1190
- request,
1191
- personaNotApplied: false
1192
- };
1193
- const gate = personaGate(service, providerName, surface);
1194
- if (gate === "unknown") {
1195
- debug(`llm-fallbacks: no subagent provider '${providerName}' is registered on the ${surface} start — role persona for '${role}' not delivered (the native start fails loud its own way)`);
1196
- return {
1197
- request,
1198
- personaNotApplied: true
1199
- };
1200
- }
1201
- if (gate === "unavailable") {
1202
- debug(`llm-fallbacks: role persona for '${role}' skipped on the ${surface} start — the '${SUBAGENT_SEAM_SERVICE}' runtime exposes no provider lookup, so the persona capability cannot be verified (the start proceeds unchanged)`);
1203
- return {
1204
- request,
1205
- personaNotApplied: true
1206
- };
1207
- }
1208
- if (gate === "unsupported") {
1209
- debug(surface === "one-shot" ? `llm-fallbacks: subagent provider '${providerName}' lacks the persona capability on the ${surface} start — role persona for '${role}' skipped (the start proceeds unchanged)` : `llm-fallbacks: subagent provider '${providerName}' does not support continuable children on the ${surface} surface — role persona for '${role}' skipped (the native continuable start fails loud its own way)`);
1210
- return {
1211
- request,
1212
- personaNotApplied: true
1213
- };
1214
- }
1215
- debug(`llm-fallbacks: role persona delivered via the native subagent persona channel for role '${role}' (${surface} start on provider '${providerName}')`);
1216
- return {
1217
- request: {
1218
- ...request,
1219
- persona
1220
- },
1221
- personaNotApplied: false
1222
- };
1223
- };
1224
- const mergePersonaAtSeam = (service, providerName, request, role, surface) => {
1172
+ const isLocalBackend = (service, providerName) => {
1225
1173
  try {
1226
- return withRolePersona(service, providerName, request, role, surface);
1174
+ if (typeof service.getProvider !== "function") return false;
1175
+ return typeof service.getProvider(providerName)?.prepareContinuable === "function";
1227
1176
  } catch (error) {
1228
1177
  reportDegrade(error);
1229
- return {
1230
- request,
1231
- personaNotApplied: false
1232
- };
1178
+ return false;
1233
1179
  }
1234
1180
  };
1235
1181
  const wrapService = (value) => {
@@ -1241,61 +1187,71 @@ function installSubagentSeam(ctx, options) {
1241
1187
  return value;
1242
1188
  }
1243
1189
  const service = value;
1244
- if (typeof service.start !== "function") {
1190
+ if (typeof service.startActivation !== "function") {
1245
1191
  if (!unreadableServiceLogged) {
1246
1192
  unreadableServiceLogged = true;
1247
- debug(`llm-fallbacks: subagent role seam skipped — '${SUBAGENT_SEAM_SERVICE}' is not a start-capable runtime`);
1193
+ debug(`llm-fallbacks: subagent role seam skipped — '${SUBAGENT_SEAM_SERVICE}' is not an activation-capable runtime`);
1248
1194
  }
1249
1195
  return value;
1250
1196
  }
1251
1197
  const cached = wrappers.get(value);
1252
1198
  if (cached !== void 0) return cached;
1253
1199
  const wrapper = Object.create(value);
1254
- wrapper.start = (name, request) => {
1255
- const role = resolveRoleAtSeam(request);
1256
- if (role === void 0) return service.start(name, request);
1257
- const persona = mergePersonaAtSeam(service, name, request, role, "one-shot");
1200
+ wrapper.startActivation = (spec) => {
1201
+ const role = resolveRoleAtSeam(spec?.request) ?? (typeof spec?.childId === "string" ? records.get(spec.childId)?.role : void 0);
1202
+ if (role === void 0) return service.startActivation(spec);
1203
+ const at = Date.now();
1204
+ let effectiveSpec = spec;
1205
+ let reservedId;
1206
+ let reservationPersonaMissed;
1207
+ try {
1208
+ if (isLocalBackend(service, spec.provider)) {
1209
+ reservedId = spec.childId ?? randomUUID();
1210
+ if (reservedId !== spec.childId) effectiveSpec = {
1211
+ ...spec,
1212
+ childId: reservedId
1213
+ };
1214
+ recordChild(reservedId, role, at);
1215
+ }
1216
+ } catch (error) {
1217
+ if (reservedId !== void 0) records.delete(reservedId);
1218
+ effectiveSpec = spec;
1219
+ reservedId = void 0;
1220
+ reportDegrade(error);
1221
+ try {
1222
+ reservationPersonaMissed = personaForRole(options.roles?.() ?? [], role) !== void 0;
1223
+ } catch (personaError) {
1224
+ reportDegrade(personaError);
1225
+ }
1226
+ }
1258
1227
  let pendingKey;
1259
1228
  try {
1260
- pendingKey = beginCorrelation(request, role, persona.personaNotApplied);
1229
+ pendingKey = beginCorrelation(spec, role, at);
1261
1230
  } catch (error) {
1262
1231
  reportDegrade(error);
1263
1232
  }
1264
1233
  let result;
1265
1234
  try {
1266
- result = service.start(name, persona.request);
1235
+ result = service.startActivation(effectiveSpec);
1267
1236
  } catch (error) {
1268
1237
  endCorrelation(pendingKey);
1238
+ if (reservedId !== void 0) records.delete(reservedId);
1269
1239
  throw error;
1270
1240
  }
1271
1241
  try {
1272
- observeStartResult(result, (childSessionId) => {
1242
+ observeStartResult(result, (childId) => {
1243
+ endCorrelation(pendingKey);
1244
+ recordChild(childId, role, at, reservationPersonaMissed);
1245
+ }, () => {}, () => {
1273
1246
  endCorrelation(pendingKey);
1274
- recordChild(childSessionId, role, Date.now(), persona.personaNotApplied);
1275
- }, () => {}, () => endCorrelation(pendingKey));
1247
+ if (reservedId !== void 0) records.delete(reservedId);
1248
+ });
1276
1249
  } catch (error) {
1277
1250
  endCorrelation(pendingKey);
1278
1251
  reportDegrade(error);
1279
1252
  }
1280
1253
  return result;
1281
1254
  };
1282
- const startContinuable = service.startContinuable;
1283
- if (typeof startContinuable === "function") wrapper.startContinuable = (spec) => {
1284
- const role = resolveRoleAtSeam(spec?.request) ?? (typeof spec?.childId === "string" ? records.get(spec.childId)?.role : void 0);
1285
- if (role === void 0) return startContinuable.call(service, spec);
1286
- const outcome = mergePersonaAtSeam(service, spec.provider, spec.request, role, "continuable");
1287
- const effectiveSpec = outcome.request === spec.request ? spec : {
1288
- ...spec,
1289
- request: outcome.request
1290
- };
1291
- const result = startContinuable.call(service, effectiveSpec);
1292
- try {
1293
- observeStartResult(result, (childSessionId) => recordChild(childSessionId, role, Date.now(), outcome.personaNotApplied), () => {}, () => {});
1294
- } catch (error) {
1295
- reportDegrade(error);
1296
- }
1297
- return result;
1298
- };
1299
1255
  wrappers.set(value, wrapper);
1300
1256
  return wrapper;
1301
1257
  };
@@ -1322,7 +1278,7 @@ function installSubagentSeam(ctx, options) {
1322
1278
  const claim = pending.get(key);
1323
1279
  if (claim === void 0) return;
1324
1280
  pending.delete(key);
1325
- recordChild(childSessionId, claim.role, claim.at, claim.personaNotApplied);
1281
+ recordChild(childSessionId, claim.role, claim.at);
1326
1282
  } catch (error) {
1327
1283
  reportDegrade(error);
1328
1284
  }
@@ -1347,6 +1303,96 @@ function installSubagentSeam(ctx, options) {
1347
1303
  return seam;
1348
1304
  }
1349
1305
  //#endregion
1306
+ //#region .build/host/role-persona.js
1307
+ function errorMessage(error) {
1308
+ return error instanceof Error ? error.message : String(error);
1309
+ }
1310
+ const rolePersonaStates = new WeakMap();
1311
+ function rolePersonaStateFor(root) {
1312
+ const existing = rolePersonaStates.get(root);
1313
+ if (existing !== void 0) return existing;
1314
+ const created = new Map();
1315
+ rolePersonaStates.set(root, created);
1316
+ return created;
1317
+ }
1318
+ function installRolePersona(ctx, options) {
1319
+ const root = ctx.root;
1320
+ const registered = rolePersonaStateFor(root);
1321
+ const owner = {};
1322
+ const fibers = new Map();
1323
+ const debug = (message) => {
1324
+ try {
1325
+ options.debug?.(message);
1326
+ } catch {}
1327
+ };
1328
+ const contain = (agent, record, error) => {
1329
+ debug(`llm-fallbacks: role persona for agent "${agent.id}" (role "${record.role}") not applied — contained, the child runs unchanged: ${errorMessage(error)}`);
1330
+ record.personaNotApplied = true;
1331
+ };
1332
+ const release = (id) => {
1333
+ const fiber = fibers.get(id);
1334
+ if (fiber === void 0) return;
1335
+ fibers.delete(id);
1336
+ fiber.dispose().catch((error) => {
1337
+ debug(`llm-fallbacks: role persona section for agent "${id}" not withdrawn (contained): ${errorMessage(error)}`);
1338
+ });
1339
+ };
1340
+ const installPersona = async (agent) => {
1341
+ const record = options.records.get(agent.id);
1342
+ if (record === void 0) return;
1343
+ if (registered.has(agent.id)) return;
1344
+ let persona;
1345
+ try {
1346
+ persona = personaForRole(options.roles(), record.role);
1347
+ } catch (error) {
1348
+ debug(`llm-fallbacks: role persona for agent "${agent.id}" not resolved — contained, the child runs unchanged: ${errorMessage(error)}`);
1349
+ return;
1350
+ }
1351
+ if (persona === void 0) return;
1352
+ const text = persona;
1353
+ registered.set(agent.id, owner);
1354
+ try {
1355
+ const fiber = agent.ctx.inject(["systemPrompt"], (scope) => {
1356
+ try {
1357
+ scope.systemPrompt.section({
1358
+ name: PERSONA_PREFIX_SECTION,
1359
+ order: scope.systemPrompt.getSectionOrder("DEPLOYMENT_PERSONA_PREFIX"),
1360
+ text
1361
+ });
1362
+ } catch (error) {
1363
+ if (error instanceof Error && error.message === `prompt section "${PERSONA_PREFIX_SECTION}" is already registered in this scope`) {
1364
+ debug(`llm-fallbacks: role persona for agent "${agent.id}" not installed — a section named "${PERSONA_PREFIX_SECTION}" already exists in its scope: ${errorMessage(error)}`);
1365
+ return;
1366
+ }
1367
+ contain(agent, record, error);
1368
+ }
1369
+ });
1370
+ fibers.set(agent.id, fiber);
1371
+ await fiber;
1372
+ } catch (error) {
1373
+ release(agent.id);
1374
+ contain(agent, record, error);
1375
+ }
1376
+ };
1377
+ const disposers = [];
1378
+ disposers.push(ctx.on("agent/created", async ({ agent }) => {
1379
+ await installPersona(agent);
1380
+ }));
1381
+ disposers.push(ctx.on("agent/disposed", ({ agent }) => {
1382
+ release(agent.id);
1383
+ registered.delete(agent.id);
1384
+ }));
1385
+ const agents = ctx.get("agents");
1386
+ if (typeof agents?.list === "function") for (const agent of agents.list()) installPersona(agent);
1387
+ const dispose = () => {
1388
+ for (const off of disposers.splice(0)) off();
1389
+ for (const id of [...fibers.keys()]) release(id);
1390
+ for (const [id, holder] of [...registered]) if (holder === owner) registered.delete(id);
1391
+ };
1392
+ ctx.effect(() => dispose, "llm-fallbacks: role persona listener");
1393
+ return { dispose };
1394
+ }
1395
+ //#endregion
1350
1396
  //#region .build/host/cooldown.js
1351
1397
  var CooldownStore = class {
1352
1398
  entries = new Map();
@@ -3151,6 +3197,11 @@ function apply(ctx, config = defaultFallbacksConfig) {
3151
3197
  debug: (message) => logger.debug(message)
3152
3198
  });
3153
3199
  if (!subagentSeam.ownsWrapper) ctx.logger("llm-fallbacks").debug("subagent role seam wrapper already installed on this context root — this fiber registers the notice emitter and the projection unit only (multi-fiber dedupe)");
3200
+ installRolePersona(ctx, {
3201
+ records: subagentSeam.records,
3202
+ roles: () => source().roles.list,
3203
+ debug: (message) => logger.debug(message)
3204
+ });
3154
3205
  try {
3155
3206
  new FallbacksConfigGateway(ctx, bridge, seeds, () => {
3156
3207
  try {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Role-persona delivery over `agent/created` (plan dsh-021-subagent-seam-repair
3
+ * Task 1).
4
+ *
5
+ * rc.1 removed `SubagentStartRequest.persona`, so the resolved dispatch role's
6
+ * persona is delivered as a SCOPED system-prompt section on the CHILD's own
7
+ * context — the shipped `file-reference-local` pattern (no request mutation, no
8
+ * provider capability gate, no Agent Preset). The section carries the SAME name
9
+ * and order the global deployment persona uses (`SystemPrompt.section`: a
10
+ * scoped section shadows a global section with the same name), so the child's
11
+ * persona REPLACES the deployment prefix instead of stacking under it — the
12
+ * semantics the removed request slot had.
13
+ *
14
+ * Timing: `agent/created` is a serial event awaited before the child's first
15
+ * prompt assembly, and this listener AWAITS the injected section fiber: cordis
16
+ * activates a child fiber on a later tick, so awaiting it is what makes the
17
+ * persona observable by the time the dispatch resolves (the shipped
18
+ * `file-reference-local` shape). The seam's `agent/pre-step` notice emitter
19
+ * therefore always observes a child whose persona section is already
20
+ * registered; both surfaces read the SAME ROOT-shared record map, which is what
21
+ * keeps the ` (persona not applied)` suffix truthful.
22
+ *
23
+ * Ownership: the injected fiber belongs to the CHILD's scope, so it does not
24
+ * die with this listener's fiber — each listener holds ITS OWN fibers per agent
25
+ * and releases them on `agent/disposed`, on plugin-fiber teardown, and on
26
+ * explicit `dispose()`. The root-shared claim that dedupes sibling listeners
27
+ * records which listener owns it, so one installer's teardown never releases
28
+ * another's fibers or claims, and a repeated notification never replaces the
29
+ * handle to the fiber that holds the live registration.
30
+ *
31
+ * Containment (the seam's degrade-never-crash invariant, and required here
32
+ * because a throwing serial `agent/created` listener fails agent creation):
33
+ * only an agent whose id hits the shared record map is touched (a miss is a
34
+ * silent no-op), the registry's exact same-name SCOPED section duplicate means
35
+ * a persona section is already on that scope (one attribution-free debug line,
36
+ * no verdict — the registry cannot say who registered it), and every other
37
+ * throw — including a failed fiber activation — is contained to ONE debug line
38
+ * plus the record's sticky `personaNotApplied`.
39
+ *
40
+ * @module dsh-llm-fallbacks/role-persona
41
+ */
42
+ import type { Context } from '@deepseek-ai/cordis';
43
+ import { type SubagentSeamRecord } from './subagents-seam.ts';
44
+ import type { FallbacksRole } from './config.ts';
45
+ /** Options for {@link installRolePersona}. */
46
+ export interface RolePersonaOptions {
47
+ /**
48
+ * The seam's ROOT-shared per-child records — the SAME map the notice
49
+ * emitter reads; the listener writes `personaNotApplied` verdicts back
50
+ * into it (sticky per the existing {@link SubagentSeamRecord} contract).
51
+ */
52
+ records: Map<string, SubagentSeamRecord>;
53
+ /** Live declared roles (`roles.list`) — the persona source, read per agent/created. */
54
+ roles: () => readonly FallbacksRole[];
55
+ /** Contained debug sink (at most one line per contained degrade). */
56
+ debug?: (message: string) => void;
57
+ }
58
+ /** The installed listener surface (explicit teardown; fiber teardown also stops it). */
59
+ export interface RolePersona {
60
+ dispose(): void;
61
+ }
62
+ /**
63
+ * Install the role-persona listener on `ctx`: register the resolved role's
64
+ * persona as a scoped `deployment:persona-prefix` section on every created
65
+ * agent whose id hits the shared record map, plus a `agents.list()` backfill
66
+ * for children that already exist at install time.
67
+ *
68
+ * Returns the installed listener (its `dispose` withdraws this listener's OWN
69
+ * registrations; a fiber teardown stops it as well).
70
+ */
71
+ export declare function installRolePersona(ctx: Context, options: RolePersonaOptions): RolePersona;
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Dispatch-seam role resolution + per-child record (plan
3
- * role-based-subagent-adoption Task 1).
3
+ * role-based-subagent-adoption Task 1; rc.1 `startActivation` migration, plan
4
+ * dsh-021-subagent-seam-repair Task 2).
4
5
  *
5
6
  * The seam is the documented cordis service-read waterfall
6
7
  * (`Events['internal/get']`, `@deepseek-ai/cordis`
7
8
  * `lib/types/events.d.ts`): a listener wraps the `subagents` service VALUE on
8
- * read and returns a prototype-delegating wrapper whose `start` /
9
- * `startContinuable` resolve the dispatch role from the incoming request
10
- * BEFORE delegating. The real service object is never mutated; readers of the
9
+ * read and returns a prototype-delegating wrapper whose `startActivation`
10
+ * resolves the dispatch role from the incoming activation spec BEFORE
11
+ * delegating. The real service object is never mutated; readers of the
11
12
  * `subagents` service transparently receive the wrapper.
12
13
  *
13
14
  * Role source (plan Global Constraints): the child request's prompt carries
@@ -29,34 +30,36 @@
29
30
  * ambiguity rule stands unchanged: the DISTINCT DECLARED ids must collapse to
30
31
  * exactly one, else the dispatch is a no-op — never a first-occurrence pick.
31
32
  *
32
- * Per-child record: the wrapped start's RESULT carries the child session id —
33
- * `start` resolves to a `SubagentRun` (`id` IS the published child session id
34
- * for a local run) and `startContinuable` to a `ContinuableStart` (`childId`).
35
- * The record `{ role, at, firstNoticePending }` is keyed by that id (Task 3
36
- * reads it to emit the one role notice row). A result carrying only a
37
- * `jobId` — a registry job id, NOT a session id — is never keyed: the child is
38
- * correlated instead through the parent-owned `subagent/catalog` session event
39
- * `{ childId, label }` joined on the delegating parent + request label.
33
+ * Per-child record + the timing rule (rc.1 `startActivation`): the record
34
+ * `{ role, at, firstNoticePending }` is keyed by the child id (Task 3 reads it
35
+ * to emit the one role notice row). The child is created INSIDE the native
36
+ * call — `manager.startLocal` mints/uses `spec.childId` and only then resolves
37
+ * the activation — so `agent/created` fires BEFORE any `.then` on this side
38
+ * could key a record off the resolved `SubagentActivation.childId`. The
39
+ * wrapper therefore reserves the child id itself for a LOCAL backend (the
40
+ * provider exposes `prepareContinuable`; a reserved id on any other backend is
41
+ * rejected with `UNSUPPORTED_CAPABILITY`) and writes the record SYNCHRONOUSLY
42
+ * before delegating, deleting it when the native call fails. An EXTERNAL
43
+ * backend allocates its own child id, receives the caller's OWN spec object
44
+ * untouched, and has its record keyed off the resolved activation's `childId`.
40
45
  *
41
- * Persona delivery (plan Task 2): the SAME single resolution point merges the
42
- * declared role's `roles.list[].persona` (trimmed, non-empty) into the
43
- * request's NATIVE `persona` slot — the slot the runtime composes as the
44
- * scoped `deployment:persona-prefix` system-prompt section on the child. The
45
- * source is the RESOLVED ROLE only, so delivery is chain-independent: a
46
- * `chain: []` role gets its persona exactly like a chained one (no routing
47
- * state is read). Both native start surfaces are covered, with the gate each
48
- * one's fail-loud contract needs (measured on the installed
49
- * `@deepseek-ai/dsh-subagent`): the one-shot `start` REJECTS a
50
- * request carrying `persona` for a provider whose
51
- * `getProvider(name).capabilities.persona` is not `true` (`lib/index.js:3284,3340-3363`,
52
- * `assertCapabilities`), so the merge pre-checks that flag; the continuable
53
- * surface is composed by the continuation manager, which applies
54
- * `request.persona` unconditionally (`lib/index.js:1691`) and is gated by
55
- * the provider's `prepareContinuable` presence instead (`lib/index.js:3316-3319`).
56
- * A gate miss skips the persona with ONE contained debug log — never a failed
57
- * start. An explicit caller `persona` WINS (the slot is filled only when it is
58
- * absent), and every skip path returns the caller's OWN request object, so the
59
- * native call stays byte-identical.
46
+ * A result carrying no child id — a `jobId` is a registry id, NOT a session id
47
+ * — is never keyed: the child is correlated instead through the parent-owned
48
+ * `subagent/catalog` session event `{ childId, label }` joined on the
49
+ * delegating parent + the activation `label` (the same string the cataloged
50
+ * child membership carries).
51
+ *
52
+ * Persona delivery is NO LONGER this module's job (plan Task 1): rc.1 removed
53
+ * `SubagentStartRequest.persona`, so the resolved role's persona is registered
54
+ * as a scoped `deployment:persona-prefix` section on the CHILD's own context
55
+ * by `./role-persona.ts`, over the SAME ROOT-shared record map this module
56
+ * writes. That listener owns the `personaNotApplied` verdict the notice row
57
+ * reads — EXCEPT when a reservation failure means it can never see the child
58
+ * (the runtime mints its own child id and `agent/created` has already run), in
59
+ * which case this module resolves the same question against the live roles
60
+ * source and carries the verdict onto the record keyed off the RESOLVED id,
61
+ * before the child's first pre-step. This module writes no request field,
62
+ * reads no provider persona capability, and injects no agent preset.
60
63
  *
61
64
  * In-session notice row (plan Task 3): the SAME install point registers the
62
65
  * `agent/pre-step` emitter (`./role-notice.ts`) over the record map it owns, so
@@ -103,14 +106,14 @@ export interface SubagentSeamRecord {
103
106
  * The DECLARED RAW role id resolved at the dispatch seam.
104
107
  *
105
108
  * Authoritative-role precedence (CF-10, documented rather than resolved by
106
- * re-branding): a `startContinuable` resume re-keys the child and may resolve
107
- * a DIFFERENT declared role, and this field is then rewritten while the
109
+ * re-branding): a resume that reuses a reserved `childId` may resolve a
110
+ * DIFFERENT declared role, and this field is then rewritten while the
108
111
  * durable notice row the child's session already carries keeps the FIRST role.
109
112
  * The projection deliberately reads the notice row, so the durable role is the
110
113
  * child's announced identity and this record is only the LIVE dispatch hint
111
- * (what the next start / the resume fallback would use). The two can therefore
112
- * disagree after a re-dispatch until the child is disposed; no consumer may
113
- * treat the record as the durable role.
114
+ * (what the next activation / the resume fallback would use). The two can
115
+ * therefore disagree after a re-dispatch until the child is disposed; no
116
+ * consumer may treat the record as the durable role.
114
117
  */
115
118
  role: string;
116
119
  /** When the record was written (epoch ms). */
@@ -126,20 +129,24 @@ export interface SubagentSeamRecord {
126
129
  firstNoticePending: boolean;
127
130
  /**
128
131
  * `true` when the role DECLARES a persona and that persona was NOT delivered
129
- * (Task 2's capability gate declined it, or no provider was registered to
130
- * carry it). Absent when there was nothing to report: the persona was
131
- * delivered, the caller set one (explicit intent wins), the role declares
132
- * none, or no persona source is wired. Task 3 appends
133
- * ` (persona not applied)` on `true` only.
132
+ * onto the child. Written by the `agent/created` role-persona listener
133
+ * (`./role-persona.ts`) over this same map — except when a reservation
134
+ * failure means that listener can never see the child (the runtime mints its
135
+ * own child id), where the seam itself carries the verdict onto the record
136
+ * keyed off the RESOLVED id. The seam never delivers or reads a persona
137
+ * other than for that one question. Absent when there was nothing to report:
138
+ * the section was registered, a same-name section was already on the child's
139
+ * scope (whoever registered it), the listener never saw the child (an
140
+ * EXTERNAL backend creates no local agent), the role declares none, or no
141
+ * persona source is wired. Task 3 appends ` (persona not applied)` on `true`
142
+ * only.
134
143
  *
135
- * STICKY (Task 3 review Minor 4): a record rewrite (a `startContinuable`
136
- * resume re-keys the child) can only ADD this verdict, never clear it. A
137
- * resume recomputes the merge outcome on its own surface — and that surface
138
- * legitimately returns `false` when it delivers the CALLER's persona or when
139
- * its own merge degrades — but neither retroactively installs the role's
140
- * DECLARED persona on the dispatch that already reported it missing. The
141
- * child's one notice row is a claim about that dispatch, so the first `true`
142
- * survives; a later `false` is simply not recorded.
144
+ * STICKY (Task 3 review Minor 4): a record rewrite — a resume reusing the
145
+ * reserved `childId`, or the resolved activation re-keying the same child
146
+ * after `agent/created` already ran — can only ADD this verdict, never clear
147
+ * it. A rewrite carries no verdict of its own, and the child's one notice row
148
+ * is a claim about the dispatch that already reported it missing, so the first
149
+ * `true` survives.
143
150
  */
144
151
  personaNotApplied?: boolean;
145
152
  }
@@ -149,29 +156,29 @@ export interface SubagentPromptBlockView {
149
156
  readonly text?: string;
150
157
  }
151
158
  /**
152
- * Structural view of the one-shot start request (consumed fields only): the
153
- * `prompt` is the role-extraction source, `label` + `parent` are the
154
- * catalog-correlation join key, and `persona` is the NATIVE per-child persona
155
- * slot the role persona is merged into (plan Global Constraints: an explicit
156
- * caller value wins). The wrapper forwards the request object itself, so every
157
- * other field reaches the service unchanged.
159
+ * Structural view of the activation request (consumed fields only): `prompt` is
160
+ * the role-extraction source and `parent` is the catalog-correlation join half.
161
+ * The wrapper forwards the request OBJECT itself, so every other field (cwd,
162
+ * agentOptions, outputSchema, maxDepth, the signal) reaches the service
163
+ * unchanged.
158
164
  */
159
165
  export interface SubagentStartRequestView {
160
166
  readonly prompt?: readonly SubagentPromptBlockView[];
161
- readonly label?: string;
162
167
  readonly parent?: {
163
168
  readonly session?: {
164
169
  readonly id?: string;
165
170
  };
166
171
  };
167
- readonly persona?: string;
168
172
  }
169
173
  /**
170
- * Structural view of the continuable start spec (consumed fields only):
171
- * `childId` is the caller-reserved identity a resume already carries, used as
172
- * the record fallback when the resume request has no Assignment header.
174
+ * Structural view of the activation spec (rc.1 `SubagentActivationSpec`,
175
+ * consumed fields only): `childId` is the caller-reserved identity a resume
176
+ * already carries — also the record fallback when the request carries no
177
+ * Assignment header — and `label` is the catalog join's other half (the
178
+ * cataloged child membership carries this exact string). `signal`, `delivery`
179
+ * and every future field travel with the caller's spec object.
173
180
  */
174
- export interface ContinuableStartSpecView {
181
+ export interface SubagentActivationSpecView {
175
182
  readonly provider: string;
176
183
  readonly label?: string;
177
184
  readonly childId?: string;
@@ -180,14 +187,16 @@ export interface ContinuableStartSpecView {
180
187
  /** Options for {@link installSubagentSeam}. */
181
188
  export interface SubagentSeamOptions {
182
189
  /**
183
- * Live declared-role id map (trimmed id → DECLARED RAW id), read per start
184
- * so a settings change is observed without a re-install.
190
+ * Live declared-role id map (trimmed id → DECLARED RAW id), read per
191
+ * activation so a settings change is observed without a re-install.
185
192
  */
186
193
  roleIds: () => ReadonlyMap<string, string>;
187
194
  /**
188
- * Live declared roles (`roles.list`) — the persona source, read per start so
189
- * a settings/persona edit is observed without a re-install. Absent ⇒ the
190
- * seam only records roles (no persona delivery).
195
+ * Live declared roles (`roles.list`) — the SAME source `./role-persona.ts`
196
+ * reads. Consulted ONLY when a reservation failure means that listener can
197
+ * never see the child: the seam is then the only place left that can carry
198
+ * the persona verdict to the child's notice row. Absent (no persona source is
199
+ * wired) ⇒ no verdict, exactly like a listener that never saw the child.
191
200
  */
192
201
  roles?: () => readonly FallbacksRole[];
193
202
  /** Contained debug sink (at most one line per contained degrade). */
@@ -196,10 +205,12 @@ export interface SubagentSeamOptions {
196
205
  /** The installed seam (Task 3 surface + explicit teardown). */
197
206
  export interface SubagentSeam {
198
207
  /**
199
- * Per-child records keyed by the child SESSION id (the wrapped start's
200
- * result id). ROOT-shared (CF-5): every fiber applied over the same root reads
208
+ * Per-child records keyed by the child id — the reserved id this wrapper
209
+ * wrote before a local activation, or the `childId` a resolved activation
210
+ * reported. ROOT-shared (CF-5): every fiber applied over the same root reads
201
211
  * the same map. Mutable by design: Task 3 clears `firstNoticePending` after
202
- * emitting the notice.
212
+ * emitting the notice, and the role-persona listener writes
213
+ * `personaNotApplied`.
203
214
  */
204
215
  readonly records: Map<string, SubagentSeamRecord>;
205
216
  /**
@@ -253,7 +264,9 @@ export declare const PENDING_CORRELATION_LIMIT = 64;
253
264
  */
254
265
  export declare function resolveDeclaredRoleFromAssignment(prompt: string, roleIds: ReadonlyMap<string, string>): string | undefined;
255
266
  /**
256
- * PURE, chain-independent persona lookup (plan Task 2 / Global Constraints):
267
+ * PURE, chain-independent persona lookup for the `agent/created` role-persona
268
+ * listener (`./role-persona.ts`; rc.1 delivers the persona as a scoped prompt
269
+ * section on the child, never as a request field):
257
270
  * the DECLARED `persona` of `roleId`, trimmed, and only when it is still
258
271
  * non-empty after the trim — a blank persona is NO persona. `roleId` is the
259
272
  * DECLARED RAW id the seam already resolved, so this is an exact id match, not
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "dsh",
6
6
  "dsh-plugin"
7
7
  ],
8
- "version": "0.6.4",
8
+ "version": "0.6.6",
9
9
  "type": "module",
10
10
  "main": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts",
@@ -64,30 +64,31 @@
64
64
  "use-sync-external-store": "^1.6.0"
65
65
  },
66
66
  "peerDependencies": {
67
- "@deepseek-ai/cordis": "^4.0.2",
68
- "@deepseek-ai/dsh-agent": "^0.2.0-rc.1",
69
- "@deepseek-ai/dsh-api-gateway": "^0.2.0-rc.1",
70
- "@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.1",
71
- "@deepseek-ai/dsh-api-session-controller": "^0.2.0-rc.1",
72
- "@deepseek-ai/dsh-client-connection": "^0.2.0-rc.1",
73
- "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.1",
74
- "@deepseek-ai/dsh-client-store": "^0.2.0-rc.1",
75
- "@deepseek-ai/dsh-client-ui-chat": "^0.2.0-rc.1",
76
- "@deepseek-ai/dsh-client-ui-conversation": "^0.2.0-rc.1",
77
- "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.0-rc.1",
78
- "@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.1",
79
- "@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.1",
80
- "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.1",
81
- "@deepseek-ai/dsh-commands": "^0.2.0-rc.1",
82
- "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
83
- "@deepseek-ai/dsh-llm-retry": "^0.2.0-rc.1",
84
- "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
85
- "@deepseek-ai/dsh-session-projection": "^0.2.0-rc.1",
86
- "@deepseek-ai/dsh-settings": "^0.2.0-rc.1",
87
- "@deepseek-ai/dsh-tool-subagent": "^0.2.0-rc.1",
88
- "@deepseek-ai/dsh-typert-protocol": "^0.2.0-rc.1",
89
- "@deepseek-ai/dsh-typert-registry": "^0.2.0-rc.1",
90
- "@deepseek-ai/schemastery": "^3.18.2",
67
+ "@deepseek-ai/cordis": "^4.0.5",
68
+ "@deepseek-ai/dsh-agent": "^0.2.1-rc.2",
69
+ "@deepseek-ai/dsh-api-gateway": "^0.2.1-rc.2",
70
+ "@deepseek-ai/dsh-api-remotes": "^0.2.1-rc.2",
71
+ "@deepseek-ai/dsh-api-session-controller": "^0.2.1-rc.2",
72
+ "@deepseek-ai/dsh-client-connection": "^0.2.1-rc.2",
73
+ "@deepseek-ai/dsh-client-locale": "^0.2.1-rc.2",
74
+ "@deepseek-ai/dsh-client-store": "^0.2.1-rc.2",
75
+ "@deepseek-ai/dsh-client-ui-chat": "^0.2.1-rc.2",
76
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.2.1-rc.2",
77
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.1-rc.2",
78
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.2.1-rc.2",
79
+ "@deepseek-ai/dsh-client-ui-settings": "^0.2.1-rc.2",
80
+ "@deepseek-ai/dsh-client-ui-slots": "^0.2.1-rc.2",
81
+ "@deepseek-ai/dsh-commands": "^0.2.1-rc.2",
82
+ "@deepseek-ai/dsh-llm": "^0.2.1-rc.2",
83
+ "@deepseek-ai/dsh-llm-retry": "^0.2.1-rc.2",
84
+ "@deepseek-ai/dsh-session": "^0.2.1-rc.2",
85
+ "@deepseek-ai/dsh-session-projection": "^0.2.1-rc.2",
86
+ "@deepseek-ai/dsh-settings": "^0.2.1-rc.2",
87
+ "@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.2",
88
+ "@deepseek-ai/dsh-tool-subagent": "^0.2.1-rc.2",
89
+ "@deepseek-ai/dsh-typert-protocol": "^0.2.1-rc.2",
90
+ "@deepseek-ai/dsh-typert-registry": "^0.2.1-rc.2",
91
+ "@deepseek-ai/schemastery": "^3.18.5",
91
92
  "react": "^18.2.0"
92
93
  },
93
94
  "devDependencies": {