@sema-agent/core 5.24.0 → 5.26.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 (54) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/dist/agents/agent-definition.js +5 -0
  3. package/dist/agents/send-message-tool.js +1 -0
  4. package/dist/agents/subagent.d.ts +1 -0
  5. package/dist/agents/subagent.js +5 -0
  6. package/dist/core/checkpoint-store.d.ts +47 -8
  7. package/dist/core/checkpoint-store.js +1 -0
  8. package/dist/core/hooks.d.ts +12 -5
  9. package/dist/core/hooks.js +22 -4
  10. package/dist/core/memory-engine/dual-root.js +3 -1
  11. package/dist/core/memory-engine/engine.d.ts +45 -1
  12. package/dist/core/memory-engine/engine.js +40 -7
  13. package/dist/core/memory-engine/index.d.ts +1 -1
  14. package/dist/core/memory-engine/index.js +1 -1
  15. package/dist/core/permission-rule-consent.js +8 -1
  16. package/dist/core/permission-rule-org.d.ts +9 -0
  17. package/dist/core/permission-rule-org.js +12 -5
  18. package/dist/core/runner/compaction-call-options.d.ts +4 -4
  19. package/dist/core/runner/compaction-call-options.js +3 -4
  20. package/dist/core/runner/prepare-memory.d.ts +34 -15
  21. package/dist/core/runner/prepare-memory.js +85 -17
  22. package/dist/core/runner/prepare-task.d.ts +2 -0
  23. package/dist/core/runner/prepare-task.js +63 -11
  24. package/dist/core/runner/runtask.js +25 -8
  25. package/dist/core/store-contracts/tool-result-store-contract.d.ts +6 -0
  26. package/dist/core/store-contracts/tool-result-store-contract.js +24 -0
  27. package/dist/core/task-registry-agent.js +3 -3
  28. package/dist/core/task-registry-monitor.js +6 -5
  29. package/dist/core/tool-policy.d.ts +11 -0
  30. package/dist/core/tool-result-budget.d.ts +1 -1
  31. package/dist/core/tool-result-budget.js +3 -3
  32. package/dist/core/tool-result-store.d.ts +164 -9
  33. package/dist/core/tool-result-store.js +82 -23
  34. package/dist/core/types.d.ts +68 -0
  35. package/dist/core/untrusted-text.d.ts +6 -2
  36. package/dist/core/untrusted-text.js +1 -1
  37. package/dist/engine/session/import-validate.js +2 -1
  38. package/dist/index.d.ts +4 -4
  39. package/dist/index.js +4 -4
  40. package/dist/orchestration/workflow.js +2 -0
  41. package/dist/prompts/default.d.ts +11 -0
  42. package/dist/prompts/default.js +3 -0
  43. package/dist/stores/file/adoption/adopt.d.ts +23 -3
  44. package/dist/stores/file/adoption/adopt.js +1 -0
  45. package/dist/stores/file/adoption/marker.d.ts +26 -11
  46. package/dist/stores/file/fs-atomic.d.ts +1 -1
  47. package/dist/stores/file/permission-rule-store.d.ts +15 -1
  48. package/dist/stores/file/permission-rule-store.js +4 -1
  49. package/dist/stores/file/task-list-store.d.ts +15 -1
  50. package/dist/stores/file/task-list-store.js +2 -2
  51. package/dist/stores/file/tool-result-store.d.ts +45 -9
  52. package/dist/stores/file/tool-result-store.js +76 -9
  53. package/dist/tools/fs/fs-shared.js +26 -9
  54. package/package.json +1 -1
@@ -89,6 +89,10 @@ 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
+ const announcedMaterializeEnv = new Set();
93
+ export function __resetMaterializeEnvAnnouncements() {
94
+ announcedMaterializeEnv.clear();
95
+ }
92
96
  const RECONCILE_MAX_RETRIES = 3;
93
97
  const DEFAULT_MAX_SUSPENDS = 5;
94
98
  const TASK_LIMIT_KEY_DICT = {
@@ -348,6 +352,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
348
352
  e.code = "config.tool_materialize_invalid";
349
353
  throw e;
350
354
  }
