dsh-llm-fallbacks 0.6.5 → 0.6.7

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/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 {
@@ -695,6 +697,11 @@ async function resolveRoleAtDispatch(agent, rules, roleIds, opts) {
695
697
  const declared = roleIds.get(preset);
696
698
  if (declared !== void 0) return declared;
697
699
  }
700
+ const dispatch = opts.dispatchRole?.trim();
701
+ if (dispatch !== void 0 && dispatch !== "" && dispatch !== "inherit") {
702
+ const declared = roleIds.get(dispatch);
703
+ if (declared !== void 0) return declared;
704
+ }
698
705
  const role = resolveRole(agent, rules, roleIds, opts.warn, opts.servedRoute);
699
706
  if (role !== "inherit") return role;
700
707
  if (opts.automatchEnabled && opts.automatch !== void 0) {
@@ -828,7 +835,7 @@ function buildRoleNotice(role, personaNotApplied) {
828
835
  }
829
836
  });
830
837
  }
831
- function errorMessage$2(error) {
838
+ function errorMessage$3(error) {
832
839
  return error instanceof Error ? error.message : String(error);
833
840
  }
834
841
  function installRoleNotice(ctx, options) {
@@ -855,7 +862,7 @@ function installRoleNotice(ctx, options) {
855
862
  messages: [...decision.messages, notice]
856
863
  };
857
864
  } catch (error) {
858
- debug(`llm-fallbacks: role notice for agent "${agent.id}" skipped (contained, the step is unchanged): ${errorMessage$2(error)}`);
865
+ debug(`llm-fallbacks: role notice for agent "${agent.id}" skipped (contained, the step is unchanged): ${errorMessage$3(error)}`);
859
866
  return decision;
860
867
  }
861
868
  }, { prepend: true });
