@sema-agent/core 6.0.0 → 7.0.1

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/agents/launch-receipt-contract.d.ts +34 -0
  3. package/dist/agents/launch-receipt-contract.js +5 -0
  4. package/dist/agents/subagent.d.ts +134 -2
  5. package/dist/agents/subagent.js +132 -31
  6. package/dist/core/file-history-store.js +24 -2
  7. package/dist/core/governance-codes.d.ts +11 -2
  8. package/dist/core/governance-codes.js +1 -0
  9. package/dist/core/permission-rule-consent.d.ts +42 -2
  10. package/dist/core/permission-rule-consent.js +93 -11
  11. package/dist/core/permission-rule-model.d.ts +51 -9
  12. package/dist/core/permission-rule-model.js +4 -2
  13. package/dist/core/permission-rule-session.d.ts +124 -0
  14. package/dist/core/permission-rule-session.js +121 -0
  15. package/dist/core/permission-rule-store.d.ts +65 -2
  16. package/dist/core/permission-rule-store.js +60 -6
  17. package/dist/core/permission-rule-sync.d.ts +9 -0
  18. package/dist/core/permission-rule-sync.js +37 -8
  19. package/dist/core/roles.js +1 -1
  20. package/dist/core/runner/prepare-task.js +35 -2
  21. package/dist/core/runner/runtask.js +2 -0
  22. package/dist/core/store-contracts/permission-rule-sync-contract.js +15 -1
  23. package/dist/core/task-notification.d.ts +20 -0
  24. package/dist/core/trace.d.ts +7 -2
  25. package/dist/core/types.d.ts +85 -1
  26. package/dist/core/wiring-manifest.d.ts +18 -1
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.js +2 -1
  29. package/dist/orchestration/run-workflow-tool.js +2 -2
  30. package/dist/orchestration/workflow.js +18 -10
  31. package/dist/stores/file/permission-rule-store.d.ts +11 -0
  32. package/dist/stores/file/permission-rule-store.js +22 -9
  33. package/package.json +1 -1
  34. package/test/export-surface.snapshot.json +15 -1
@@ -37,10 +37,27 @@ function delegatedCostField(stats) {
37
37
  const total = rollupDelegatedCost(stats, stats.nested);
38
38
  return total !== undefined ? { costMicroUsd: total } : {};
39
39
  }
40
+ export function delegatedGranularityFields(stats, elapsedMs) {
41
+ return { ...(stats.toolCalls !== undefined ? { toolUses: stats.toolCalls } : {}), durationMs: elapsedMs };
42
+ }
43
+ function providerFaultField(child) {
44
+ if (child.apiFailure === undefined)
45
+ return {};
46
+ return {
47
+ apiFailure: {
48
+ ...(typeof child.apiFailure.status === "number" ? { status: child.apiFailure.status } : {}),
49
+ ...(typeof child.apiFailure.requestId === "string" ? { requestId: child.apiFailure.requestId } : {}),
50
+ },
51
+ };
52
+ }
53
+ export function delegatedGranularityFieldsWire(stats, elapsedMs) {
54
+ return { ...(stats.toolCalls !== undefined ? { tool_uses: stats.toolCalls } : {}), duration_ms: elapsedMs };
55
+ }
40
56
  export { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
41
57
  import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getOrCreateSessionRetainLedger, ensureSessionReapHook, createResumePrompt, } from "./retain-ledger.js";
42
58
  import { createPeerInboundChainRef, createPeerSelfRef } from "./peer-admission.js";
43
59
  import { recordRosterSpawn } from "./roster-store.js";
60
+ import { LAUNCH_RECEIPT_OWN_WORDS_CLAUSE, launchReceiptNoQuoteClause } from "./launch-receipt-contract.js";
44
61
  import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, ObserverResumeStateError, ObserverStoppedByUserError, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
45
62
  import { SubagentStepRecorder } from "./subagent-steps.js";
46
63
  const BG_AGENT_RESULT_MAX = 4_000;