355
+ if (spec.memoryPersistenceCapable !== undefined && typeof spec.memoryPersistenceCapable !== "boolean") {
356
+ const e = new Error(`memoryPersistenceCapable must be a boolean when present (got ${JSON.stringify(spec.memoryPersistenceCapable)}) — a non-boolean would silently read as capable.`);
357
+ e.code = "config.memory_persistence_invalid";
358
+ throw e;
359
+ }
351
360
  if (spec.toolMaterializeStrategy === "static" && spec.deferSelfResolve === false) {
352
361
  const e = new Error(`toolMaterializeStrategy "static" cannot be combined with deferSelfResolve: false — with the direct-call ` +
353
362
  `lane disabled a placeholder is never swapped and never self-resolves, so no deferred tool could ever be ` +
@@ -1150,6 +1159,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1150
1159
  ...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
1151
1160
  ...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
1152
1161
  ...(spec.envFacts !== undefined ? { envFacts: { ...spec.envFacts } } : {}),
1162
+ ...(spec.memoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: spec.memoryPersistenceCapable } : {}),
1153
1163
  getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
1154
1164
  parentCwd: taskRootPath,
1155
1165
  ...(centerAdoption !== undefined ? { centerArtifactDigest: centerAdoption.artifact.artifactDigest } : {}),
@@ -1683,20 +1693,24 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1683
1693
  `Set spec.shellGate to "classify" or "always" if this deployment expects doctrine-gated shell behavior.`), { phase: "config", sessionId, classification: "shell-gate-off" });
1684
1694
  }