@@ -863,6 +870,9 @@ function installRoleNotice(ctx, options) {
863
870
  //#endregion
864
871
  //#region .build/host/role-projection-key.js
865
872
  const ROLE_PROJECTION_KEY = "fallbacksSubagentRole";
873
+ function readRoleProjectionValue(value) {
874
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
875
+ }
866
876
  const ROLE_STATE_SCHEMA = { parse(value) {
867
877
  if (value === null) return null;
868
878
  if (typeof value === "object") {
@@ -942,7 +952,7 @@ const roleProjectionUnit = {
942
952
  view: (state) => state === null ? null : state.role ?? null
943
953
  }
944
954
  };
945
- function errorMessage$1(error) {
955
+ function errorMessage$2(error) {
946
956
  return error instanceof Error ? error.message : String(error);
947
957
  }
948
958
  function installRoleProjection(ctx, options = {}) {
@@ -962,12 +972,12 @@ function installRoleProjection(ctx, options = {}) {
962
972
  }
963
973
  return registry.register(roleProjectionUnit);
964
974
  } catch (error) {
965
- debug(`llm-fallbacks: role projection not registered (contained): ${errorMessage$1(error)}`);
975
+ debug(`llm-fallbacks: role projection not registered (contained): ${errorMessage$2(error)}`);
966
976
  return () => {};
967
977
  }
968
978
  });
969
979
  } catch (error) {
970
- debug(`llm-fallbacks: role projection install skipped (contained): ${errorMessage$1(error)}`);
980
+ debug(`llm-fallbacks: role projection install skipped (contained): ${errorMessage$2(error)}`);
971
981
  }
972
982
  return () => {
973
983
  try {
@@ -1066,7 +1076,7 @@ function parentSessionIdOf(request) {
1066
1076
  function correlationKey(parentSessionId, label) {
1067
1077
  return `${parentSessionId}\u0000${label}`;
1068
1078
  }
1069
- function errorMessage(error) {
1079
+ function errorMessage$1(error) {
1070
1080
  return error instanceof Error ? error.message : String(error);
1071
1081
  }
1072
1082
  const subagentSeamStores = new WeakMap();
@@ -1082,6 +1092,9 @@ function seamStateFor(root) {
1082
1092
  return created;
1083
1093
  }
1084
1094
  const subagentSeamRoots = new WeakSet();
1095
+ function subagentSeamOf(ctx) {
1096
+ return subagentSeamStores.get(ctx);
1097
+ }
1085
1098
  function installSubagentSeam(ctx, options) {
1086
1099
  const root = ctx.root;
1087
1100
  const state = seamStateFor(root);
@@ -1100,20 +1113,20 @@ function installSubagentSeam(ctx, options) {
1100
1113
  } catch {}
1101
1114
  };
1102
1115
  const reportDegrade = (error) => {
1103
- debug(`llm-fallbacks: subagent role seam degraded to the native path (start unaffected): ${errorMessage(error)}`);
1116
+ debug(`llm-fallbacks: subagent role seam degraded to the native path (start unaffected): ${errorMessage$1(error)}`);
1104
1117
  };
1105
- const recordChild = (childSessionId, role, at, personaNotApplied) => {
1106
- const existing = records.get(childSessionId);
1107
- records.set(childSessionId, {
1118
+ const recordChild = (childId, role, at, personaMissed) => {
1119
+ const existing = records.get(childId);
1120
+ records.set(childId, {
1108
1121
  role,
1109
1122
  at,
1110
1123
  firstNoticePending: existing?.firstNoticePending ?? true,
1111
- ...personaNotApplied || existing?.personaNotApplied === true ? { personaNotApplied: true } : {}
1124
+ ...existing?.personaNotApplied === true || personaMissed === true ? { personaNotApplied: true } : {}
1112
1125
  });
1113
1126
  };
1114
- const beginCorrelation = (request, role, personaNotApplied) => {
1115
- const parentSessionId = parentSessionIdOf(request);
1116
- const label = typeof request?.label === "string" ? request.label : void 0;
1127
+ const beginCorrelation = (spec, role, at) => {
1128
+ const parentSessionId = parentSessionIdOf(spec?.request);
1129
+ const label = typeof spec?.label === "string" ? spec.label : void 0;
1117
1130
  if (parentSessionId === void 0 || label === void 0 || label === "") return void 0;
1118
1131
  const key = correlationKey(parentSessionId, label);
1119
1132
  if (!pending.has(key) && pending.size >= 64) {
@@ -1123,8 +1136,7 @@ function installSubagentSeam(ctx, options) {
1123
1136
  pending.delete(key);
1124
1137
  pending.set(key, {
1125
1138
  role,
1126
- at: Date.now(),
1127
- personaNotApplied
1139
+ at
1128
1140
  });
1129
1141
  return key;
1130
1142
  };
@@ -1168,68 +1180,13 @@ function installSubagentSeam(ctx, options) {
1168
1180
  }
1169
1181
  onValue(result);
1170
1182
  };
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) => {
1183
+ const isLocalBackend = (service, providerName) => {
1225
1184
  try {
1226
- return withRolePersona(service, providerName, request, role, surface);
1185
+ if (typeof service.getProvider !== "function") return false;
1186
+ return typeof service.getProvider(providerName)?.prepareContinuable === "function";
1227
1187
  } catch (error) {
1228
1188
  reportDegrade(error);
1229
- return {
1230
- request,
1231
- personaNotApplied: false
1232
- };
1189
+ return false;
1233
1190
  }
1234
1191
  };
1235
1192
  const wrapService = (value) => {
@@ -1241,61 +1198,71 @@ function installSubagentSeam(ctx, options) {
1241
1198
  return value;
1242
1199
  }
1243
1200
  const service = value;
1244
- if (typeof service.start !== "function") {
1201
+ if (typeof service.startActivation !== "function") {
1245
1202
  if (!unreadableServiceLogged) {
1246
1203
  unreadableServiceLogged = true;
1247
- debug(`llm-fallbacks: subagent role seam skipped — '${SUBAGENT_SEAM_SERVICE}' is not a start-capable runtime`);
1204
+ debug(`llm-fallbacks: subagent role seam skipped — '${SUBAGENT_SEAM_SERVICE}' is not an activation-capable runtime`);
1248
1205
  }
1249
1206
  return value;
1250
1207
  }
1251
1208
  const cached = wrappers.get(value);
1252
1209
  if (cached !== void 0) return cached;
1253
1210
  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");
1211
+ wrapper.startActivation = (spec) => {
1212
+ const role = resolveRoleAtSeam(spec?.request) ?? (typeof spec?.childId === "string" ? records.get(spec.childId)?.role : void 0);
1213
+ if (role === void 0) return service.startActivation(spec);
1214
+ const at = Date.now();
1215
+ let effectiveSpec = spec;
1216
+ let reservedId;
1217
+ let reservationPersonaMissed;
1218
+ try {
1219
+ if (isLocalBackend(service, spec.provider)) {
1220
+ reservedId = spec.childId ?? randomUUID();
1221
+ if (reservedId !== spec.childId) effectiveSpec = {
1222
+ ...spec,
1223
+ childId: reservedId
1224
+ };
1225
+ recordChild(reservedId, role, at);
1226
+ }
1227
+ } catch (error) {
1228
+ if (reservedId !== void 0) records.delete(reservedId);
1229
+ effectiveSpec = spec;
1230
+ reservedId = void 0;
1231
+ reportDegrade(error);
1232
+ try {
1233
+ reservationPersonaMissed = personaForRole(options.roles?.() ?? [], role) !== void 0;
1234
+ } catch (personaError) {
1235
+ reportDegrade(personaError);
1236
+ }
1237
+ }
1258
1238
  let pendingKey;
1259
1239
  try {
1260
- pendingKey = beginCorrelation(request, role, persona.personaNotApplied);
1240
+ pendingKey = beginCorrelation(spec, role, at);
1261
1241
  } catch (error) {
1262
1242
  reportDegrade(error);
1263
1243
  }
1264
1244
  let result;
1265
1245
  try {
1266
- result = service.start(name, persona.request);
1246
+ result = service.startActivation(effectiveSpec);
1267
1247
  } catch (error) {
1268
1248
  endCorrelation(pendingKey);
1249
+ if (reservedId !== void 0) records.delete(reservedId);
1269
1250
  throw error;
1270
1251
  }
1271
1252
  try {
1272
- observeStartResult(result, (childSessionId) => {
1253
+ observeStartResult(result, (childId) => {
1254
+ endCorrelation(pendingKey);
1255
+ recordChild(childId, role, at, reservationPersonaMissed);
1256
+ }, () => {}, () => {
1273
1257
  endCorrelation(pendingKey);
1274
- recordChild(childSessionId, role, Date.now(), persona.personaNotApplied);
1275
- }, () => {}, () => endCorrelation(pendingKey));
1258
+ if (reservedId !== void 0) records.delete(reservedId);
1259
+ });
1276
1260
  } catch (error) {
1277
1261
  endCorrelation(pendingKey);
1278
1262
  reportDegrade(error);
1279
1263
  }
1280
1264
  return result;
1281
1265
  };
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
1266
  wrappers.set(value, wrapper);
1300
1267
  return wrapper;
1301
1268
  };
@@ -1322,7 +1289,7 @@ function installSubagentSeam(ctx, options) {
1322
1289
  const claim = pending.get(key);
1323
1290
  if (claim === void 0) return;
1324
1291
  pending.delete(key);
1325
- recordChild(childSessionId, claim.role, claim.at, claim.personaNotApplied);
1292
+ recordChild(childSessionId, claim.role, claim.at);
1326
1293
  } catch (error) {
1327
1294
  reportDegrade(error);
1328
1295
  }
@@ -1347,6 +1314,96 @@ function installSubagentSeam(ctx, options) {
1347
1314
  return seam;
1348
1315
  }
1349
1316
  //#endregion
1317
+ //#region .build/host/role-persona.js
1318
+ function errorMessage(error) {
1319
+ return error instanceof Error ? error.message : String(error);
1320
+ }
1321
+ const rolePersonaStates = new WeakMap();
1322
+ function rolePersonaStateFor(root) {
1323
+ const existing = rolePersonaStates.get(root);
1324
+ if (existing !== void 0) return existing;
1325
+ const created = new Map();
1326
+ rolePersonaStates.set(root, created);
1327
+ return created;
1328
+ }
1329
+ function installRolePersona(ctx, options) {
1330
+ const root = ctx.root;
1331
+ const registered = rolePersonaStateFor(root);
1332
+ const owner = {};
1333
+ const fibers = new Map();
1334
+ const debug = (message) => {
1335
+ try {
1336
+ options.debug?.(message);
1337
+ } catch {}
1338
+ };
1339
+ const contain = (agent, record, error) => {
1340
+ debug(`llm-fallbacks: role persona for agent "${agent.id}" (role "${record.role}") not applied — contained, the child runs unchanged: ${errorMessage(error)}`);
1341
+ record.personaNotApplied = true;
1342
+ };
1343
+ const release = (id) => {
1344
+ const fiber = fibers.get(id);
1345
+ if (fiber === void 0) return;
1346
+ fibers.delete(id);
1347
+ fiber.dispose().catch((error) => {
1348
+ debug(`llm-fallbacks: role persona section for agent "${id}" not withdrawn (contained): ${errorMessage(error)}`);
1349
+ });
1350
+ };
1351
+ const installPersona = async (agent) => {
1352
+ const record = options.records.get(agent.id);
1353
+ if (record === void 0) return;
1354
+ if (registered.has(agent.id)) return;
1355
+ let persona;
1356
+ try {
1357
+ persona = personaForRole(options.roles(), record.role);
1358
+ } catch (error) {
1359
+ debug(`llm-fallbacks: role persona for agent "${agent.id}" not resolved — contained, the child runs unchanged: ${errorMessage(error)}`);
1360
+ return;
1361
+ }
1362
+ if (persona === void 0) return;
1363
+ const text = persona;
1364
+ registered.set(agent.id, owner);
1365
+ try {
1366
+ const fiber = agent.ctx.inject(["systemPrompt"], (scope) => {
1367
+ try {
1368
+ scope.systemPrompt.section({
1369
+ name: PERSONA_PREFIX_SECTION,
1370
+ order: scope.systemPrompt.getSectionOrder("DEPLOYMENT_PERSONA_PREFIX"),
1371
+ text
1372
+ });
1373
+ } catch (error) {
1374
+ if (error instanceof Error && error.message === `prompt section "${PERSONA_PREFIX_SECTION}" is already registered in this scope`) {
1375
+ debug(`llm-fallbacks: role persona for agent "${agent.id}" not installed — a section named "${PERSONA_PREFIX_SECTION}" already exists in its scope: ${errorMessage(error)}`);
1376
+ return;
1377
+ }
1378
+ contain(agent, record, error);
1379
+ }
1380
+ });
1381
+ fibers.set(agent.id, fiber);
1382
+ await fiber;
1383
+ } catch (error) {
1384
+ release(agent.id);
1385
+ contain(agent, record, error);
1386
+ }
1387
+ };
1388
+ const disposers = [];
1389
+ disposers.push(ctx.on("agent/created", async ({ agent }) => {
1390
+ await installPersona(agent);
1391
+ }));
1392
+ disposers.push(ctx.on("agent/disposed", ({ agent }) => {
1393
+ release(agent.id);
1394
+ registered.delete(agent.id);
1395
+ }));
1396
+ const agents = ctx.get("agents");
1397
+ if (typeof agents?.list === "function") for (const agent of agents.list()) installPersona(agent);
1398
+ const dispose = () => {
1399
+ for (const off of disposers.splice(0)) off();
1400
+ for (const id of [...fibers.keys()]) release(id);
1401
+ for (const [id, holder] of [...registered]) if (holder === owner) registered.delete(id);
1402
+ };
1403
+ ctx.effect(() => dispose, "llm-fallbacks: role persona listener");
1404
+ return { dispose };
1405
+ }
1406
+ //#endregion
1350
1407
  //#region .build/host/cooldown.js
1351
1408
  var CooldownStore = class {
1352
1409
  entries = new Map();
@@ -3088,7 +3145,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
3088
3145
  detectLegacyKeys,
3089
3146
  declareSeeds: (declarations, options) => seeds.declare(declarations, seedsIo, options?.set === void 0 ? void 0 : { set: options.set }),
3090
3147
  getEffectiveRoles: () => seeds.effectiveRoles(seedsIo),
3091
- revertSeededPersona: (id) => seeds.revert(id, seedsIo)
3148
+ revertSeededPersona: (id) => seeds.revert(id, seedsIo),
3149
+ getAgentRole: (agentId) => subagentSeamOf(ctx)?.records.get(agentId)?.role ?? null
3092
3150
  });
3093
3151
  serviceOwned = true;
3094
3152
  } catch (error) {
@@ -3151,6 +3209,11 @@ function apply(ctx, config = defaultFallbacksConfig) {
3151
3209
  debug: (message) => logger.debug(message)
3152
3210
  });
3153
3211
  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)");
3212
+ installRolePersona(ctx, {
3213
+ records: subagentSeam.records,
3214
+ roles: () => source().roles.list,
3215
+ debug: (message) => logger.debug(message)
3216
+ });
3154
3217
  try {
3155
3218
  new FallbacksConfigGateway(ctx, bridge, seeds, () => {
3156
3219
  try {
@@ -3345,6 +3408,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
3345
3408
  const role = await resolveRoleAtDispatch(agent, config.roles.rules, roleIds, {
3346
3409
  automatchEnabled: config.roleAutoMatch ?? true,
3347
3410
  automatch: (agent) => pickRoleByLlm(ctx, config.roles, agent, { warn: logger.warn }),
3411
+ dispatchRole: subagentSeam.records.get(agent.id)?.role,
3348
3412
  warn: logger.warn,
3349
3413
  servedRoute: servedRouteFor(agent, config)
3350
3414
  });
@@ -3513,4 +3577,4 @@ function apply(ctx, config = defaultFallbacksConfig) {
3513
3577
  });
3514
3578
  }
3515
3579
  //#endregion
3516
- export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, SelectorError, annotateCandidates, apply, blockedAttempts, chainHeads, countRetryEvents, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, presetRoles, provide, resolveCandidate, resolveChain, resolveChainViews, resolveRole, selectCandidates, stateStore, validateFallbacksConfig };
3580
+ export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, ROLE_PROJECTION_KEY, SelectorError, annotateCandidates, apply, blockedAttempts, chainHeads, countRetryEvents, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, presetRoles, provide, readRoleProjectionValue, resolveCandidate, resolveChain, resolveChainViews, resolveRole, selectCandidates, stateStore, validateFallbacksConfig };
@@ -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,17 +1,23 @@
1
1
  /**
2
- * Dispatch-time three-stage role resolution (plan fallbacks-role-automatch
3
- * Task 2).
2
+ * Dispatch-time role resolution (plan fallbacks-role-automatch Task 2; the
3
+ * dispatch-role stage added by plan dsh-148-agent-team-role-delivery Task 1).
4
4
  *
5
- * Precedence (first hit wins):
5
+ * Precedence (first hit wins) across four ordered stages:
6
6
  * 1. explicit — `session.header.agentPreset` trimmed matches a declared role
7
7
  * id → that role's DECLARED RAW id (via the `roleIds` trimmed-id map, the
8
8
  * same canonicalization the rules path uses); the reserved `'inherit'`
9
9
  * and undeclared presets are NEVER dispatchable explicit roles and fall
10
- * through to rules;
11
- * 2. rules — the existing `resolveRole` passthrough, unchanged (a declared
10
+ * through to the next stage;
11
+ * 2. dispatch — `opts.dispatchRole`: the role the dispatch seam resolved from
12
+ * the Assignment/teammate prompt (`**Execute as**: <id>`, the single
13
+ * parser in `src/subagents-seam.ts`), canonicalized by the same trim +
14
+ * `roleIds` lookup. An absent, empty, `'inherit'` or no-longer-declared
15
+ * reference falls through silently — a role removed after the dispatch is
16
+ * not an error;
17
+ * 3. rules — the existing `resolveRole` passthrough, unchanged (a declared
12
18
  * role wins; no-rule-match / an explicit rule targeting `'inherit'`
13
19
  * resolve to `'inherit'`);
14
- * 3. auto-match — only when stage 2 resolved to `'inherit'` AND
20
+ * 4. auto-match — only when stage 3 resolved to `'inherit'` AND
15
21
  * `opts.automatchEnabled`: call `opts.automatch` with the agent. The hook
16
22
  * (Task 3 supplies the bounded LLM caller) MUST return a declared raw id
17
23
  * or `null`; the resolver defensively validates the return against
@@ -35,10 +41,16 @@ import type { Selector } from './selectors.ts';
35
41
  export type { FallbacksRoleRule } from './config.ts';
36
42
  /** Options for {@link resolveRoleAtDispatch}. */
37
43
  export interface ResolveRoleAtDispatchOptions {
44
+ /**
45
+ * Dispatch-declared role: the DECLARED RAW id the seam resolved from the
46
+ * Assignment/teammate prompt (`**Execute as**: <id>`). Validated against the
47
+ * live `roleIds` map; absent, `inherit`, or no-longer-declared → skipped.
48
+ */
49
+ dispatchRole?: string;
38
50
  /**
39
51
  * Whether the auto-match stage may run (the `roleAutoMatch` config switch).
40
- * When `false`, stage 3 never runs and the outcome is identical to the
41
- * rules stage alone.
52
+ * When `false`, stage 4 never runs and the outcome is identical to the
53
+ * explicit/dispatch/rules stages alone.
42
54
  */
43
55
  automatchEnabled: boolean;
44
56
  /**
@@ -64,10 +76,11 @@ export interface ResolveRoleAtDispatchOptions {
64
76
  };
65
77
  }
66
78
  /**
67
- * Resolve the dispatch-time role for an agent across three ordered stages
68
- * (explicit → rules → auto-match hook). Never throws on its own inputs; the
69
- * hook's own rejection is deliberately NOT caught here (the Task-3/4 wiring
70
- * bounds the LLM call so it never throws out of the request path).
79
+ * Resolve the dispatch-time role for an agent across four ordered stages
80
+ * (explicit → dispatch → rules → auto-match hook). Never throws on its own
81
+ * inputs; the hook's own rejection is deliberately NOT caught here (the
82
+ * Task-3/4 wiring bounds the LLM call so it never throws out of the request
83
+ * path).
71
84
  *
72
85
  * `roleIds` is the canonical trimmed-id map (`trimmed id → declared raw id`,
73
86
  * built in `src/index.ts` apply()): every stage canonicalizes its reference
package/dist/roles.d.ts CHANGED
@@ -3,10 +3,12 @@
3
3
  * Task 1).
4
4
  *
5
5
  * This module is the RULES stage. Dispatch-time role resolution
6
- * (plan fallbacks-role-automatch) is the three-stage resolver in
7
- * `role-resolution.ts` — explicit (`session.header.agentPreset`) → this
8
- * rules stage → an optional auto-match hook; `AgentLike` carries the additive
9
- * `session.header.agentPreset?` carrier for that explicit stage.
6
+ * (plan fallbacks-role-automatch; plan dsh-148 added the dispatch stage) is
7
+ * the four-stage resolver in `role-resolution.ts` — explicit
8
+ * (`session.header.agentPreset`) → dispatch-declared role (the seam record
9
+ * resolved from `**Execute as**`) → this rules stage → an optional auto-match
10
+ * hook; `AgentLike` carries the additive `session.header.agentPreset?`
11
+ * carrier for that explicit stage.
10
12
  *
11
13
  * Precedence (first hit wins):
12
14
  * 1. the first `rules` entry whose specified provider/model patterns match