@sema-agent/core 5.26.0 → 5.28.0

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 (58) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +5 -2
  3. package/dist/agents/agent-transcript-tool.js +2 -1
  4. package/dist/agents/send-message-tool.d.ts +4 -1
  5. package/dist/agents/subagent.d.ts +5 -2
  6. package/dist/core/checkpoint-store.d.ts +7 -2
  7. package/dist/core/hooks.d.ts +61 -4
  8. package/dist/core/hooks.js +37 -15
  9. package/dist/core/memory-engine/engine.d.ts +8 -5
  10. package/dist/core/memory-engine/engine.js +18 -6
  11. package/dist/core/memory-engine/file-backend.d.ts +144 -4
  12. package/dist/core/memory-engine/file-backend.js +304 -36
  13. package/dist/core/memory-engine/layout.d.ts +31 -2
  14. package/dist/core/memory-engine/layout.js +132 -8
  15. package/dist/core/memory-engine/types.d.ts +9 -1
  16. package/dist/core/memory-vector.d.ts +6 -1
  17. package/dist/core/memory-vector.js +14 -4
  18. package/dist/core/memory.js +1 -6
  19. package/dist/core/permission-rule-consent.d.ts +82 -8
  20. package/dist/core/permission-rule-consent.js +92 -1
  21. package/dist/core/permission-rule-model.d.ts +87 -6
  22. package/dist/core/permission-rule-model.js +79 -0
  23. package/dist/core/permission-rule-org.d.ts +22 -3
  24. package/dist/core/permission-rule-org.js +67 -20
  25. package/dist/core/permission-rule-store.js +2 -2
  26. package/dist/core/permission-rule-sync.d.ts +15 -1
  27. package/dist/core/permission-rule-sync.js +89 -47
  28. package/dist/core/runner/prepare-memory.js +14 -9
  29. package/dist/core/runner/prepare-task.d.ts +9 -3
  30. package/dist/core/runner/prepare-task.js +37 -11
  31. package/dist/core/runner/runtask.d.ts +8 -1
  32. package/dist/core/runner/runtask.js +8 -1
  33. package/dist/core/task-registry-agent.d.ts +13 -3
  34. package/dist/core/task-registry-agent.js +51 -21
  35. package/dist/core/task-registry-monitor.js +1 -1
  36. package/dist/core/task-registry-shared.d.ts +9 -0
  37. package/dist/core/task-registry.d.ts +6 -3
  38. package/dist/core/tool-policy.d.ts +44 -4
  39. package/dist/core/tool-policy.js +37 -3
  40. package/dist/core/tool-result-store.d.ts +108 -7
  41. package/dist/core/tool-result-store.js +95 -15
  42. package/dist/core/types.d.ts +115 -17
  43. package/dist/core/types.js +30 -1
  44. package/dist/engine/loop/types.d.ts +10 -3
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.js +1 -1
  47. package/dist/orchestration/run-workflow-tool.d.ts +5 -3
  48. package/dist/orchestration/workflow.d.ts +9 -6
  49. package/dist/stores/file/checkpoint-store.d.ts +2 -1
  50. package/dist/stores/file/index.d.ts +1 -1
  51. package/dist/stores/file/tool-result-store.d.ts +41 -1
  52. package/dist/stores/file/tool-result-store.js +107 -19
  53. package/dist/tools/fs/fs-bash.d.ts +7 -0
  54. package/dist/tools/fs/fs-shared.d.ts +5 -0
  55. package/dist/tools/fs/fs-shared.js +11 -7
  56. package/dist/tools/fs/index.d.ts +6 -0
  57. package/dist/tools/fs/index.js +2 -0
  58. package/package.json +1 -1
@@ -126,7 +126,12 @@ export async function prepareMemory(input) {
126
126
  };
127
127
  };
128
128
  const onEngineIncident = (err) => deps.onError?.(err, { phase: "memory", sessionId });
129
- const retrievalBackend = (b) => b.retrievalView?.() ?? b;
129
+ const adoptionRestricted = input.memoryPersistenceDeclared === false;
130
+ const retrievalBackend = (b, planeRestricted) => planeRestricted
131
+ ? (b.restrictedAdoptionView?.({ audit: false }) ??
132
+ b.retrievalView?.() ??
133
+ b)
134
+ : (b.retrievalView?.() ?? b);
130
135
  const planeScopes = (scopes, write) => [...new Set([...scopes, ...(write !== null ? [write] : [])])];
131
136
  const pollutedOpts = (engine) => {
132
137
  const rec = engine.sessionPollution(sessionId);
@@ -173,8 +178,8 @@ export async function prepareMemory(input) {
173
178
  const personal = createPersonalEngine(personalBackendChosen);
174
179
  const personalEngine = personal.engine;
175
180
  const p = planes;
176
- const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null);
177
- const personalHandle = await personalEngine.materialize(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null);
181
+ const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null, { adoptionRestricted });
182
+ const personalHandle = await personalEngine.materialize(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null, { adoptionRestricted });
178
183
  const writeIsPersonal = p.writePlane === "personal";
179
184
  writeEngine = writeIsPersonal ? personalEngine : projectEngine;
180
185
  writeHandle = writeIsPersonal ? personalHandle : projectHandle;