1685
1695
  if (shellGate !== "off" && !(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
1686
- shellGatedBash = !egressTools.has("Bash") && !irreversibilityTier.has("Bash");
1687
- irreversibilityTier.set("Bash", shellGate === "always" ? "always" : "maybe");
1696
+ const bashTierBefore = irreversibilityTier.get("Bash");
1697
+ shellGatedBash = !egressTools.has("Bash") && bashTierBefore !== "always" && bashTierBefore !== "maybe";
1698
+ const bashEffectiveTier = shellGate === "always" || bashTierBefore === "always" ? "always" : "maybe";
1699
+ irreversibilityTier.set("Bash", bashEffectiveTier);
1688
1700
  irreversibleTools.add("Bash");
1689
1701
  const shellReadBoundary = () => ({
1690
1702
  roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
1691
1703
  ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
1692
1704
  });
1693
- if (shellGate === "classify")
1705
+ if (shellGate === "classify" && shellGatedBash)
1694
1706
  reversibilityProbes.set("Bash", bashReversibilityProbe(undefined, shellReadBoundary));
1695
1707
  if (backgroundTaskToolsActive) {
1696
- shellGatedMonitor = !egressTools.has("Monitor") && !irreversibilityTier.has("Monitor");
1697
- irreversibilityTier.set("Monitor", shellGate === "always" ? "always" : "maybe");
1708
+ const monitorTierBefore = irreversibilityTier.get("Monitor");
1709
+ shellGatedMonitor = !egressTools.has("Monitor") && monitorTierBefore !== "always" && monitorTierBefore !== "maybe";
1710
+ const monitorEffectiveTier = shellGate === "always" || monitorTierBefore === "always" ? "always" : "maybe";
1711
+ irreversibilityTier.set("Monitor", monitorEffectiveTier);
1698
1712
  irreversibleTools.add("Monitor");
1699
- if (shellGate === "classify")
1713
+ if (shellGate === "classify" && shellGatedMonitor)
1700
1714
  reversibilityProbes.set("Monitor", bashReversibilityProbe(undefined, shellReadBoundary));
1701
1715
  }
1702
1716
  }
@@ -1932,7 +1946,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1932
1946
  sessionId,
1933
1947
  taskRootPath,
1934
1948
  memoryWriteGateRef,
1935
- writeToolsMounted: tools.some((t) => t.name === "Write") && !(toolFaceSnapshot.exclude?.includes("Write") ?? false),
1949
+ writeToolsMounted: tools.some((t) => t.name === "Write") &&
1950
+ !(toolFaceSnapshot.exclude?.includes("Write") ?? false) &&
1951
+ !(handsEnabled && isRemoteExecutionEnv(executionEnv) && spec.memoryPersistenceCapable !== true),
1952
+ memoryPersistenceDeclared: spec.memoryPersistenceCapable,
1953
+ rosterCanPersist: spec.memoryPersistenceCapable ??
1954
+ tools.some((t) => {
1955
+ if (toolFaceSnapshot.exclude?.includes(t.name) ?? false)
1956
+ return false;
1957
+ const effect = (t.effect ?? toolEffects.get(t.name) ?? "write");
1958
+ if (effect === "read")
1959
+ return false;
1960
+ if (t.name !== "Write" && t.name !== "Edit" && t.name !== "NotebookEdit" && t.name !== "Bash")
1961
+ return false;
1962
+ return !(handsEnabled && isRemoteExecutionEnv(executionEnv));
1963
+ }),
1936
1964
  memorySearchToolsPlanned,
1937
1965
  admissionCtx: {
1938
1966
  orgMemoryDenied: complianceDenies.has("org_memory_mount"),
@@ -2572,6 +2600,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2572
2600
  toolsDeltaRef = { pending: [], pendingRemoved: [], pendingReadded: [], pendingFailed: failedMcpServers };
2573
2601
  }
2574
2602
  const listingRideRef = {};
2603
+ {
2604
+ const raw = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
2605
+ if (raw !== undefined && raw !== "swap" && raw !== "static" && deferred.size === 0) {
2606
+ 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
+ if (!announcedMaterializeEnv.has(line)) {
2608
+ announcedMaterializeEnv.add(line);
2609
+ console.warn(line);
2610
+ }
2611
+ }
2612
+ }
2575
2613
  if (deferred.size > 0) {
2576
2614
  if (deferred.has(TOOL_SEARCH_NAME) || tools.some((t) => t.name === TOOL_SEARCH_NAME)) {
2577
2615
  const e = new Error(`Tool name "${TOOL_SEARCH_NAME}" is reserved when deferred tools are present.`);
@@ -2579,12 +2617,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2579
2617
  throw e;
2580
2618
  }
2581
2619
  const registry = buildDeferredRegistry(deferred, tools);
2582
- const envStrategy = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
2583
- if (envStrategy !== undefined && envStrategy !== "swap" && envStrategy !== "static") {
2584
- const e = new Error(`SEMA_TOOL_MATERIALIZE_STRATEGY must be "swap" or "static" (got ${JSON.stringify(envStrategy)}).`);
2620
+ const rawEnvStrategy = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
2621
+ const envStrategyInvalid = rawEnvStrategy !== undefined && rawEnvStrategy !== "swap" && rawEnvStrategy !== "static";
2622
+ if (envStrategyInvalid && spec.toolMaterializeStrategy === undefined) {
2623
+ const e = new Error(`SEMA_TOOL_MATERIALIZE_STRATEGY must be "swap" or "static" (got ${JSON.stringify(rawEnvStrategy)}).`);
2585
2624
  e.code = "config.tool_materialize_invalid";
2586
2625
  throw e;
2587
2626
  }
2627
+ if (envStrategyInvalid) {
2628
+ 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
+ if (!announcedMaterializeEnv.has(line)) {
2630
+ announcedMaterializeEnv.add(line);
2631
+ console.warn(line);
2632
+ }
2633
+ }
2634
+ const envStrategy = envStrategyInvalid ? undefined : rawEnvStrategy;
2588
2635
  const requestedStrategy = spec.toolMaterializeStrategy ?? envStrategy ?? "swap";
2589
2636
  const laneDegrade = requestedStrategy === "static" && spec.deferSelfResolve === false;
2590
2637
  const materializeStatic = requestedStrategy === "static" && !laneDegrade;
@@ -3273,6 +3320,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3273
3320
  ...askSourceIdentity(),
3274
3321
  ...riskAxesOf(creq.toolName),
3275
3322
  ...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3323
+ ...(re.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: re.persistedRuleShadowed } : {}),
3276
3324
  }, onAskOf, csignal ?? abortController.signal);
3277
3325
  if (rr.action !== "allow")
3278
3326
  return rr;
@@ -3357,6 +3405,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3357
3405
  ...askSourceIdentity(),