@@ -326,6 +343,7 @@ function resumabilityClaim(f) {
326
343
  const BG_AGENT_COLLATERAL_REAP_REASON = "its parent run ended";
327
344
  const RESERVED_AGENT_NAMES = new Set([OUTPUT_TOOL_NAME, TOOL_SEARCH_NAME, OFFLOAD_TOOL_NAME, REPORT_BLOCKED_TOOL_NAME, DEFAULT_SUBAGENT_TOOL_NAME]);
328
345
  export const REPORT_FIELD_MAX = 300;
346
+ const ROSTER_ECHO_MAX = 400;
329
347
  function configError(message, code) {
330
348
  const e = new Error(message);
331
349
  e.code = code;
@@ -364,6 +382,7 @@ export function completedAgentCard(child, extras) {
364
382
  ...(child.blockedReason !== undefined ? { blockedReason: child.blockedReason } : {}),
365
383
  ...(child.errorMessage !== undefined ? { errorMessage: child.errorMessage } : {}),
366
384
  ...(child.errorCode !== undefined ? { errorCode: child.errorCode } : {}),
385
+ ...providerFaultField(child),
367
386
  ...(child.retryAfterMs !== undefined ? { retryAfterMs: child.retryAfterMs } : {}),
368
387
  ...(child.degraded !== undefined ? { degraded: child.degraded } : {}),
369
388
  ...(extras.handbackWarning !== undefined ? { handbackWarning: extras.handbackWarning } : {}),
@@ -711,9 +730,10 @@ export function createSubagentResume(deps) {
711
730
  const completionIdRevive = deps.registry?.getCompletionId(deps.taskId ?? "");
712
731
  const reviveName = deps.rowDescription ?? `sub-agent ${marker}`;
713
732
  const failReasonRevive = status === "failed" ? child.errorMessage : undefined;
733
+ const reviveElapsedMs = Date.now() - reviveStartedAt;
714
734
  const reviveTerminalSummary = failReasonRevive !== undefined
715
- ? `Agent "${reviveName}" (resumed) failed: ${failReasonRevive}${ccElapsedTag(Date.now() - reviveStartedAt)}`.slice(0, 300) + errorKindClause(errClassRevive)
716
- : `Agent "${reviveName}" (resumed) ${status === "killed" ? "stopped" : child.status === "completed" ? "finished" : String(child.status)}${ccElapsedTag(Date.now() - reviveStartedAt)}`;
735
+ ? `Agent "${reviveName}" (resumed) failed: ${failReasonRevive}${ccElapsedTag(reviveElapsedMs)}`.slice(0, 300) + errorKindClause(errClassRevive)
736
+ : `Agent "${reviveName}" (resumed) ${status === "killed" ? "stopped" : child.status === "completed" ? "finished" : String(child.status)}${ccElapsedTag(reviveElapsedMs)}`;
717
737
  const reviveDurableProbe = deps.registry !== undefined && deps.taskId !== undefined ? deps.registry.durableAgentRowProbe(deps.taskId) : undefined;
718
738
  const reviveDurableRow = reviveDurableProbe !== undefined ? await reviveDurableProbe().catch(() => false) : false;
719
739
  const resumableRevive = resumabilityClaim({
@@ -739,7 +759,7 @@ export function createSubagentResume(deps) {
739
759
  summary: reviveTerminalSummary,
740
760
  ...resumeResidual(),
741
761
  resumable: resumableRevive,
742
- usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
762
+ usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFields(child.stats, reviveElapsedMs) },
743
763
  ...(completionIdRevive !== undefined ? { completionId: completionIdRevive } : {}),
744
764
  });
745
765
  const resumeFrame = {
@@ -753,11 +773,12 @@ export function createSubagentResume(deps) {
753
773
  summary: reviveTerminalSummary,
754
774
  ...(failReasonRevive !== undefined ? { error: failReasonRevive.slice(0, REPORT_FIELD_MAX) } : {}),
755
775
  ...(status === "failed" && errCodeRevive !== undefined ? { errorCode: errCodeRevive } : {}),
776
+ ...providerFaultField(child),
756
777
  ...(resumeHandbackResult ? { result: notifyResultField(resumeHandbackResult) } : {}),
757
778
  ...(status === "killed" && child.result ? { partial: true } : {}),
758
779
  ...resumeResidual(),
759
780
  resumable: resumableRevive,
760
- usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
781
+ usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFieldsWire(child.stats, reviveElapsedMs) },
761
782
  ...(completionIdRevive !== undefined ? { completionId: completionIdRevive } : {}),
762
783
  };
763
784
  const gateOwner = ownsTerminalFacesRevive && status === "completed" && deps.notify !== undefined && deps.registry !== undefined && ledger.get(deps.parentToolCallId) === entry
@@ -1206,7 +1227,7 @@ export function forkWorktreeTranslationNote(parentCwd, worktreeDir) {
1206
1227
  export function asyncLaunchedReceipt(p) {
1207
1228
  const noteLines = (p.notes ?? []).filter((n) => n !== undefined && n !== "");
1208
1229
  const oneShotBlocked = p.notify && p.oneShot === true;
1209
- return (`Async agent launched successfully. (This tool result is internal metadata — never quote or paste any part of it, including the task_id below, into a user-facing reply.)\n` +
1230
+ return (`Async agent launched successfully. (${launchReceiptNoQuoteClause(", including the task_id below,")})\n` +
1210
1231
  `task_id: ${p.taskId} (internal ID - do not mention to user. Use SendMessage with to: '${p.taskId}', summary: '<5-10 word recap>' to continue this agent.)\n` +
1211
1232
  (oneShotBlocked
1212
1233
  ? `${p.workingLine} This is a ONE-SHOT submission — there is no later turn for a background notification to land in, so do NOT end your turn expecting one. Actively wait instead: TaskOutput(task_id: "${p.taskId}", block: true). If it is still running after the wait, wait again (bounded) rather than ending the turn, or write out your best available answer now if you are near your own time budget.\n`
@@ -1215,10 +1236,18 @@ export function asyncLaunchedReceipt(p) {
1215
1236
  : `${p.workingLine} Its result is NOT pushed automatically — retrieve progress and results with TaskOutput(task_id) where mounted.\n`) +
1216
1237
  noteLines.map((n) => `${n}\n`).join("") +
1217
1238
  (oneShotBlocked
1218
- ? `In your own words, briefly tell the user what you launched — do not echo this tool result. Do not assume a later message will deliver the result: this submission ends after this turn, so retrieve it now with the blocking TaskOutput wait above before writing your final answer.`
1239
+ ? `${LAUNCH_RECEIPT_OWN_WORDS_CLAUSE} Do not assume a later message will deliver the result: this submission ends after this turn, so retrieve it now with the blocking TaskOutput wait above before writing your final answer.`
1219
1240
  : p.notify
1220
- ? `In your own words, briefly tell the user what you launched — do not echo this tool result. Agent results will arrive in a subsequent message. If the user asks for progress, say the agent is still running.`
1221
- : `In your own words, briefly tell the user what you launched — do not echo this tool result. The result will NOT arrive on its own — retrieve it with TaskOutput(task_id) where mounted before relying on it.`));
1241
+ ? `${LAUNCH_RECEIPT_OWN_WORDS_CLAUSE} Agent results will arrive in a subsequent message. If the user asks for progress, say the agent is still running.`
1242
+ : `${LAUNCH_RECEIPT_OWN_WORDS_CLAUSE} The result will NOT arrive on its own — retrieve it with TaskOutput(task_id) where mounted before relying on it.`));
1243
+ }
1244
+ function runnerModelCatalog(runner) {
1245
+ try {
1246
+ return runner?.agentCatalog?.models;
1247
+ }
1248
+ catch {
1249
+ return undefined;
1250
+ }
1222
1251
  }
1223
1252
  export function createSubagentTool(opts) {
1224
1253
  const catalog = opts.runner.agentCatalog;
@@ -1227,8 +1256,7 @@ export function createSubagentTool(opts) {
1227
1256
  if (opts.builtinAgents === undefined && catalog?.builtinAgents !== undefined) {
1228
1257
  opts = { ...opts, builtinAgents: catalog.builtinAgents };
1229
1258
  }
1230
- if (opts.models === undefined && catalog?.models)
1231
- opts = { ...opts, models: catalog.models };
1259
+ const assemblyModels = opts.models ?? catalog?.models;
1232
1260
  if (opts.agents?.length) {
1233
1261
  opts = {
1234
1262
  ...opts,
@@ -1252,7 +1280,7 @@ export function createSubagentTool(opts) {
1252
1280
  }
1253
1281
  if (typeof a.model === "string") {
1254
1282
  try {
1255
- resolveModel(a.model, opts.models);
1283
+ resolveModel(a.model, assemblyModels);
1256
1284
  }
1257
1285
  catch {
1258
1286
  throw configError(`createSubagentTool: agent "${a.name}" references unknown model "${a.model}" (pass \`models\` to resolve string refs).`, "config.agent.unknown_model");
@@ -1308,7 +1336,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1308
1336
  ...(forkOffered ? [FORK_SUBAGENT_TYPE] : []),
1309
1337
  ...(generalPurposeShadowed ? [] : [GENERAL_PURPOSE_SUBAGENT_TYPE]),
1310
1338
  ];
1311
- const rosterNames = opts.models ? Object.keys(opts.models) : undefined;
1339
+ const activeModels = () => opts.models ?? runnerModelCatalog(opts.runner);
1340
+ const mountModels = activeModels();
1341
+ const rosterNames = mountModels ? Object.keys(mountModels) : undefined;
1312
1342
  const agentListing = [
1313
1343
  ...(generalPurposeShadowed
1314
1344
  ? []
@@ -1424,7 +1454,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1424
1454
  })),
1425
1455
  model: Type.Optional(Type.String({
1426
1456
  description: rosterNames !== undefined && rosterNames.length > 0
1427
- ? `Optional model override for this agent. Takes precedence over the agent definition's model. If omitted, uses the agent definition's model, or inherits from the parent. Ignored for subagent_type: "${FORK_SUBAGENT_TYPE}" — forks always inherit the parent model.`
1457
+ ? `Optional model override for this agent. Takes precedence over the agent definition's model. If omitted, uses the agent definition's model, or inherits from the parent. A value outside this deployment's model catalog is REFUSED — the call does not fall back silently. Ignored for subagent_type: "${FORK_SUBAGENT_TYPE}" — forks always inherit the parent model.`
1428
1458
  : `Optional model override request. No model catalog is configured here, so a requested name cannot be resolved — the agent runs its definition's model (or inherits from the parent) and the result notes the override was not applied. Ignored for subagent_type: "${FORK_SUBAGENT_TYPE}".`,
1429
1459
  })),
1430
1460
  isolation: Type.Optional(Type.Literal("worktree", {
@@ -1560,23 +1590,54 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1560
1590
  };
1561
1591
  }
1562
1592
  }
1593
+ if (typeof a.model === "string" && a.model.trim() === "" && !wantsFork && reviveClaim === undefined) {
1594
+ return {
1595
+ isError: true,
1596
+ content: "Sub-agent not started: `model` was empty. Name a model from this deployment's catalog, or omit the parameter to run the agent type's own model.",
1597
+ details: { error: "model.empty" },
1598
+ };
1599
+ }
1563
1600
  const requestedModel = typeof a.model === "string" && a.model.trim() !== "" ? a.model.trim() : undefined;
1564
1601
  let perCallModel;
1565
1602
  let modelNote;
1566
- if (requestedModel !== undefined) {
1603
+ const judgedModels = activeModels();
1604
+ const rosterKeys = judgedModels !== undefined ? Object.keys(judgedModels) : [];
1605
+ if (typeof a.model === "string" && requestedModel === undefined) {
1606
+ modelNote = wantsFork
1607
+ ? `note: model "" was ignored — a fork always runs on the caller's model.`
1608
+ : `note: the durable record's model was blank and was NOT applied — the revived agent ran on its default (inherited) model.`;
1609
+ }
1610
+ else if (requestedModel !== undefined) {
1567
1611
  if (wantsFork) {
1568
1612
  modelNote = `note: model "${requestedModel}" was ignored — a fork always runs on the caller's model.`;
1569
1613
  }
1570
- else if (!opts.models) {
1614
+ else if (rosterKeys.length === 0) {
1571
1615
  modelNote = `note: model "${requestedModel}" was NOT applied — this delegation tool has no model roster configured; the sub-agent ran on its default (inherited) model.`;
1572
1616
  }
1573
1617
  else {
1574
1618
  try {
1575
- resolveModel(requestedModel, opts.models);
1576
- perCallModel = requestedModel;
1619
+ perCallModel = resolveModel(requestedModel, judgedModels);
1577
1620
  }
1578
1621
  catch {
1579
- modelNote = `note: model "${requestedModel}" was NOT applied — unknown model (roster: ${Object.keys(opts.models).join(", ")}); the sub-agent ran on its default (inherited) model.`;
1622
+ if (reviveClaim !== undefined) {
1623
+ modelNote = `note: recorded model "${inlineUntrusted(requestedModel, 80)}" is not on the current model roster and was NOT applied — the revived agent ran on its default (inherited) model.`;
1624
+ }
1625
+ else {
1626
+ const shown = [];
1627
+ let budget = ROSTER_ECHO_MAX;
1628
+ for (const k of rosterKeys) {
1629
+ if (budget - k.length < 0)
1630
+ break;
1631
+ shown.push(k);
1632
+ budget -= k.length + 2;
1633
+ }
1634
+ const rosterEcho = shown.length === rosterKeys.length ? shown.join(", ") : `${shown.join(", ")}, … (${rosterKeys.length - shown.length} more)`;
1635
+ return {
1636
+ isError: true,
1637
+ content: `Sub-agent not started: unknown model "${inlineUntrusted(requestedModel, 80)}". Available model values: ${inlineUntrusted(rosterEcho, ROSTER_ECHO_MAX + 32)}. Omit \`model\` to run the agent type's own model, or inherit the caller's.`,
1638
+ details: { error: "model.unknown" },
1639
+ };
1640
+ }
1580
1641
  }
1581
1642
  }
1582
1643
  }
@@ -1690,9 +1751,39 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1690
1751
  const childTools = def
1691
1752
  ? resolveToolSubset(childPool, def.allowTools, def.denyTools)
1692
1753
  : resolveToolSubset(childPool, opts.allowTools, opts.denyTools);
1693
- const childModel = wantsFork
1754
+ let childModel = wantsFork
1694
1755
  ? ctx.model ?? opts.model
1695
1756
  : perCallModel ?? (def ? (def.model ?? ctx.model ?? opts.model) : opts.model);
1757
+ if (reviveClaim !== undefined && typeof childModel === "string") {
1758
+ const resolvable = (ref) => {
1759
+ try {
1760
+ resolveModel(ref, runnerModelCatalog(opts.runner));
1761
+ return true;
1762
+ }
1763
+ catch {
1764
+ return false;
1765
+ }
1766
+ };
1767
+ if (!resolvable(childModel)) {
1768
+ const dead = childModel;
1769
+ const rest = def !== undefined ? [ctx.model, opts.model] : [opts.model];
1770
+ childModel = rest.find((c) => c !== undefined && (typeof c !== "string" || resolvable(c)));
1771
+ if (modelNote === undefined) {
1772
+ modelNote = `note: model "${inlineUntrusted(dead, 80)}" from this agent type's configuration is not on the current model roster and was NOT applied — the revived agent ran on its default (inherited) model.`;
1773
+ }
1774
+ }
1775
+ }
1776
+ const spawnModel = typeof childModel === "string"
1777
+ ? (() => {
1778
+ try {
1779
+ return resolveModel(childModel, judgedModels).id;
1780
+ }
1781
+ catch {
1782
+ return resolveModelDisplayLabel(childModel);
1783
+ }
1784
+ })()
1785
+ : childModel?.id;
1786
+ const rowModelKey = perCallModel !== undefined ? requestedModel : typeof childModel === "string" ? childModel : childModel?.id;
1696
1787
  const childSystemPrompt = def?.systemPrompt ?? opts.systemPrompt;
1697
1788
  const childDefaultPersona = childSystemPrompt === undefined && !wantsFork ? SUBAGENT_PROMPT : undefined;
1698
1789
  const defMaxTurns = typeof def?.maxTurns === "number" && Number.isFinite(def.maxTurns) && def.maxTurns > 0 ? def.maxTurns : undefined;
@@ -2342,7 +2433,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2342
2433
  ...(ctx.taskId !== undefined && ctx.taskId !== ctx.sessionId ? { parentTaskId: ctx.taskId } : {}),
2343
2434
  ...(ctx.sessionId !== undefined ? { parentSessionId: ctx.sessionId } : {}),
2344
2435
  ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}),
2345
- ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}),
2436
+ ...(rowModelKey !== undefined ? { model: rowModelKey } : {}),
2346
2437
  deliveryChannel: "attaching",
2347
2438
  toolUseId: ctx.toolCallId,
2348
2439
  ...(bg.agentStore !== undefined ? { store: bg.agentStore } : {}),
@@ -2403,7 +2494,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2403
2494
  }, { priority: "next" }), "subagent.reapTerminalNotify");
2404
2495
  });
2405
2496
  if (agentName !== undefined) {
2406
- recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(modelFallback !== undefined ? { modelFallback } : {}), ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
2497
+ recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...(rowModelKey !== undefined ? { model: rowModelKey } : {}), ...(modelFallback !== undefined ? { modelFallback } : {}), ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
2407
2498
  }
2408
2499
  const bgSink = ctx.onBackgroundChildEvent;
2409
2500
  const sinkEmit = (event) => {
@@ -2422,6 +2513,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2422
2513
  ...(bgScope !== undefined ? { scope: bgScope } : {}),
2423
2514
  description: shortDesc,
2424
2515
  agentType: spawnAgentType,
2516
+ ...(spawnModel !== undefined ? { model: spawnModel } : {}),
2425
2517
  ...(bgForkCycleSeq !== undefined ? { seq: bgForkCycleSeq } : {}),
2426
2518
  ...(agentName !== undefined ? { name: agentName } : {}),
2427
2519
  sessionId: forkedId,
@@ -2451,6 +2543,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2451
2543
  parentToolCallId: ctx.toolCallId,
2452
2544
  progressTaskId: forkedId,
2453
2545
  agentType: spawnAgentType,
2546
+ ...(spawnModel !== undefined ? { model: spawnModel } : {}),
2454
2547
  ...(bgForkCycleSeq !== undefined ? { seq: bgForkCycleSeq } : {}),
2455
2548
  ...(currentAction !== undefined ? { currentAction } : {}),
2456
2549
  ...(currentTool !== undefined ? { currentTool } : {}),
@@ -2485,6 +2578,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2485
2578
  progressTaskId: e.taskId,
2486
2579
  ...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
2487
2580
  agentType: spawnAgentType,
2581
+ ...(spawnModel !== undefined ? { model: spawnModel } : {}),
2488
2582
  ...(e.name !== undefined ? { name: e.name } : {}),
2489
2583
  ...(currentAction !== undefined ? { currentAction } : {}),
2490
2584
  ...(currentTool !== undefined ? { currentTool } : {}),
@@ -2562,9 +2656,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2562
2656
  const resumableFork = false;
2563
2657
  const forkTranscriptId = child.sessionId ?? forkedId;
2564
2658
  const failReasonFork = settledBg === "failed" ? child.errorMessage : undefined;
2659
+ const forkElapsedMs = Date.now() - forkBgStartedAt;
2565
2660
  const forkTerminalSummary = failReasonFork !== undefined
2566
- ? `Agent "${shortDesc}" failed: ${failReasonFork}${ccElapsedTag(Date.now() - forkBgStartedAt)}`.slice(0, 300) + errorKindClause(errClassFork)
2567
- : `${ccCompletionText(shortDesc, settledBg, String(child.status), Date.now() - forkBgStartedAt)}`;
2661
+ ? `Agent "${shortDesc}" failed: ${failReasonFork}${ccElapsedTag(forkElapsedMs)}`.slice(0, 300) + errorKindClause(errClassFork)
2662
+ : `${ccCompletionText(shortDesc, settledBg, String(child.status), forkElapsedMs)}`;
2568
2663
  const ownsTerminalFacesFork = bg.registry.claimAgentTerminalNotify(taskId);
2569
2664
  if (ownsTerminalFacesFork)
2570
2665
  sinkEmit({
@@ -2580,7 +2675,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2580
2675
  summary: forkTerminalSummary,
2581
2676
  ...residualFork,
2582
2677
  resumable: resumableFork,
2583
- usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
2678
+ usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFields(child.stats, forkElapsedMs) },
2584
2679
  ...(completionIdFork !== undefined ? { completionId: completionIdFork } : {}),
2585
2680
  });
2586
2681
  try {
@@ -2594,12 +2689,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2594
2689
  summary: forkTerminalSummary,
2595
2690
  ...(failReasonFork !== undefined ? { error: failReasonFork.slice(0, REPORT_FIELD_MAX) } : {}),
2596
2691
  ...(settledBg === "failed" && errCodeFork !== undefined ? { errorCode: errCodeFork } : {}),
2692
+ ...providerFaultField(child),
2597
2693
  ...(child.sessionId ? { sessionId: child.sessionId } : {}),
2598
2694
  ...(forkBgResult ? { result: notifyResultField(forkBgResult) } : {}),
2599
2695
  ...(settledBg === "killed" && child.result ? { partial: true } : {}),
2600
2696
  ...residualFork,
2601
2697
  resumable: resumableFork,
2602
- usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
2698
+ usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFieldsWire(child.stats, forkElapsedMs) },
2603
2699
  ...(completionIdFork !== undefined ? { completionId: completionIdFork } : {}),
2604
2700
  }, { priority: "next" });