@@ -183,13 +188,13 @@ export async function prepareMemory(input) {
183
188
  injectFn = () => mergeInjections(projectEngine.inject(projectHandle, { writeToolMounted: input.writeToolsMounted }), personalEngine.inject(personalHandle, { writeToolMounted: input.writeToolsMounted }));
184
189
  toolPlanes = [
185
190
  {
186
- backend: retrievalBackend(backend),
191
+ backend: retrievalBackend(backend, adoptionRestricted || p.writePlane !== "project"),
187
192
  scopes: planeScopes(p.project, p.writePlane === "project" ? memorySpec.writeScope : null),
188
193
  recordRetrieved: (ids) => projectEngine.recordRetrieved(ids),
189
194
  challengeExclusions: () => projectEngine.readChallengeExclusions(),
190
195
  },
191
196
  {
192
- backend: retrievalBackend(personal.backend),
197
+ backend: retrievalBackend(personal.backend, adoptionRestricted || p.writePlane !== "personal"),
193
198
  scopes: planeScopes(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null),
194
199
  recordRetrieved: (ids) => personalEngine.recordRetrieved(ids),
195
200
  challengeExclusions: () => personalEngine.readChallengeExclusions(),
@@ -216,14 +221,14 @@ export async function prepareMemory(input) {
216
221
  else if (personalOnly) {
217
222
  const personal = createPersonalEngine(choosePersonalBackend());
218
223
  const personalEngine = personal.engine;
219
- const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope);
224
+ const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted });
220
225
  writeEngine = personalEngine;
221
226
  writeHandle = handle;
222
227
  injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted });
223
228
  harvestBoth = () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId, ...admitNothingOpts });
224
229
  toolPlanes = [
225
230
  {
226
- backend: retrievalBackend(personal.backend),
231
+ backend: retrievalBackend(personal.backend, adoptionRestricted || memorySpec.writeScope === null),
227
232
  scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope),
228
233
  recordRetrieved: (ids) => personalEngine.recordRetrieved(ids),
229
234
  challengeExclusions: () => personalEngine.readChallengeExclusions(),
@@ -237,14 +242,14 @@ export async function prepareMemory(input) {
237
242
  controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
238
243
  onIncident: onEngineIncident,
239
244
  });
240
- const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope);
245
+ const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted });
241
246
  writeEngine = engine;
242
247
  writeHandle = handle;
243
248
  injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted });
244
249
  harvestBoth = () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId, ...admitNothingOpts });