3358
3406
  ...riskAxesOf(creq.toolName),
3359
3407
  ...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3408
+ ...(first.action === "ask" && first.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: first.persistedRuleShadowed } : {}),
3360
3409
  }, pc.onAsk, csignal ?? abortController.signal);
3361
3410
  const askWaitMs = Math.max(0, now() - askT0);
3362
3411
  if (resolved.action === "deny" && resolved.approverUnavailable === true) {
@@ -3443,6 +3492,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3443
3492
  ...askSourceIdentity(),
3444
3493
  ...riskAxesOf(creq.toolName),
3445
3494
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3495
+ ...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
3446
3496
  }, pc.onAsk, csignal ?? abortController.signal);
3447
3497
  const askWaitMs = Math.max(0, now() - askT0);
3448
3498
  if (resolved.action === "deny" && resolved.approverUnavailable === true) {
@@ -3755,6 +3805,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3755
3805
  ...askSourceIdentity(),
3756
3806
  ...riskAxesOf(req.toolName),
3757
3807
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
3808
+ ...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
3758
3809
  }, onAsk, abortController.signal);
3759
3810
  const waitMs = Math.max(0, now() - t0);
3760
3811
  if (resolved.approverUnavailable !== true) {
@@ -4236,7 +4287,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4236
4287
  }
4237
4288
  };
4238
4289
  const suspendAsk = parkLaneArmed && checkpointStore !== undefined
4239
- ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval) => {
4290
+ ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule) => {
4240
4291
  const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
4241
4292
  if (syncFirstEligible &&
4242
4293
  runtimeCaps?.forceDurableGate !== true &&
@@ -4341,6 +4392,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4341
4392
  toolName: req.toolName,
4342
4393
  args: parkedArgs,
4343
4394
  safety,
4395
+ ...(shadowedRule !== undefined ? { shadowedRule } : {}),
4344
4396
  shellGated: (req.toolName === "Bash" && shellGatedBash) || (req.toolName === "Monitor" && shellGatedMonitor),
4345
4397
  ...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
4346
4398
  });
@@ -1,6 +1,6 @@
1
1
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
2
2
  import { snapshotActorAssertion } from "../../internal/llm.js";
3
- import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
3
+ import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
4
4
  import { engineVersion } from "../version.js";
5
5
  import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
6
6
  import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
@@ -71,6 +71,7 @@ const STOP_HOOK_BLOCK_CAP = 8;
71
71
  const COMPACTION_REGROWTH_FACTOR = 1.5;
72
72
  const COMPACTION_FREED_EPSILON = 256;
73
73
  const BATCH_RESPONSE_MAX_CHARS = 500;
74
+ const ORG_DISCLOSURE_MAX_CHARS = 600;
74
75
  function batchResponseDigest(result) {
75
76
  const content = result !== null && typeof result === "object" ? result.content : result;
76
77
  if (content === undefined || content === null)
@@ -3702,7 +3703,7 @@ export class Runner {
3702
3703
  }
3703
3704
  }
3704
3705
  if (checkpointVersionOf(cp) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
3705
- throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`);
3706
+ throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`, { reason: "version_newer" });
3706
3707
  }
3707
3708
  const preCasGateBit = cp.gate.kind === "irreversible_ask" ? cp.gate.realApproval : undefined;
3708
3709
  const preCasBitWellFormed = preCasGateBit !== undefined &&
@@ -3713,11 +3714,11 @@ export class Runner {
3713
3714
  if (checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? !preCasBitWellFormed : preCasGateBit !== undefined) {
3714
3715
  throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION
3715
3716
  ? `a v${checkpointVersionOf(cp)} checkpoint must carry a well-formed non-budgetable realApproval gate bit (origin org_rule/org_unavailable/policy) on an irreversible_ask gate — this row does not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
3716
- : `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`);
3717
+ : `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? "real_approval_damaged" : "real_approval_forged" });
3717
3718
  }
3718
3719
  if ((preCasGateBit?.origin === "org_rule" || preCasGateBit?.origin === "org_unavailable") &&
3719
3720
  this.deps.permissionRuleOrg === undefined) {
3720
- throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (${preCasGateBit.origin}) and this worker has no org adjudication wiring (permissionRuleOrg) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`);
3721
+ throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (${preCasGateBit.origin}) and this worker has no org adjudication wiring (permissionRuleOrg) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`, { reason: "governed_unwired" });
3721
3722
  }