2605
2701
  }
@@ -2912,7 +3008,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2912
3008
  ...(ctx.taskId !== undefined && ctx.taskId !== ctx.sessionId ? { parentTaskId: ctx.taskId } : {}),
2913
3009
  ...(ctx.sessionId !== undefined ? { parentSessionId: ctx.sessionId } : {}),
2914
3010
  ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}),
2915
- ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}),
3011
+ ...(rowModelKey !== undefined ? { model: rowModelKey } : {}),
2916
3012
  deliveryChannel: "attaching",
2917
3013
  toolUseId: ctx.toolCallId,
2918
3014
  ...(bg.agentStore !== undefined ? { store: bg.agentStore } : {}),
@@ -2936,7 +3032,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2936
3032
  chargeDelegationEntryHandle(bg.registry, capLedgerKey, bg.agentStore, reviveRow === undefined ? taskId : undefined);
2937
3033
  childInternals.peerSelfRef?.addAxis("h", taskId);
2938
3034
  if (agentName !== undefined) {
2939
- recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(reviveRow !== undefined ? ((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId } : {}) : (ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: reviveRow?.spawnedAt ?? Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
3035
+ recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...(reviveRow !== undefined ? (reviveRow.model !== undefined ? { model: reviveRow.model } : {}) : rowModelKey !== undefined ? { model: rowModelKey } : {}), ...(modelFallback !== undefined ? { modelFallback } : {}), ...(reviveRow !== undefined ? ((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId } : {}) : (ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: reviveRow?.spawnedAt ?? Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
2940
3036
  }
2941
3037
  const bgSink = ctx.onBackgroundChildEvent;
2942
3038
  const sinkEmit = (event) => {
@@ -2992,6 +3088,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2992
3088
  ...(bgScope !== undefined ? { scope: bgScope } : {}),
2993
3089
  description: reviveRow !== undefined ? `${shortDesc} (revived)` : shortDesc,
2994
3090
  agentType: spawnAgentType,
3091
+ ...(spawnModel !== undefined ? { model: spawnModel } : {}),
2995
3092
  ...(bgCycleSeq !== undefined ? { seq: bgCycleSeq } : {}),
2996
3093
  ...(agentName !== undefined ? { name: agentName } : {}),
2997
3094
  sessionId: bgChildSessionId,
@@ -3065,6 +3162,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3065
3162
  parentToolCallId: ctx.toolCallId,
3066
3163
  progressTaskId: bgChildSessionId,
3067
3164
  agentType: spawnAgentType,
3165
+ ...(spawnModel !== undefined ? { model: spawnModel } : {}),
3068
3166
  ...(bgCycleSeq !== undefined ? { seq: bgCycleSeq } : {}),
3069
3167
  ...(currentAction !== undefined ? { currentAction } : {}),
3070
3168
  ...(currentTool !== undefined ? { currentTool } : {}),
@@ -3111,6 +3209,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3111
3209
  progressTaskId: e.taskId,
3112
3210
  ...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
3113
3211
  agentType: spawnAgentType,
3212
+ ...(spawnModel !== undefined ? { model: spawnModel } : {}),
3114
3213
  ...(e.name !== undefined ? { name: e.name } : {}),
3115
3214
  ...(currentAction !== undefined ? { currentAction } : {}),
3116
3215
  ...(currentTool !== undefined ? { currentTool } : {}),
@@ -3399,9 +3498,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3399
3498
  named: agentName !== undefined,
3400
3499
  });
3401
3500
  const failReasonBg = settled === "failed" ? unparkedPauseReason ?? child.errorMessage : undefined;
3501
+ const bgElapsedMs = Date.now() - bgStartedAt;
3402
3502
  const bgTerminalSummary = failReasonBg !== undefined
3403
- ? `Agent "${shortDesc}" failed: ${failReasonBg}${ccElapsedTag(Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300) + errorKindClause(errClassBg)
3404
- : `${ccCompletionText(shortDesc, settled, String(child.status), Date.now() - bgStartedAt)}${observerNote}`;
3503
+ ? `Agent "${shortDesc}" failed: ${failReasonBg}${ccElapsedTag(bgElapsedMs)}${observerNote}`.slice(0, 300) + errorKindClause(errClassBg)
3504
+ : `${ccCompletionText(shortDesc, settled, String(child.status), bgElapsedMs)}${observerNote}`;
3405
3505
  const ownsTerminalFaces = bg.registry.claimAgentTerminalNotify(taskId);
3406
3506
  if (ownsTerminalFaces)
3407
3507
  sinkEmit({
@@ -3416,7 +3516,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3416
3516
  summary: bgTerminalSummary,
3417
3517
  ...residual,
3418
3518
  resumable: resumableBg,
3419
- usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
3519
+ usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFields(child.stats, bgElapsedMs) },
3420
3520
  ...(completionIdBg !== undefined ? { completionId: completionIdBg } : {}),
3421
3521
  });
3422
3522
  const completionFrame = {
@@ -3429,12 +3529,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3429
3529
  summary: bgTerminalSummary,
3430
3530
  ...(failReasonBg !== undefined ? { error: failReasonBg.slice(0, REPORT_FIELD_MAX) } : {}),
3431
3531
  ...(settled === "failed" && errCodeBg !== undefined ? { errorCode: errCodeBg } : {}),
3532
+ ...providerFaultField(child),
3432
3533
  ...(child.sessionId ? { sessionId: child.sessionId } : {}),
3433
3534
  ...(bgHandbackResult ? { result: notifyResultField(bgHandbackResult) } : {}),
3434
3535
  ...(settled === "killed" && child.result ? { partial: true } : {}),
3435
3536
  ...residual,
3436
3537
  resumable: resumableBg,
3437
- usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
3538
+ usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFieldsWire(child.stats, bgElapsedMs) },
3438
3539
  ...(completionIdBg !== undefined ? { completionId: completionIdBg } : {}),
3439
3540
  };
3440
3541
  const gateOwner = ownsTerminalFaces && settled === "completed" && notify !== undefined ? bgRetain?.childSessionId : undefined;
@@ -166,6 +166,14 @@ export function validateFileHistoryExport(data) {
166
166
  return bad(`tracked row ${t.relOrAbsPath} carries a non-integer version number`);
167
167
  if (v.blobHash !== null && (typeof v.blobHash !== "string" || !/^[0-9a-f]{64}$/.test(v.blobHash)))
168
168
  return bad(`tracked row ${t.relOrAbsPath} carries a blob reference that is not a sha256 digest`);
169
+ if (v.mode !== undefined) {
170
+ if (typeof v.mode !== "number" || !Number.isInteger(v.mode) || v.mode < 0 || v.mode > 0o7777) {
171
+ return bad(`tracked row ${t.relOrAbsPath} carries a mode outside the permission-bit domain [0, 0o7777] — refused, never coerced`);
172
+ }
173
+ if ((v.mode & 0o6000) !== 0) {
174
+ return bad(`tracked row ${t.relOrAbsPath} carries a set-ID mode (0o${v.mode.toString(8)}) — a restore replaying setuid/setgid bits would mint a privilege-escalation primitive; refused`);
175
+ }
176
+ }
169
177
  }
170
178
  if (new Set(t.versions.map((v) => v.version)).size !== t.versions.length)
171
179
  return bad(`tracked row ${t.relOrAbsPath} carries the same version number twice — a chain whose versions are ambiguous cannot be published`);
@@ -284,13 +292,15 @@ export async function applyFileRestoreViaEnv(env, absPath, target, opts) {
284
292
  return { outcome: "identical" };
285
293
  if (modeOnly === "applied")
286
294
  return { outcome: "applied" };
295
+ if (modeOnly === "applied_unpreserved")
296
+ return { outcome: "applied", modeNotPreserved: true };
287
297
  return { outcome: "identical", modeNotPreserved: true };
288
298
  }
289
299
  const w = await writeRestoredBytes(env, absPath, target.bytes, signal);
290
300
  if (!w.ok)
291
301
  return { outcome: w.refused ? "refused" : "failed", reason: `write: ${w.code}` };
292
302
  const modeAfter = await applyRecordedMode(env, absPath, opts, undefined);
293
- return { outcome: "applied", ...(modeAfter === "unpreserved" ? { modeNotPreserved: true } : {}) };
303
+ return { outcome: "applied", ...(modeAfter === "unpreserved" || modeAfter === "applied_unpreserved" ? { modeNotPreserved: true } : {}) };
294
304
  }
295
305
  async function restorePreflight(env, absPath, opts) {
296
306
  const info = await env.fileInfo(absPath, opts.signal);
@@ -337,7 +347,10 @@ export async function previewFileDelta(env, absPath, targetHash, targetBytes, ta
337
347
  return undefined;
338
348
  const info = await env.fileInfo(absPath);
339
349
  const currentMode = info.ok ? info.value.mode : undefined;
340
- return modeProvenDifferent(targetMode, currentMode) ? { insertions: 0, deletions: 0 } : undefined;
350
+ const replayMode = (targetMode & 0o6000) !== 0 ? targetMode & 0o1777 : targetMode;
351
+ return modeProvenDifferent(targetMode, currentMode) && modeProvenDifferent(replayMode, currentMode)
352
+ ? { insertions: 0, deletions: 0 }
353
+ : undefined;
341
354
  }
342
355
  return countRestoreLineDiff(curBytes, targetBytes);
343
356
  }
@@ -347,6 +360,15 @@ async function applyRecordedMode(env, absPath, opts, currentMode) {
347
360
  return "unchanged";
348
361
  if (currentMode === (want & 0o7777))
349
362
  return "unchanged";
363
+ if ((want & 0o6000) !== 0) {
364
+ const stripped = want & 0o1777;
365
+ if (currentMode === stripped)
366
+ return "unpreserved";
367
+ if (env.setFileMode === undefined)
368
+ return "unpreserved";
369
+ const r = await env.setFileMode(absPath, stripped, opts.signal);
370
+ return r.ok ? "applied_unpreserved" : "unpreserved";
371
+ }
350
372
  if (env.setFileMode === undefined)
351
373
  return "unpreserved";
352
374
  const r = await env.setFileMode(absPath, want & 0o7777, opts.signal);
@@ -62,12 +62,21 @@ export declare const RULE_SYNC_DROP_CODES: {
62
62
  /** The server refused this local row (its response `dropped` names it); it leaves the live view so
63
63
  * it does not ride — and get refused on — every future round. */
64
64
  readonly server_rejected: "local-quarantined";
65
+ /** design/382 §4.3 — the row carries a SESSION scope, which the durable face (store, sync wire,
66
+ * at-rest bytes) structurally never holds: a session authorization lives in its session's overlay
67
+ * and dies with it. The row is dropped and disclosed, never landed and never quarantined (the
68
+ * quarantine area is itself durable — parking a session row there would persist it). Inbound-
69
+ * refused in every reachable case: the direct write arms refuse session scopes LOUDLY
70
+ * (`unsupported.session_scope_store`) before one can become local. */
71
+ readonly session_scope_not_durable: "inbound-refused";
65
72
  };
66
73
  /** Every reason a sync round may drop or quarantine a record. Closed set; free text is not a member. */
67
74
  export type RuleSyncDropReason = keyof typeof RULE_SYNC_DROP_CODES;
68
75
  /** The subset that may appear on a LOCAL quarantined row (design/182 §8.1 `quarantine` instruction /
69
- * fence arm / local screening). `own_actor_forged` is inbound-only by construction. */
70
- export type RuleQuarantineReason = Exclude<RuleSyncDropReason, "own_actor_forged">;
76
+ * fence arm / local screening). `own_actor_forged` is inbound-only by construction;
77
+ * `session_scope_not_durable` never quarantines — the quarantine area is durable, and a session row
78
+ * parked there would be a session row persisted (design/382 §4.3). */
79
+ export type RuleQuarantineReason = Exclude<RuleSyncDropReason, "own_actor_forged" | "session_scope_not_durable">;
71
80
  /**
72
81
  * WHO a notice code is for. `"user"` = a session-scoped disclosure the end user of that session is
73
82
  * entitled to see (safe to project onto that session's event stream); `"operator"` = a
@@ -90,6 +90,7 @@ export const RULE_SYNC_DROP_CODES = {
90
90
  dot_identity_conflict: "inbound-refused",
91
91
  below_gc_frontier: "local-quarantined",
92
92
  server_rejected: "local-quarantined",
93
+ session_scope_not_durable: "inbound-refused",
93
94
  };
94
95
  export const ENGINE_NOTICE_CODES = [
95
96
  "config.autocompact_window_clamped",
@@ -29,6 +29,7 @@
29
29
  */
30
30
  import { type RuleOffer, type RuleRejectCode, type RuleScope, type RuleDot } from "./permission-rule-model.js";
31
31
  import type { PermissionRuleStoreProvider, RuleOwner } from "./permission-rule-store.js";
32
+ import type { SessionRuleOverlay } from "./permission-rule-session.js";
32
33
  /** One candidate rule inside an approval record: the exact text and where it would apply. */
33
34
  export interface RuleCandidate {
34
35
  rule: string;
@@ -199,6 +200,15 @@ export interface RuleConsentDeps {
199
200
  * happened, and withholding the receipt helps no one.
200
201
  */
201
202
  cardEdits?: boolean;
203
+ /**
204
+ * design/382 §4.3 — the SESSION-RULE OVERLAY: where a `{kind:"session"}` scoped candidate lands at
205
+ * redemption (the store write leg is never taken for one), and where the prepare-time coverage read
206
+ * merges a session's standing rows from. Host-provided, session-lifetime, never synced — see
207
+ * `permission-rule-session.ts` for the contract and the reference implementation. Absent ⇒ a
208
+ * session-scope candidate's redemption refuses loudly (there is nowhere for it to land), and
209
+ * coverage reads see no session rows — both fail toward asking.
210
+ */
211
+ sessionRules?: SessionRuleOverlay;
202
212
  }
203
213
  /** In-memory approval records — the test backend and the reference CAS semantics, the stale-row
204
214
  * envelope read included. */
@@ -257,8 +267,27 @@ export declare function prepareCardApproval(opts: {
257
267
  /** The ask's call id and argument digest, recorded for reconciliation. */
258
268
  toolCallId?: string;
259
269
  boundInputHash?: string;
260
- /** Where a redeemed rule would apply. Defaults to global. */
270
+ /**
271
+ * Where a redeemed rule would apply — any of the three consent dimensions (design/382 §4.1).
272
+ *
273
+ * design/382 §4.4 (#490②, BREAKING B2) — the DEFAULT is no longer global:
274
+ * · present ⇒ used as given (the explicit channel; all three members legal, garbage refused loudly);
275
+ * · absent with `cwd` present ⇒ `{ kind: "project", root: cwd }`. Root = the adjudicated call's
276
+ * cwd is a deliberately NARROW default — the engine is a library and repository semantics are
277
+ * host business; a host wanting the true project root passes an explicit scope. The narrow cost
278
+ * is more asks (a nested-directory rule does not cover siblings), the safe direction;
279
+ * · both absent ⇒ a LOUD refusal (`config.missing_scope`): silent-global was the #490② defect,
280
+ * silent-project has no root to anchor, and "where does this consent land" is not guessable.
281
+ */
261
282
  scope?: RuleScope;
283
+ /**
284
+ * design/382 §4.3 — the SESSION IDENTITY of the adjudicated call, threaded by the caller from the
285
+ * original call context exactly like `cwd` (never inferred from the process). It is the coverage
286
+ * read's third eligibility axis: with it, a standing session rule of that session counts as
287
+ * coverage; without it, no session rule covers anything (fail-closed). Independent of `scope` on
288
+ * purpose — `scope` names where a NEW consent would land, this names where the CALL is running.
289
+ */
290
+ sessionId?: string;
262
291
  /**
263
292
  * The working directory of the ADJUDICATED CALL — the same task root the gate's lane judged
264
293
  * with — threaded by the caller from the original call context. Never inferred from `scope`
@@ -400,7 +429,12 @@ export type EditedRuleTextPrecheck = {
400
429
  * so the surface's move is to not offer the edit box at all.
401
430
  */
402
431
  export declare function precheckEditedRuleText(text: string, command: string): EditedRuleTextPrecheck;
403
- /** What a redemption produced. `alreadyRedeemed` marks the replay path — the same dot, no second rule. */
432
+ /** What a redemption produced. `alreadyRedeemed` marks the replay path — the same dot, no second rule.
433
+ *
434
+ * `rev` is the DURABLE store's revision. A session-scope redemption (design/382 §4.3) lands in the
435
+ * session overlay and does not move it: its `rev` is a best-effort read of the store's current
436
+ * revision (0 when the store could not be read — the landing is in the overlay either way, and the
437
+ * durable revision is reporting, not the landing's identity). */
404
438
  export type RedeemResult = {
405
439
  status: "redeemed";
406
440
  rule: string;
@@ -508,6 +542,12 @@ export interface ImportPreview {
508
542
  * `false` AND `deduped` — the two axes answer different questions and neither implies the other).
509
543
  * A refused row always carries its `reason`. `deduped` counts as LANDED: an equivalent rule
510
544
  * already standing means the consent is already in effect.
545
+ *
546
+ * design/382 §4.3 — on a SESSION-scope member the two landed words mean the same thing about a
547
+ * different home: landed in / already in the session's OVERLAY (a session rule's home store), never
548
+ * the persisted store. The words state that the landing happened; DURABILITY is what the member's own
549
+ * `scope` says, so a consumer never has to guess it off the status (deliberately no fourth status
550
+ * value — the closed set names landing outcomes, not storage classes).
511
551
  */
512
552
  export type RedeemedBatchMember = {
513
553
  readonly candidateIndex: number;