245
250
  toolPlanes = [
246
251
  {
247
- backend: retrievalBackend(backend),
252
+ backend: retrievalBackend(backend, adoptionRestricted || memorySpec.writeScope === null),
248
253
  scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope),
249
254
  recordRetrieved: (ids) => engine.recordRetrieved(ids),
250
255
  challengeExclusions: () => engine.readChallengeExclusions(),
@@ -354,7 +354,10 @@ export interface Prepared {
354
354
  * deferred; a caller with no accessor has no deferred family to describe. */
355
355
  staticFaceFor?: (name: string) => boolean;
356
356
  /**
357
- * design/138 S1 — the MemoryEngine session (present iff `deps.memoryBackend` + `spec.memory.enabled`).
357
+ * design/138 S1 — the MemoryEngine session. Present when `deps.memoryBackend` + `spec.memory.enabled`
358
+ * hold AND the engine mount succeeded: a materialize failure without a `config.memory_*` code is
359
+ * fail-open (reported via `deps.onError`, the task runs memory-less), leaving this absent even though
360
+ * both flags hold.
358
361
  * `harvest` is the swallow-guarded boundary hook (task terminal in runtask + the checkpoint mint
359
362
  * point in commitSuspendSaga): it runs the FULL gate set (containment/secret/caps/deletion fuse),
360
363
  * commits entry patches to the backend, self-heals the derived index, and re-baselines (a second
@@ -1296,8 +1299,11 @@ export interface RunInternals {
1296
1299
  * design/99 (nested-subagent live tree) — an OPT-IN, DISPLAY-ONLY event sink a deployment sets on the TOP run to
1297
1300
  * receive a subagent's live `task_progress` ticks (which otherwise stay in the child's ISOLATED stream). Threaded
1298
1301
  * recursively down the delegation tree (via `ctx.forwardEvent`), so every nested subagent's ticks bubble to the
1299
- * SAME sink. The Runner forwards ONLY `task_progress` through it; the child stream is NEVER merged into the
1300
- * parent's MODEL context (this is purely a render channel). Absent unless the deployment opted in.
1302
+ * SAME sink. The Runner's ctx wrapper forwards `task_progress` always; when the run's spec sets
1303
+ * `forwardSubagentEvents: true` it ALSO forwards the child's content events (`text_delta` / `reasoning_delta` /
1304
+ * `tool_start` / `tool_end` — the subagent viewing pane, carrying the same UNTRUSTED-RAW/consumer-must-redact
1305
+ * contract as the main stream's tool events). Either way the child stream is NEVER merged into the parent's
1306
+ * MODEL context (this is purely a render channel). Absent unless the deployment opted in.
1301
1307
  */
1302
1308
  onForwardEvent?: (event: TaskEvent) => void;
1303
1309
  /**
@@ -29,7 +29,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
29
29
  import { policyAskClassOf } from "../ask-class.js";
30
30
  import { emitTrace } from "../trace.js";
31
31
  import { createSessionRulePolicy } from "./session-rule-policy.js";
32
- import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, runToolGate } from "../hooks.js";
32
+ import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, persistedRuleMandateOf, runToolGate } from "../hooks.js";
33
33
  import { orgRuleVerdictFor } from "../permission-rule-org.js";
34
34
  import { reconcileInterruptedSession } from "../session-reconcile.js";
35
35
  import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
@@ -89,10 +89,14 @@ import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOI
89
89
  import { boundInputHashOf } from "../canonical-json.js";
90
90
  import { countElicitOptIns, deriveAskEffective, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
91
91
  import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
92
+ import { deliverEngineNotice } from "../types.js";
92
93
  const announcedMaterializeEnv = new Set();
93
94
  export function __resetMaterializeEnvAnnouncements() {
94
95
  announcedMaterializeEnv.clear();
95
96
  }
97
+ function emitMaterializeEnvNotice(onNotice, message, detail) {
98
+ deliverEngineNotice(onNotice, { code: "config.materialize_env_discarded", message, detail });
99
+ }
96
100
  const RECONCILE_MAX_RETRIES = 3;
97
101
  const DEFAULT_MAX_SUSPENDS = 5;
98
102
  const TASK_LIMIT_KEY_DICT = {
@@ -1659,6 +1663,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1659
1663
  ...(sessionId !== undefined ? { sessionId } : {}),
1660
1664
  ...(internals?.onTaskNotification !== undefined ? { taskNotification: internals.onTaskNotification } : {}),
1661
1665
  ...(internals?.detachHub !== undefined ? { detachHub: internals.detachHub } : {}),
1666
+ ...(deps.onNotice !== undefined ? { onNotice: deps.onNotice } : {}),
1662
1667
  pdfModelCapabilities: pdfModelCapabilitiesOf(model),
1663
1668
  ...(deps.hands?.bashReadonlyAllow !== undefined ? { bashReadonlyAllow: deps.hands.bashReadonlyAllow } : {}),
1664
1669
  ...(deps.hands?.commitCoAuthor !== undefined ? { commitCoAuthor: deps.hands.commitCoAuthor } : {}),
@@ -2606,7 +2611,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2606
2611
  const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(raw)} is not "swap" or "static" — inert on this task (no deferred tools), but a deferring task WITHOUT an explicit spec strategy will refuse to prepare under it (an explicit legal spec outranks and discards it, loudly). Fix or unset the flag.`;
2607
2612
  if (!announcedMaterializeEnv.has(line)) {
2608
2613
  announcedMaterializeEnv.add(line);
2609
- console.warn(line);
2614
+ emitMaterializeEnvNotice(deps.onNotice, line, { raw });
2610
2615
  }
2611
2616
  }
2612
2617
  }
@@ -2628,7 +2633,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2628
2633
  const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(rawEnvStrategy)} was ignored — not "swap" or "static", and the task spec pins toolMaterializeStrategy=${JSON.stringify(spec.toolMaterializeStrategy)} which outranks it. Fix or unset the env flag.`;
2629
2634
  if (!announcedMaterializeEnv.has(line)) {
2630
2635
  announcedMaterializeEnv.add(line);
2631
- console.warn(line);
2636
+ emitMaterializeEnvNotice(deps.onNotice, line, { raw: rawEnvStrategy, specStrategy: spec.toolMaterializeStrategy });
2632
2637
  }
2633
2638
  }
2634
2639
  const envStrategy = envStrategyInvalid ? undefined : rawEnvStrategy;
@@ -3260,9 +3265,25 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3260
3265
  },
3261
3266
  };
3262
3267
  })();
3263
- const ruleSuggestionsOf = (toolName, args) => {
3268
+ const ruleSuggestionsOf = (toolName, args, ask) => {
3264
3269
  if (permissionRuleLane === undefined || toolName !== PERSISTED_RULE_TOOL)
3265
3270
  return {};
3271
+ if ((spec.principal === undefined || spec.principal === "") && deps.localOwnerRules !== true)
3272
+ return {};
3273
+ if (ask?.requiresRealApproval === true ||
3274
+ ask?.persistedRuleShadowed !== undefined ||
3275
+ ask?.decisionReason === "hook" ||
3276
+ ask?.inheritedUnresolved === true ||
3277
+ ask?.ancestorResolved === true) {
3278
+ return {};
3279
+ }
3280
+ if (persistedRuleMandateOf({
3281
+ egress: egressTools.has(toolName),
3282
+ irreversibility: irreversibilityTier.get(toolName),
3283
+ shellGated: shellGatedBash,
3284
+ }) !== undefined) {
3285
+ return {};
3286
+ }
3266
3287
  const command = args?.command;
3267
3288
  if (typeof command !== "string")
3268
3289
  return {};
@@ -3315,7 +3336,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3315
3336
  toolName: creq.toolName,
3316
3337
  toolCallId: creq.toolCallId,
3317
3338
  args: editArgs,
3318
- ...ruleSuggestionsOf(creq.toolName, editArgs),
3339
+ ...ruleSuggestionsOf(creq.toolName, editArgs, { ...(re ?? {}), ancestorResolved: true }),
3319
3340
  message: re.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
3320
3341
  ...askSourceIdentity(),
3321
3342
  ...riskAxesOf(creq.toolName),
@@ -3400,7 +3421,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3400
3421
  toolName: creq.toolName,
3401
3422
  toolCallId: creq.toolCallId,
3402
3423
  args: presentedArgs,
3403
- ...ruleSuggestionsOf(creq.toolName, presentedArgs),
3424
+ ...ruleSuggestionsOf(creq.toolName, presentedArgs, { ...(first.action === "ask" ? first : {}), ancestorResolved: true }),
3404
3425
  message: first.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
3405
3426
  ...askSourceIdentity(),
3406
3427
  ...riskAxesOf(creq.toolName),
@@ -3487,7 +3508,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3487
3508
  toolName: creq.toolName,
3488
3509
  toolCallId: creq.toolCallId,
3489
3510
  args: presentedArgs,
3490
- ...ruleSuggestionsOf(creq.toolName, presentedArgs),
3511
+ ...ruleSuggestionsOf(creq.toolName, presentedArgs, { ...(decision.action === "ask" ? decision : {}), ancestorResolved: true }),
3491
3512
  message: decision.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
3492
3513
  ...askSourceIdentity(),
3493
3514
  ...riskAxesOf(creq.toolName),
@@ -3800,7 +3821,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3800
3821
  const preview = approvalPreviewOf(req.toolName, req.args);
3801
3822
  return preview !== undefined ? { preview } : {};
3802
3823
  })(),
3803
- ...ruleSuggestionsOf(req.toolName, req.args),
3824
+ ...ruleSuggestionsOf(req.toolName, req.args, decision.action === "ask" ? decision : undefined),
3804
3825
  message: decision.message ?? `approval required for "${req.toolName}"`,
3805
3826
  ...askSourceIdentity(),
3806
3827
  ...riskAxesOf(req.toolName),
@@ -4287,7 +4308,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4287
4308
  }
4288
4309
  };
4289
4310
  const suspendAsk = parkLaneArmed && checkpointStore !== undefined
4290
- ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule) => {
4311
+ ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason) => {
4291
4312
  const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
4292
4313
  if (syncFirstEligible &&
4293
4314
  runtimeCaps?.forceDurableGate !== true &&
@@ -4442,7 +4463,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4442
4463
  const preview = approvalPreviewOf(req.toolName, parkedArgs);
4443
4464
  return preview !== undefined ? { preview } : {};
4444
4465
  })(),
4445
- ...ruleSuggestionsOf(req.toolName, parkedArgs),
4466
+ ...ruleSuggestionsOf(req.toolName, parkedArgs, {
4467
+ ...(realApproval !== undefined ? { requiresRealApproval: true } : {}),
4468
+ ...(shadowedRule !== undefined ? { persistedRuleShadowed: shadowedRule } : {}),
4469
+ ...(askDecisionReason !== undefined ? { decisionReason: askDecisionReason } : {}),
4470
+ ...(inheritedUnavailableAsks.has(req.toolCallId) ? { inheritedUnresolved: true } : {}),
4471
+ }),
4446
4472
  boundInputHash: boundInputHashOf(parkedArgs),
4447
4473
  batchToolCallIds,
4448
4474
  completedCallIds,
@@ -4710,7 +4736,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4710
4736
  ...(offloadStore
4711
4737
  ? {
4712
4738
  offload: {
4713
- persist: createOffloadPersist(offloadStore, sessionId),
4739
+ persist: createOffloadPersist(offloadStore, sessionId, deps.onNotice),
4714
4740
  },
4715
4741
  }
4716
4742
  : {}),
@@ -250,7 +250,14 @@ export declare class Runner {
250
250
  * env_failed re-resume supplied a decision ≠ the persisted winner), `reopened_concurrently` (the
251
251
  * optimistic-concurrency `rev` changed under a concurrent resolve/reopen — re-resume against current
252
252
  * state), `unsupported_version` (checkpoint newer than this worker / remote handle with no factory),
253
- * `already_resolved` (lost the CAS — idempotent no-op).
253
+ * `already_resolved` (lost the CAS — idempotent no-op), `walltime_axis_retired` (pre-CAS: the
254
+ * persisted ledger carries the retired wall-clock budget axis — a worker of the previous release can
255
+ * still finish it), `resume_aborted` (the caller's signal was already aborted at entry, or aborted
256
+ * during the resume — pre-CAS/edit-re-adjudication legs leave the checkpoint pending and resumable,
257
+ * the post-claim leg leaves it consumed; the message says which), `reopen_failed` (an aborted resume
258
+ * needed to reopen the checkpoint and the store refused, or the reopen failed in flight — the message
259
+ * distinguishes terminally-consumed from state-unprovable). Each code's full contract is on
260
+ * {@link CheckpointError.code}.
254
261
  */
255
262
  resume(token: CheckpointToken, outcome: ResumeOutcome, taskConfig: ResumeTaskConfig,
256
263
  /** RB-48② (1.404): same TRUSTED run-internals seam as {@link resumeStream} — the convenience wrapper
@@ -131,7 +131,10 @@ const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call t
131
131
  function toolEndBodyFrom(result, isError, settledBy) {
132
132
  const o = toolOutputFrom(result);
133
133
  const st = structuredFrom(result);
134
- const code = isError ? result?.details?.code : undefined;
134
+ const det = isError ? result?.details : undefined;
135
+ const codeRaw = det?.code;
136
+ const kindRaw = det?.errorKind;
137
+ const code = typeof codeRaw === "string" ? codeRaw : typeof kindRaw === "string" ? kindRaw : undefined;
135
138
  return {
136
139
  ...(o !== undefined ? { output: o.output, ...(o.truncated ? { truncated: true } : {}), ...(o.totalChars !== undefined ? { totalChars: o.totalChars } : {}) } : {}),
137
140
  ...(st !== undefined ? { structured: st } : {}),
@@ -1412,6 +1415,10 @@ export class Runner {
1412
1415
  return runSideQuery(spec, { brain: this.deps.brain, models: this.deps.models, roles: this.deps.roles });
1413
1416
  }
1414
1417
  runTaskStream(spec, resume, internals) {
1418
+ if (resume !== undefined && (typeof resume !== "object" || resume.outcome === undefined)) {
1419
+ throw new TypeError("runTaskStream: `resume` must be a ResumeRun carrying `outcome` — got a value without one. " +
1420
+ "(Note the parameter order: runTaskStream(spec, resume?, internals?) — internals is the THIRD parameter.)");
1421
+ }
1415
1422
  const entryActor = spec.actor === undefined ? undefined : snapshotActorAssertion(spec.actor);
1416
1423
  const queue = new PushQueue();
1417
1424
  const detachHub = new ToolDetachHub();
@@ -415,9 +415,10 @@ export declare function notFoundRunningAgentsTail(footer: {
415
415
  * so a structured consumer lost the failure FACT at exactly the moment it could no longer reach the
416
416
  * live handle, left to scrape the prose body's `error:` line.
417
417
  *
418
- * The status GATES live here too (killed ⇒ stoppedBy, failed ⇒ error/errorCode/retryable) rather than
419
- * at the call sites: which facts a given status may carry is part of the same contract, and a gate
420
- * copied per face is the same drift with extra steps. `error` is model/provider-influenceable text, so
418
+ * The fact gates live here too (failed ⇒ error/errorCode/retryable/retryAfterMs; `stoppedBy` rides
419
+ * whenever the ROW carries it, on any status a reaper-settled `failed` row keeps its attribution,
420
+ * see the widening note in the builder) rather than at the call sites: which facts a given row may
421
+ * carry is part of the same contract, and a gate copied per face is the same drift with extra steps. `error` is model/provider-influenceable text, so
421
422
  * the fencing + bounding (RB-386②'s posture) happens once, here, for both faces.
422
423
  * The parked projection is this builder with fewer facts, not a third literal. */
423
424
  export interface AgentPollDetailsInput {
@@ -470,6 +471,15 @@ export declare function serveDurableAgentRowLane(row: BackgroundAgentRecord): Un
470
471
  *
471
472
  * No store configured (a deployment that never wired a `toolResultStore`) ⇒ returns `clipped` UNCHANGED
472
473
  * — the legal degrade design/158 §2.2 calls for, byte-identical to pre-S1 behavior.
474
+ *
475
+ * Backlog #169 — a FAILING `put` (throwing or rejecting; the store contract admits both dialects) is
476
+ * NOT fatal to the poll: the clipped text is still served, with {@link AGENT_SPILL_FAILED_NOTE} in
477
+ * place of the ref disclosure (advertising a ref nobody can read back would be a false promise), and
478
+ * `handle.spillFailed` latches so later polls of the same cycle repeat the honest note instead of
479
+ * retrying the write — see the catch below for why latch-not-retry. Same "store failure is never
480
+ * fatal" posture as the monitor/offload/budget/compaction write sites. A put that settles AFTER a
481
+ * revive (stale cycle) is generation-guarded: it answers its own caller honestly but never writes
482
+ * spill state onto the new cycle (see `mintCycle` below).
473
483
  */
474
484
  export declare function spillClippedAgentResult(handle: BackgroundAgentTaskHandle, full: string, clipped: string, store: ToolResultStore | undefined, sessionId: string | undefined): Promise<string>;
475
485
  export declare function pollBackgroundAgentLane(handle: BackgroundAgentTaskHandle, deadline?: number, signal?: AbortSignal,
@@ -981,6 +981,7 @@ export async function reviveBackgroundAgentLane(core, id, access, abort) {
981
981
  handle.result = undefined;
982
982
  handle.resultFull = undefined;
983
983
  handle.spillRef = undefined;
984
+ handle.spillFailed = undefined;
984
985
  handle.error = undefined;
985
986
  handle.errorCode = undefined;
986
987
  handle.errorRetryable = undefined;
@@ -1226,17 +1227,33 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1226
1227
  ...(row.status === "failed" ? { isError: true } : {}),
1227
1228
  };
1228
1229
  }
1230
+ const AGENT_SPILL_FAILED_NOTE = "\n\n[full output could not be spilled to the offload store (the write failed or went unconfirmed) — no ref is available to read the dropped middle back.]";
1229
1231
  export async function spillClippedAgentResult(handle, full, clipped, store, sessionId) {
1230
1232
  if (clipped === full)
1231
1233
  return clipped;
1232
1234
  if (store === undefined)
1233
1235
  return clipped;
1234
- if (handle.spillRef === undefined) {
1235
- const ref = buildToolResultRef(sessionId ?? "no-session", handle.id, `c${handle.reviveCycle ?? 0}`);
1236
- await store.put(ref, full, sessionId === undefined ? undefined : toolResultProvenanceOf(sessionId, handle.id));
1237
- handle.spillRef = ref;
1236
+ const disclose = (ref) => `${clipped}\n\n[full output persisted — call ${OFFLOAD_TOOL_NAME} with ref "${ref}" to read it back.]`;
1237
+ if (handle.spillRef !== undefined)
1238
+ return disclose(handle.spillRef);
1239
+ if (handle.spillFailed === true)
1240
+ return `${clipped}${AGENT_SPILL_FAILED_NOTE}`;
1241
+ const mintCycle = handle.reviveCycle ?? 0;
1242
+ const ref = buildToolResultRef(sessionId ?? "no-session", handle.id, `c${mintCycle}`);
1243
+ try {
1244
+ await store.put(ref, full, sessionId === undefined ? undefined : toolResultProvenanceOf(sessionId));
1245
+ }
1246
+ catch {
1247
+ if ((handle.reviveCycle ?? 0) === mintCycle) {
1248
+ if (handle.spillRef !== undefined)
1249
+ return disclose(handle.spillRef);
1250
+ handle.spillFailed = true;
1251
+ }
1252
+ return `${clipped}${AGENT_SPILL_FAILED_NOTE}`;
1238
1253
  }
1239
- return `${clipped}\n\n[full output persisted — call ${OFFLOAD_TOOL_NAME} with ref "${handle.spillRef}" to read it back.]`;
1254
+ if ((handle.reviveCycle ?? 0) === mintCycle)
1255
+ handle.spillRef = ref;
1256
+ return disclose(ref);
1240
1257
  }
1241
1258
  export async function pollBackgroundAgentLane(handle, deadline, signal, oneShot, store, sessionId) {
1242
1259
  while (handle.status === "running" && deadline !== undefined && Date.now() < deadline && !signal?.aborted) {
@@ -1256,10 +1273,23 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
1256
1273
  }),
1257
1274
  };
1258
1275
  }
1259
- const fullResult = handle.result ? (handle.resultFull ?? handle.result) : undefined;
1276
+ const snap = {
1277
+ status: handle.status,
1278
+ result: handle.result,
1279
+ resultIsPartial: handle.resultIsPartial,
1280
+ error: handle.error,
1281
+ errorKind: handle.errorKind,
1282
+ errorCode: handle.errorCode,
1283
+ errorRetryable: handle.errorRetryable,
1284
+ errorRetryAfterMs: handle.errorRetryAfterMs,
1285
+ cycleSeq: handle.cycleSeq,
1286
+ stoppedBy: handle.stoppedBy,
1287
+ completionId: handle.completionId,
1288
+ };
1289
+ const fullResult = snap.result ? (handle.resultFull ?? snap.result) : undefined;
1260
1290
  const resultText = fullResult !== undefined ? await spillClippedAgentResult(handle, fullResult, clipTaskOutput(fullResult, handle.outputFile), store, sessionId) : undefined;
1261
- const kindClause = handle.status === "failed" && handle.errorKind !== undefined && handle.errorRetryable !== undefined
1262
- ? ` (error_kind: ${handle.errorKind}, retryable: ${handle.errorRetryable}${handle.errorRetryAfterMs !== undefined ? `, retry_after_ms: ${handle.errorRetryAfterMs}` : ""})`
1291
+ const kindClause = snap.status === "failed" && snap.errorKind !== undefined && snap.errorRetryable !== undefined
1292
+ ? ` (error_kind: ${snap.errorKind}, retryable: ${snap.errorRetryable}${snap.errorRetryAfterMs !== undefined ? `, retry_after_ms: ${snap.errorRetryAfterMs}` : ""})`
1263
1293
  : "";
1264
1294
  const body = running
1265
1295
  ? oneShot === true
@@ -1267,26 +1297,26 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
1267
1297
  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: "${handle.id}", 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.`
1268
1298
  : `status: running
1269
1299
  The agent is still working — you will be notified when it completes.`
1270
- : `status: ${handle.status}
1271
- ${handle.error ? `error: ${handle.error}${kindClause}
1272
- ` : ""}${handle.result ? `--- result${handle.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
1300
+ : `status: ${snap.status}
1301
+ ${snap.error ? `error: ${snap.error}${kindClause}
1302
+ ` : ""}${snap.result ? `--- result${snap.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
1273
1303
  ${resultText}` : "(no result text)"}`;
1274
1304
  return {
1275
1305
  content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
1276
1306
  details: buildAgentPollDetails({
1277
1307
  taskId: handle.id,
1278
- status: handle.status,
1308
+ status: snap.status,
1279
1309
  retrievalStatus: retrieval,
1280
- ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
1281
- ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1282
- ...(handle.error !== undefined ? { error: handle.error } : {}),
1283
- ...(handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
1284
- ...(handle.errorRetryable !== undefined ? { errorRetryable: handle.errorRetryable } : {}),
1285
- ...(handle.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: handle.errorRetryAfterMs } : {}),
1286
- ...(handle.resultIsPartial === true ? { resultIsPartial: true } : {}),
1287
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1310
+ ...(snap.cycleSeq !== undefined ? { seq: snap.cycleSeq } : {}),
1311
+ ...(snap.stoppedBy !== undefined ? { stoppedBy: snap.stoppedBy } : {}),
1312
+ ...(snap.error !== undefined ? { error: snap.error } : {}),
1313
+ ...(snap.errorCode !== undefined ? { errorCode: snap.errorCode } : {}),
1314
+ ...(snap.errorRetryable !== undefined ? { errorRetryable: snap.errorRetryable } : {}),
1315
+ ...(snap.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: snap.errorRetryAfterMs } : {}),
1316
+ ...(snap.resultIsPartial === true ? { resultIsPartial: true } : {}),
1317
+ ...(snap.completionId !== undefined ? { completionId: snap.completionId } : {}),
1288
1318
  }),
1289
- ...(handle.status === "failed" ? { isError: true } : {}),
1319
+ ...(snap.status === "failed" ? { isError: true } : {}),
1290
1320
  };
1291
1321
  }
1292
1322
  export async function stopBackgroundAgentLane(core, handle) {
@@ -68,7 +68,7 @@ function spillRolledMonitorChunk(handle, stream, dropped) {
68
68
  else
69
69
  handle.spillErrSegCount = n + 1;
70
70
  try {
71
- const provenance = handle.spillSessionId === undefined ? undefined : toolResultProvenanceOf(handle.spillSessionId, handle.id);
71
+ const provenance = handle.spillSessionId === undefined ? undefined : toolResultProvenanceOf(handle.spillSessionId);
72
72
  void Promise.resolve(store.put(ref, dropped, provenance)).catch(() => {
73
73
  handle.spillFailed = true;
74
74
  });
@@ -324,6 +324,15 @@ export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
324
324
  * eventual spill lands under a DIFFERENT ref rather than colliding with (and being silently refused
325
325
  * by the write-once store under) cycle 1's. */
326
326
  spillRef?: string;
327
+ /** Backlog #169 — the spill `put` for the CURRENT revive cycle threw or rejected. One-time latch,
328
+ * scoped to the cycle ({@link import("./task-registry-agent.js").reviveBackgroundAgentLane} clears
329
+ * it in lockstep with `spillRef`): later polls serve the clipped text with an honest ref-free loss
330
+ * note instead of retrying the write — the ref is minted deterministically, so the known permanent
331
+ * failure class (the store's write-once/provenance conflict) would fail identically on every poll,
332
+ * each retry a guaranteed-failure round trip to a durable backend. The monitor lane's
333
+ * `spillFailed` twin (same degrade discipline, different write shape — that one is fire-and-forget,
334
+ * this one is awaited). */
335
+ spillFailed?: true;
327
336
  error?: string;
328
337
  /** RB-386② ([2090]) — the machine-readable failure code beside `error`, threaded from the settle
329
338
  * mint point (subagent.ts computes it ONCE from the child's TaskResult.errorCode taxonomy /
@@ -33,8 +33,10 @@ export interface RegisterBackgroundBashInput extends TaskAccess {
33
33
  /** CC 2.1.209 对齐批A A5: the task's OUTPUT FILE — the launch receipt advertises it and the watcher
34
34
  * mirrors every polled increment into it (append-only, unbounded — unlike the rolling in-memory
35
35
  * spool), so "Read the output file path" is a real alternative to TaskOutput. Terminal
36
- * task-notifications carry the same path (`output_file`). Only meaningful with `onTerminal` (the
37
- * watcher is the writer); callers create the file (empty) before registering. */
36
+ * task-notifications carry the same path (`output_file`). Primarily meaningful with `onTerminal`
37
+ * (only the watcher mirrors increments live); a watcher-less row still gets ONE final best-effort
38
+ * append at TaskStop/run-teardown (the tail drain), so the file is written even without a watcher —
39
+ * there is just no live mirror. Callers create the file (empty) before registering. */
38
40
  outputFile?: string;
39
41
  }
40
42
  export interface TaskPollOptions {
@@ -369,7 +371,8 @@ export declare class TaskRegistry {
369
371
  * BEFORE the abort()/status flip — the ordering is load-bearing: the guard below refuses markers on a
370
372
  * non-running handle, so a caller that flips first loses its claim and attribution falls back to
371
373
  * "system". First-marker-wins: an earlier marker (e.g. a service-wire "user") is never overwritten.
372
- * Applies to background_agent AND background_bash (same class); workflow cancellation is out of scope. */
374
+ * Applies to background_agent, background_bash AND monitor handles (the markable kinds the monitor
375
+ * watcher/stop lanes mark and read it too); workflow cancellation is out of scope. */
373
376
  markStopSource(id: string, source: StopSource): void;
374
377
  /**
375
378
  * [1712] / RB-164 — attribute an ENV-LEVEL blanket sweep before it runs.
@@ -78,7 +78,8 @@ export interface ToolCallRequest {
78
78
  * - `"org_unavailable"` — an org-governed deployment could not adjudicate against a snapshot, so the
79
79
  * whole decision boundary failed closed (see `ORG_UNAVAILABLE_DECISION_REASON`, the single
80
80
  * spelling this word is minted from). */
81
- export type DecisionReason = "rule" | "mode" | "hook" | "safety" | "classifier" | "persisted_rule" | "sandbox" | "org_rule" | "org_unavailable";
81
+ declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
82
+ export type DecisionReason = (typeof DECISION_REASONS)[number];
82
83
  /**
83
84
  * WHO (or what) ENDED an approval — the machine-readable twin of a settlement's human-readable text,
84
85
  * so a consumer tells "a person decided this" from "nobody answered" without prose-matching a sentence.
@@ -336,8 +337,39 @@ export declare function constraintChainDigest(chain: readonly ConstraintChainEnt
336
337
  * out-of-contract decision object, so it is not a rewrite this layer is willing to report as audited.
337
338
  * (A rewrite from an EARLIER, contract-shaped policy still rides out — {@link combinePolicies} adds it
338
339
  * on the deny path per the observer contract.)
340
+ *
341
+ * The second arm screens `decisionReason` against the closed set ({@link DecisionReason}): a
342
+ * deployment-authored `ToolPolicy.check` returning a value outside it would otherwise ride silently
343
+ * into every consumer that BRANCHES on the word — provenance-keyed exclusions, audit rows, checkpoint
344
+ * discriminants — each of which would treat the unclassifiable value as "none of the reasons I know",
345
+ * a meaning the producing policy never chose. Same doctrine as the retired-field arm: out-of-contract
346
+ * input to the permission face is refused loudly, not passed through as an accidental tenth reason.
347
+ * An ABSENT `decisionReason` stays legal (it is optional; `undefined` is the typed spelling of
348
+ * absence), and hook results never reach here with a foreign word — the hook seam stamps its own
349
+ * (`hooks.ts`) — so this arm's live producers are exactly the deployment policies the tripwire exists
350
+ * to screen. Unlike the retired-field arm this one reads the property PLAINLY (prototype chain
351
+ * included), because it answers a different question: the retired arm asks whether the AUTHOR wrote
352
+ * the old field (own property = authorship), this arm asks what a CONSUMER would read — and every
353
+ * consumer branches on a plain `d.decisionReason` read, which the prototype can satisfy.
354
+ *
355
+ * SCOPE of the carrier check (ruled after three review rounds converged on the same root): it screens
356
+ * STRUCTURALLY unstable carriers — getters, prototype-supplied values — i.e. shapes an ordinary
357
+ * deployment can write by accident. It does NOT try to defeat a Proxy whose descriptor trap reports a
358
+ * data property while its get trap stays stateful: same-process JavaScript has no trust boundary a
359
+ * function can enforce (an adversary who ships such a Proxy can as easily patch this module), so
360
+ * chasing that shape adds complexity without adding a guarantee. The screen's promise is against
361
+ * drift and accident, not against a hostile co-resident.
362
+ *
363
+ * `reasonIsNonInput` (the HOOK seam's spelling — merged-code scan, 5.28 window): at that seam the
364
+ * field is documented as DISCARDED — every mint point downstream unconditionally re-stamps
365
+ * `decisionReason:"hook"` (fold, deny arm, delegated twin), and the allow path never reads it — so
366
+ * both reason arms are skipped there: refusing a value that cannot travel would fail-close a call
367
+ * over a field with no consumer, which inverted the discard contract the stamp exists to enforce.
368
+ * The retired-`reason` arm still applies (that one is about the MESSAGE channel, which does travel).
339
369
  */
340
- export declare function refuseOutOfContractDecision(d: PermissionResult): PermissionResult;
370
+ export declare function refuseOutOfContractDecision(d: PermissionResult, opts?: {
371
+ reasonIsNonInput?: boolean;
372
+ }): PermissionResult;
341
373
  /** The raw tool-name lists a name-keyed policy was built from (audit feed, see block note above). */
342
374
  export interface ToolPolicyNameSets {
343
375
  readonly allow?: readonly string[];
@@ -632,9 +664,16 @@ export interface AskRequest {
632
664
  /**
633
665
  * design/179 §4 — the persistable allow-rule forms this exact call could be covered by, so a surface can
634
666
  * offer "allow, and stop asking me this" with something concrete behind it. Present only when a
635
- * persisted allow-rule lane is armed AND the call is one the lane can speak for: a compound, a
667
+ * persisted allow-rule lane is armed AND the call is one the lane can speak for (a compound, a
636
668
  * redirection or a substitution yields NO suggestion, which is the honest answer rather than an option
637
- * that would be refused on redemption.
669
+ * that would be refused on redemption) AND the ask is one a persisted rule could actually clear — a
670
+ * mandated ask (operator shellGate:"always", the tool's own egress/irreversibility marks, a
671
+ * `requiresRealApproval` demand) and an ask carrying {@link persistedRuleShadowed} offer none.
672
+ *
673
+ * CONTRACT — array order is display order, narrowest first: the EXACT form is always index 0, a
674
+ * broader reviewed PREFIX form (at most one) follows. Basis ≤ 2. Selection indices and redemption
675
+ * tickets are keyed against this order, and the durable park row carries the same array under the
676
+ * same contract.
638
677
  *
639
678
  * ADVISORY display metadata, never adjudication input, and never a rule by itself: minting one is a
640
679
  * separate act that goes through the approval-record protocol, so a surface that ignores this field
@@ -844,3 +883,4 @@ export type ResolvedAsk = PermissionResult & {
844
883
  * have ended, and `decisionReason: "mode"` is already the honest word for what produced them.
845
884
  */
846
885
  export declare function resolveAsk(req: AskRequest, onAsk: OnAsk | undefined, signal?: AbortSignal): Promise<ResolvedAsk>;
886
+ export {};
@@ -7,6 +7,8 @@ import { boundInputHashOf } from "./canonical-json.js";
7
7
  import { inlineUntrusted } from "./untrusted-text.js";
8
8
  import { parsePermissionRule } from "./permission-rules.js";
9
9
  import { isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
10
+ const DECISION_REASONS = ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
11
+ const DECISION_REASON_SET = new Set(DECISION_REASONS);
10
12
  export const APPROVAL_SETTLED_BY_VALUES = ["human", "timeout", "aborted"];
11
13
  export function isApprovalSettledBy(v) {
12
14
  return typeof v === "string" && APPROVAL_SETTLED_BY_VALUES.includes(v);
@@ -79,12 +81,44 @@ const ALLOW = { action: "allow" };
79
81
  const RETIRED_TEXT_FIELD = "reason";
80
82
  const RETIRED_TEXT_FIELD_DENY_MESSAGE = `a permission decision carries the retired "${RETIRED_TEXT_FIELD}" field — rename it to "message" (the one text field ` +
81
83
  `a decision carries); denied fail-closed rather than executing a decision whose text this layer cannot read`;
82
- export function refuseOutOfContractDecision(d) {
84
+ export function refuseOutOfContractDecision(d, opts) {
83
85
  if (typeof d !== "object" || d === null)
84
86
  return d;
85
- if (!Object.prototype.hasOwnProperty.call(d, RETIRED_TEXT_FIELD))
87
+ if (Object.prototype.hasOwnProperty.call(d, RETIRED_TEXT_FIELD)) {
88
+ return { action: "deny", message: RETIRED_TEXT_FIELD_DENY_MESSAGE, decisionReason: "rule" };
89
+ }
90
+ if (opts?.reasonIsNonInput === true)
91
+ return d;
92
+ const dr = d.decisionReason;
93
+ if (dr === undefined)
86
94
  return d;
87
- return { action: "deny", message: RETIRED_TEXT_FIELD_DENY_MESSAGE, decisionReason: "rule" };
95
+ if (!DECISION_REASON_SET.has(dr)) {
96
+ let rendered;
97
+ try {
98
+ rendered = JSON.stringify(String(dr).slice(0, 64));
99
+ }
100
+ catch {
101
+ rendered = `[unprintable ${typeof dr}]`;
102
+ }
103
+ return {
104
+ action: "deny",
105
+ message: `a permission decision carries an unrecognized "decisionReason" value (${rendered}) — ` +
106
+ `the closed set is ${DECISION_REASONS.join("/")}; denied fail-closed rather than letting consumers ` +
107
+ `branch on a value this layer cannot classify`,
108
+ decisionReason: "rule",
109
+ };
110
+ }
111
+ const carrier = Object.getOwnPropertyDescriptor(d, "decisionReason");
112
+ if (carrier === undefined || !("value" in carrier)) {
113
+ return {
114
+ action: "deny",
115
+ message: `a permission decision supplies "decisionReason" through ${carrier === undefined ? "its prototype" : "an accessor"} — ` +
116
+ `contract fields must be own data properties, so consumers re-reading the field cannot be shown a ` +
117
+ `different value than this screen validated; denied fail-closed`,
118
+ decisionReason: "rule",
119
+ };
120
+ }
121
+ return d;
88
122
  }
89
123
  const DEADLINE_ELAPSED = Symbol("approval.deadline_elapsed");
90
124
  function withTimeout(p, ms, onTimeout) {