3722
3723
  const retiredWalltimeTotal = cp.resourceLedger?.totalWalltimeSec;
3723
3724
  if (retiredWalltimeTotal !== undefined) {
@@ -3729,7 +3730,7 @@ export class Runner {
3729
3730
  throw new CheckpointError("checkpoint.resume_aborted", "the resume was handed an ALREADY-ABORTED signal — refusing to consume the approval on a leg that cannot run it (the checkpoint stays pending and is resumable with a live signal)");
3730
3731
  }
3731
3732
  if (cp.state.workspaceHandle !== undefined && this.deps.executionEnvFactory === undefined) {
3732
- throw new CheckpointError("checkpoint.unsupported_version", "checkpoint has a remote workspaceHandle but no RunnerDeps.executionEnvFactory is wired to rebuild the env");
3733
+ throw new CheckpointError("checkpoint.unsupported_version", "checkpoint has a remote workspaceHandle but no RunnerDeps.executionEnvFactory is wired to rebuild the env", { reason: "env_factory_missing" });
3733
3734
  }
3734
3735
  if (cp.state.inheritedGate?.requiresParentConstraint === true) {
3735
3736
  const supplied = internals?.inheritedGate?.parentConstraints?.length ?? 0;
@@ -3745,6 +3746,10 @@ export class Runner {
3745
3746
  "re-supply would run the resumed leg under a different ancestor chain than it suspended with; " +
3746
3747
  "rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
3747
3748
  }
3749
+ if (checkpointVersionOf(cp) >= F012_CHECKPOINT_VERSION &&
3750
+ (cp.state.inheritedGate.constraintChain === undefined || cp.state.inheritedGate.constraintDigest === undefined)) {
3751
+ throw new CheckpointError("checkpoint.invalid_outcome", `a v${checkpointVersionOf(cp)} checkpoint that requires parent constraints must carry BOTH the frozen constraintChain and its constraintDigest (they are minted in one write with the version stamp) — this row carries neither or only one; refusing to fall back to the count-only contract on a damaged row (corruption / downgrade guard), the checkpoint stays pending`, { reason: "constraint_chain_missing" });
3752
+ }
3748
3753
  const expectedDigest = cp.state.inheritedGate.constraintDigest;
3749
3754
  if (expectedDigest !== undefined) {
3750
3755
  const persistedChain = cp.state.inheritedGate.constraintChain;
@@ -4037,16 +4042,28 @@ export class Runner {
4037
4042
  const orgVerdict = prepared.permissionRuleOrg
4038
4043
  .adjudicate({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId })
4039
4044
  .catch(() => ({ status: "unavailable", disclosures: ["the org adjudication face threw on resume"] }));
4040
- const org = await settleOrgVerdictWithin(orgVerdict, { status: "unavailable", disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`] }, { signal: prepared.abortController.signal, timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
4045
+ let orgWaitCancelled = false;
4046
+ const org = await settleOrgVerdictWithin(orgVerdict, { status: "unavailable", disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`] }, {
4047
+ signal: prepared.abortController.signal,
4048
+ timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS,
4049
+ onFallback: (cause) => {
4050
+ orgWaitCancelled = cause === "aborted";
4051
+ },
4052
+ });
4041
4053
  const blocked = org.status === "unavailable"
4042
4054
  ? gateRealApproval?.origin === "org_unavailable"
4043
4055
  ? undefined
4044
- : "this deployment is org-governed and cannot currently adjudicate against an organization policy snapshot"
4056
+ : orgWaitCancelled
4057
+ ? "this deployment is org-governed and the task was cancelled before the organization policy snapshot could be adjudicated"
4058
+ : "this deployment is org-governed and cannot currently adjudicate against an organization policy snapshot"
4045
4059
  : org.verdict?.behavior === "deny"
4046
4060
  ? `an organization policy rule (${org.verdict.rule}) denies it`
4047
4061
  : undefined;
4048
4062
  if (blocked !== undefined) {
4049
- const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}. This approval is spent — the call has to be re-issued and approved again once organization policy permits it.`);
4063
+ const orgDisclosures = org.status === "unavailable" && org.disclosures.length > 0
4064
+ ? ` Governance disclosures: ${inlineUntrusted(org.disclosures.join("; "), ORG_DISCLOSURE_MAX_CHARS)}.`
4065
+ : "";
4066
+ const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}.${orgDisclosures} This approval is spent — the call has to be re-issued and approved again once organization policy permits it.`);
4050
4067
  emitEnd(true, { content: orgDenial });
4051
4068
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, orgDenial, true));
4052
4069
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
@@ -12,5 +12,11 @@ import { type ContractAssertionRunner } from "./contract-harness.js";
12
12
  * with a narrower native key space owns an injective encoding, not a rejection). Before 2.1.0 the
13
13
  * bundled backends genuinely diverged on both halves (dev-green / deployment-red), so running this
14
14
  * kit against a pre-2.1.0-modeled backend is expected to go red on those two entries.
15
+ *
16
+ * Backlog #119 adds the provenance leg: `put`'s third argument and the `ownerOf` read face. `ownerOf`
17
+ * is typed OPTIONAL on the interface (source compatibility for a backend written against an older
18
+ * engine), but this kit REQUIRES it — a backend that cannot say who owns a ref cannot authorize a
19
+ * host-side read face, and an optional parameter one store honors while another drops it silently is
20
+ * exactly the divergence the two legs above exist to prevent.
15
21
  */
16
22
  export declare function toolResultStoreContract(make: () => ToolResultStore, runAssertion?: ContractAssertionRunner): Promise<void>;
@@ -31,5 +31,29 @@ export async function toolResultStoreContract(make, runAssertion) {
31
31
  }
32
32
  assert.equal((await store.get("tr_sess_call:1")).content, "payload-tr_sess_call:1");
33
33
  });
34
+ run("#119 provenance: content+owner are one write; ownerOf answers; a different owner is a TYPED refusal; unowned rows stay unowned", async () => {
35
+ const store = make();
36
+ assert.equal(typeof store.ownerOf, "function", "a backend must implement ownerOf — a store that cannot say who owns a ref cannot back a host read face");
37
+ const ownerOf = (ref) => Promise.resolve(store.ownerOf(ref));
38
+ const ref = "tr_s1~c1";
39
+ await store.put(ref, "0123456789", { sessionId: "sess-A", taskId: "task-1" });
40
+ assert.deepEqual(await ownerOf(ref), { sessionId: "sess-A", taskId: "task-1" }, "the winning write's owner round-trips");
41
+ await store.put(ref, "IGNORED", { sessionId: "sess-A", taskId: "task-1" });
42
+ assert.equal((await store.get(ref)).content, "0123456789");
43
+ assert.deepEqual(await ownerOf(ref), { sessionId: "sess-A", taskId: "task-1" });
44
+ await store.put(ref, "IGNORED");
45
+ assert.deepEqual(await ownerOf(ref), { sessionId: "sess-A", taskId: "task-1" }, "an ownerless put must not clear the owner");
46
+ for (const other of [{ sessionId: "sess-B" }, { sessionId: "sess-A", taskId: "task-2" }]) {
47
+ await assert.rejects((async () => store.put(ref, "other tenant's bytes", other))(), (err) => err.code === "tool_result.ref_conflict", `put(${JSON.stringify(other)}) on an occupied ref must reject with code tool_result.ref_conflict`);
48
+ }
49
+ assert.equal((await store.get(ref)).content, "0123456789", "a refused put must not have overwritten anything");
50
+ const unowned = "tr_no_owner~c";
51
+ await store.put(unowned, "bytes from a write site that stated no owner");
52
+ assert.equal(await ownerOf(unowned), undefined, "a row stored without provenance is UNOWNED");
53
+ await store.put(unowned, "bytes from a write site that stated no owner", { sessionId: "sess-A" });
54
+ assert.equal(await ownerOf(unowned), undefined, "put must not back-fill an owner onto an unowned row");
55
+ assert.equal((await store.get(unowned)).content, "bytes from a write site that stated no owner");
56
+ assert.equal(await ownerOf("tr_never~written"), undefined);
57
+ });
34
58
  await settle();
35
59
  }
@@ -6,7 +6,7 @@ import { shutdownDebug } from "./shutdown-debug.js";
6
6
  import { delimitUntrusted } from "./untrusted-text.js";
7
7
  import { boundedRedactedSummary } from "./untrusted-egress.js";
8
8
  import { mintCompletionId, commitCompletionIdIfEmpty, clipTaskOutput, assertOwnership, sleepPollStep, alreadyTerminalStopNote, canAccess, normalizeAgentName, closestName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR, } from "./task-registry-shared.js";
9
- import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
9
+ import { buildToolResultRef, OFFLOAD_TOOL_NAME, toolResultProvenanceOf } from "./tool-result-store.js";
10
10
  export function ensureDurableHeartbeatLane(core) {
11
11
  if (core.durableHeartbeatTimer !== undefined)
12
12
  return;
@@ -1232,8 +1232,8 @@ export async function spillClippedAgentResult(handle, full, clipped, store, sess
1232
1232
  if (store === undefined)
1233
1233
  return clipped;
1234
1234
  if (handle.spillRef === undefined) {
1235
- const ref = buildToolResultRef(sessionId ?? "no-session", `${handle.id}_c${handle.reviveCycle ?? 0}`);
1236
- await store.put(ref, full);
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
1237
  handle.spillRef = ref;
1238
1238
  }
1239
1239
  return `${clipped}\n\n[full output persisted — call ${OFFLOAD_TOOL_NAME} with ref "${handle.spillRef}" to read it back.]`;
@@ -1,6 +1,6 @@
1
1
  import { delimitUntrusted } from "./untrusted-text.js";
2
2
  import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP, MONITOR_SPILL_CAP_CHARS, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, clipMonitorLine, terminalTaskSummary, accountDroppedBytes, renderSpoolBody, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
3
- import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
3
+ import { buildToolResultRef, OFFLOAD_TOOL_NAME, toolResultProvenanceOf } from "./tool-result-store.js";
4
4
  export function registerMonitorLane(core, input) {
5
5
  assertOwnership(input, "registerMonitor");
6
6
  const id = core.mintTaskId("monitor");
@@ -61,14 +61,15 @@ function spillRolledMonitorChunk(handle, stream, dropped) {
61
61
  return;
62
62
  }
63
63
  const n = stream === "out" ? (handle.spillSegCount ?? 0) : (handle.spillErrSegCount ?? 0);
64
- const ref = buildToolResultRef(handle.spillSessionId ?? "no-session", `${handle.id}_${stream}_seg${n}`);
64
+ const ref = buildToolResultRef(handle.spillSessionId ?? "no-session", handle.id, `${stream}_seg${n}`);
65
65
  handle.spillCharsUsed = used + dropped.length;
66
66
  if (stream === "out")
67
67
  handle.spillSegCount = n + 1;
68
68
  else
69
69
  handle.spillErrSegCount = n + 1;
70
70
  try {
71
- void Promise.resolve(store.put(ref, dropped)).catch(() => {
71
+ const provenance = handle.spillSessionId === undefined ? undefined : toolResultProvenanceOf(handle.spillSessionId, handle.id);
72
+ void Promise.resolve(store.put(ref, dropped, provenance)).catch(() => {
72
73
  handle.spillFailed = true;
73
74
  });
74
75
  }
@@ -83,8 +84,8 @@ function monitorSpillNote(handle) {
83
84
  return "";
84
85
  const sid = handle.spillSessionId ?? "no-session";
85
86
  const segLabel = (stream, n) => {
86
- const first = buildToolResultRef(sid, `${handle.id}_${stream}_seg0`);
87
- return n <= 1 ? `ref "${first}"` : `refs "${first}" .. "${buildToolResultRef(sid, `${handle.id}_${stream}_seg${n - 1}`)}"`;
87
+ const first = buildToolResultRef(sid, handle.id, `${stream}_seg0`);
88
+ return n <= 1 ? `ref "${first}"` : `refs "${first}" .. "${buildToolResultRef(sid, handle.id, `${stream}_seg${n - 1}`)}"`;
88
89
  };
89
90
  const clauses = [];
90
91
  if (outN > 0)
@@ -167,6 +167,13 @@ export type PermissionResult = {
167
167
  message?: string;
168
168
  decisionReason?: DecisionReason;
169
169
  requiresRealApproval?: boolean;
170
+ /** #144 disclosure (additive): a persisted allow rule MATCHED this call but could not clear the
171
+ * ask, because the ask is MANDATED (operator shellGate:"always", or the tool's own
172
+ * egress/irreversibility marks) rather than a classifier's hesitation — "allow rules silence
173
+ * the classifier's questions, never a mandated one". Carries the matched rule text so a
174
+ * consumer (approval card, wire frame) can tell the person their rule is alive, just outranked.
175
+ * Absent ⇒ no rule matched, or the ask was cleared normally. */
176
+ persistedRuleShadowed?: string;
170
177
  } | {
171
178
  action: "deny";
172
179
  updatedInput?: unknown;
@@ -601,6 +608,10 @@ export interface AskDelegationProvenance {
601
608
  /** The structured context an `onAsk` approver receives for an `ask` decision (design/37). */
602
609
  export interface AskRequest {
603
610
  toolName: string;
611
+ /** #144: a persisted allow rule MATCHED this call but could not clear the ask (mandated — see
612
+ * {@link PermissionResult}'s ask arm). The matched rule text, so the approval card renders "your
613
+ * rule is alive, just outranked" instead of leaving the person to regex the message prose. */
614
+ persistedRuleShadowed?: string;
604
615
  toolCallId: string;
605
616
  /** The (post-rewrite) args the tool would run with. */
606
617
  args: unknown;
@@ -10,7 +10,7 @@ import { type ToolResultStore } from "./tool-result-store.js";
10
10
  * **Simpler than CC by construction (§24.2):** a request-only, non-destructive transform (returns a new
11
11
  * array; the durable session keeps full results) applied in the existing `harness.on("context")` hook —
12
12
  * the same per-query point as `clearStaleToolResults` (the mirror of CC's query.ts:379). It is DETERMINISTIC
13
- * (stable `ref = tr_<sessionId>_<toolCallId>` + deterministic {@link buildPreview}), so re-running it every
13
+ * (stable `ref = tr_<sessionId>~<toolCallId>` + deterministic {@link buildPreview}), so re-running it every
14
14
  * query yields byte-identical previews → prompt-cache safe **without** CC's ContentReplacementState freeze
15
15
  * machine (the determinism IS the freeze), and resume-free (the transcript holds originals; this re-applies
16
16
  * on replay). Reuses design/30's offload store + preview format; with no store it falls back to a
@@ -1,5 +1,5 @@
1
1
  import { isToolResult } from "./message-utils.js";
2
- import { buildPreview, buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX } from "./tool-result-store.js";
2
+ import { buildPreview, buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX, toolResultContentSegment, toolResultProvenanceOf, } from "./tool-result-store.js";
3
3
  export const AGGREGATE_TOOL_RESULT_BUDGET_CHARS = 200_000;
4
4
  const HEAD = 1_000;
5
5
  const TAIL = 1_000;
@@ -61,13 +61,13 @@ export async function capAggregateToolResults(messages, opts) {
61
61
  const previewOne = async (k, before, head, tail, fromOriginal) => {
62
62
  const m = out[k];
63
63
  const full = textOf((fromOriginal ? messages[k] : m).content);
64
- const ref = buildToolResultRef(opts.sessionId, m.toolCallId ?? `idx${k}`);
64
+ const ref = buildToolResultRef(opts.sessionId, m.toolCallId ?? `idx${k}`, toolResultContentSegment(full));
65
65
  const sizes = head === HEAD && tail === TAIL ? undefined : { head, tail };
66
66
  let previewText;
67
67
  let storeFallback;
68
68
  if (opts.store) {
69
69
  try {
70
- await opts.store.put(ref, full);
70
+ await opts.store.put(ref, full, toolResultProvenanceOf(opts.sessionId));
71
71
  previewText = buildPreview(full, ref, sizes);
72
72
  storeFallback = false;
73
73
  }