@sema-agent/core 7.31.0 → 7.32.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 (99) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/dist/agents/inherited-tool-pool.d.ts +28 -0
  3. package/dist/agents/inherited-tool-pool.js +15 -0
  4. package/dist/agents/list-agents-tool.d.ts +7 -0
  5. package/dist/agents/list-agents-tool.js +9 -2
  6. package/dist/agents/peer-session-drain.d.ts +2 -0
  7. package/dist/agents/peer-session-drain.js +1 -0
  8. package/dist/agents/send-message-tool.d.ts +17 -0
  9. package/dist/agents/send-message-tool.js +11 -4
  10. package/dist/agents/subagent.d.ts +10 -1
  11. package/dist/agents/subagent.js +24 -8
  12. package/dist/core/ask-origin.d.ts +15 -6
  13. package/dist/core/ask-origin.js +15 -9
  14. package/dist/core/background-shell.d.ts +45 -0
  15. package/dist/core/background-shell.js +13 -0
  16. package/dist/core/checkpoint-store.d.ts +5 -0
  17. package/dist/core/checkpoint-store.js +3 -2
  18. package/dist/core/engine-notice.d.ts +9 -0
  19. package/dist/core/gate-lanes.js +1 -1
  20. package/dist/core/governance-codes.d.ts +1 -1
  21. package/dist/core/governance-codes.js +5 -0
  22. package/dist/core/hooks.d.ts +2 -0
  23. package/dist/core/memory-engine/engine.d.ts +10 -1
  24. package/dist/core/memory-engine/engine.js +30 -6
  25. package/dist/core/memory-engine/file-backend.d.ts +15 -0
  26. package/dist/core/memory-engine/file-backend.js +53 -2
  27. package/dist/core/memory-engine/layout.d.ts +29 -4
  28. package/dist/core/memory-engine/layout.js +24 -4
  29. package/dist/core/read-root-grant-screen.d.ts +23 -0
  30. package/dist/core/read-root-grant-screen.js +17 -0
  31. package/dist/core/runner/contracts.d.ts +1 -1
  32. package/dist/core/runner/prepare-artifact.d.ts +12 -7
  33. package/dist/core/runner/prepare-artifact.js +10 -8
  34. package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
  35. package/dist/core/runner/prepare-ask-lane.js +5 -1
  36. package/dist/core/runner/prepare-boundary-parks.js +5 -5
  37. package/dist/core/runner/prepare-delegation-surface.d.ts +18 -0
  38. package/dist/core/runner/prepare-delegation-surface.js +6 -25
  39. package/dist/core/runner/prepare-hands-readface.d.ts +12 -89
  40. package/dist/core/runner/prepare-hands-readface.js +14 -48
  41. package/dist/core/runner/prepare-inherited-gate.d.ts +3 -1
  42. package/dist/core/runner/prepare-inherited-gate.js +2 -1
  43. package/dist/core/runner/prepare-park-ask.js +6 -4
  44. package/dist/core/runner/prepare-policy-chain.js +4 -0
  45. package/dist/core/runner/prepare-task.js +5 -3
  46. package/dist/core/runner/read-root-grant.d.ts +66 -0
  47. package/dist/core/runner/read-root-grant.js +77 -0
  48. package/dist/core/runner/run-notification-lane.d.ts +1 -1
  49. package/dist/core/runner/run-settle-and-teardown.js +3 -9
  50. package/dist/core/runner/runtask.js +1 -1
  51. package/dist/core/runner/send-message-continuation.d.ts +45 -0
  52. package/dist/core/runner/send-message-continuation.js +27 -0
  53. package/dist/core/runner/session-read-file-states.d.ts +91 -0
  54. package/dist/core/runner/session-read-file-states.js +43 -0
  55. package/dist/core/secret-env.d.ts +1 -1
  56. package/dist/core/secret-env.js +7 -2
  57. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
  58. package/dist/core/store-contracts/checkpoint-store-contract.js +19 -0
  59. package/dist/core/task-registry-monitor.js +3 -0
  60. package/dist/core/task-registry-shared.d.ts +16 -3
  61. package/dist/core/task-registry.d.ts +65 -29
  62. package/dist/core/task-registry.js +78 -28
  63. package/dist/core/task-spec.d.ts +5 -3
  64. package/dist/core/tool-policy.d.ts +23 -0
  65. package/dist/core/tool-policy.js +15 -4
  66. package/dist/core/tool-spec.d.ts +2 -0
  67. package/dist/engine/execution-env/node-execution-env.d.ts +13 -3
  68. package/dist/engine/execution-env/node-execution-env.js +26 -0
  69. package/dist/index.d.ts +4 -3
  70. package/dist/index.js +2 -1
  71. package/dist/scenarios/full-body.d.ts +5 -2
  72. package/dist/scenarios/full-body.js +14 -7
  73. package/dist/tools/fs/fs-bash-background-text.d.ts +16 -0
  74. package/dist/tools/fs/fs-bash-background-text.js +22 -0
  75. package/dist/tools/fs/fs-bash.d.ts +6 -0
  76. package/dist/tools/fs/fs-bash.js +9 -27
  77. package/dist/tools/fs/fs-read.d.ts +2 -2
  78. package/dist/tools/fs/fs-search-tools.d.ts +3 -2
  79. package/dist/tools/fs/index.d.ts +8 -1
  80. package/dist/tools/fs/index.js +2 -3
  81. package/dist/tools/fs/repo-map.d.ts +2 -1
  82. package/dist/tools/fs/safety.d.ts +3 -1
  83. package/dist/tools/fs/safety.js +2 -1
  84. package/dist/tools/fs/shell-read-check.d.ts +3 -0
  85. package/dist/tools/fs/shell-read-check.js +1 -1
  86. package/dist/tools/fs/shell-read-establish.d.ts +3 -1
  87. package/dist/tools/fs/shell-read-establish.js +2 -2
  88. package/dist/tools/fs/shell-read-plan.d.ts +2 -2
  89. package/dist/tools/fs/shell-read-plan.js +15 -10
  90. package/dist/tools/fs/shell-read-word.d.ts +5 -4
  91. package/dist/tools/fs/shell-read-word.js +3 -2
  92. package/dist/tools/monitor.d.ts +5 -4
  93. package/dist/tools/monitor.js +5 -4
  94. package/dist/tools/todo.d.ts +3 -3
  95. package/dist/tools/todo.js +5 -4
  96. package/package.json +1 -1
  97. package/test/export-surface.snapshot.json +13 -1
  98. package/wire-changes/core@7.31.1.json +189 -0
  99. package/wire-changes/core@7.32.0.json +850 -0
@@ -44,7 +44,7 @@ import type { DelegationContentSafety } from "../memory-engine/delegation-proven
44
44
  import { type SessionPermissionRules } from "../session-policy-store.js";
45
45
  import type { SessionStore } from "../session.js";
46
46
  import { type OnAsk } from "../tool-policy.js";
47
- import type { ApproverPosture, RunnerDeps, RuntimeCaps, ShellGateDoctrine, TaskEvent, TaskSpec, ToolExecuteContext } from "../types.js";
47
+ import type { ApproverPosture, RunnerDeps, RuntimeCaps, ShellGateDoctrine, TaskEvent, TaskSpec, ToolExecuteContext, ToolSpec } from "../types.js";
48
48
  import type { ActiveSkillScope } from "./active-skill-scope.js";
49
49
  import type { InheritedGate, Prepared, PrepareResume, RunInternals, StopForDenialLimit, ToolFaceSnapshot } from "./contracts.js";
50
50
  export interface PrepareInheritedGateInput {
@@ -89,6 +89,8 @@ export interface PrepareInheritedGateInput {
89
89
  /** borrowed-readonly — the frozen tool-face snapshot (the ctx forwards exclude / defer / alwaysLoad / restoreGated; the
90
90
  * full-shell predicate reads the exclusion of Bash). */
91
91
  toolFaceSnapshot: ToolFaceSnapshot;
92
+ /** borrowed-readonly — the ctx's `mountsCallerTool` seat: identity membership in the caller's `spec.tools`, pre-gate. */
93
+ mountsCallerTool: (tool: ToolSpec) => boolean;
92
94
  /** borrowed-readonly — the resolved prompt profile (ctx seat). */
93
95
  promptProfile: ToolExecuteContext["promptProfile"];
94
96
  /** borrowed-readonly — the resolved interaction posture (ctx seat; absent ⇒ no key). */
@@ -8,7 +8,7 @@ import { askApproverIdentity, durableMandateSourceOf } from "../tool-policy.js";
8
8
  import { attachRebuiltDenialTrackers, createDenialLimitStop } from "./denial-limit-arms.js";
9
9
  import { asThrownError } from "../thrown-value.js";
10
10
  export function prepareInheritedGate(input) {
11
- const { spec, deps, internals, resume, sessions, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, ownedEnv, lockedPreflight, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, autoModeSeat, approverPostureSeat, handsEnabled, reportUsage, harnessRef, skillScope, forwardEvent, subagentRetain, worktreeIsolation, requestReview, requestStopAfterTurn, enterPlanMode, reminderMark, reminderDisclosureCounts, fileHistoryEnabled, historyScope, historyRoot, historyFs, abortRun } = input;
11
+ const { spec, deps, internals, resume, sessions, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, ownedEnv, lockedPreflight, toolFaceSnapshot, mountsCallerTool, promptProfile, resolvedInteractionPosture, autoModeSeat, approverPostureSeat, handsEnabled, reportUsage, harnessRef, skillScope, forwardEvent, subagentRetain, worktreeIsolation, requestReview, requestStopAfterTurn, enterPlanMode, reminderMark, reminderDisclosureCounts, fileHistoryEnabled, historyScope, historyRoot, historyFs, abortRun } = input;
12
12
  const liveInheritedGate = internals?.inheritedGate;
13
13
  const seedInheritedGate = resume?.seed.inheritedGate;
14
14
  const inheritedAncestorRules = (() => {
@@ -205,6 +205,7 @@ export function prepareInheritedGate(input) {
205
205
  ...(readDenyAdditionsNormalized.length > 0 ? { readDenyPatterns: Object.freeze(readDenyAdditionsNormalized.map((e) => ({ ...e }))) } : {}),
206
206
  ...(spec.interactiveTools === false ? { interactiveTools: false } : {}),
207
207
  oneShot: spec.oneShot,
208
+ mountsCallerTool,
208
209
  parentSpecSeat,
209
210
  ...(internals?.questionFaceStripped === true ? { questionFaceStripped: true } : {}),
210
211
  excludeTools: toolFaceSnapshot.exclude,
@@ -1,7 +1,7 @@
1
1
  import { uuidv7 } from "../../engine/session/uuid.js";
2
2
  import { Value } from "typebox/value";
3
3
  import { ASK_USER_QUESTION_TOOL_NAME, classifyQuestionOutcome, validateAskQuestions } from "../ask-question.js";
4
- import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
4
+ import { hasBackgroundShell, sweepBackgroundShells, sweepParkOnlyBackgroundShells } from "../background-shell.js";
5
5
  import { boundInputHashOf } from "../canonical-json.js";
6
6
  import { buildRiskDescriptor, checkpointVersionFor, debitLedger, encodeAtFidelity, mintCheckpointId, mintCheckpointToken, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
7
7
  import { pauseOf } from "../pause-registry.js";
@@ -229,8 +229,8 @@ export function prepareParkAsk(input) {
229
229
  }
230
230
  }
231
231
  else if (parkOnlyRemoteEnv !== undefined) {
232
- if (hasBackgroundShell(parkOnlyRemoteEnv) && spec.retainBackgroundProcesses !== true)
233
- await sweepBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry);
232
+ if (hasBackgroundShell(parkOnlyRemoteEnv))
233
+ await sweepParkOnlyBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry, { task: spec.retainBackgroundProcesses === true });
234
234
  remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
235
235
  }
236
236
  token = mintCheckpointToken();
@@ -277,8 +277,10 @@ export function prepareParkAsk(input) {
277
277
  }
278
278
  const approvalLedger = debitLedger(priorLedger, liveSpendRef.get?.() ?? { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: 0 }, resourceTotal, { countSlice: false });
279
279
  const rowMatchedAskRule = matchedAskRule ?? storedShapeFacts?.matchedAskRule;
280
+ const storedShapeMandated = askMandateMember(storedShapeFacts).mandated === true;
280
281
  const clearanceFacts = {
281
- ...(carry?.mandated === true || askMandateMember(storedShapeFacts).mandated === true ? { mandated: true } : {}),
282
+ ...(carry?.mandated === true || storedShapeMandated ? { mandated: true } : {}),
283
+ ...(carry?.mandate !== undefined && !storedShapeMandated ? { mandate: carry.mandate } : {}),
282
284
  ...(rowRealApproval !== undefined ? { requiresRealApproval: true } : {}),
283
285
  ...(shadowedRule !== undefined ? { persistedRuleShadowed: shadowedRule } : {}),
284
286
  ...(askDecisionReason !== undefined ? { decisionReason: askDecisionReason } : {}),
@@ -13,6 +13,7 @@ import { createActiveSkillScopePolicy } from "./active-skill-scope.js";
13
13
  import { askMandateMember, probeMandatedAsk } from "../ask-origin.js";
14
14
  const ancestorFacts = (ask) => ({ ...(ask ?? {}), ancestorResolved: true });
15
15
  import { headlessDenyAtFold, headlessDenyAtRecheck, inheritedAskCarry, judgeInheritedClassifier, lateAskSettlementObserver, settleDenialLimitFallback } from "./denial-limit-arms.js";
16
+ import { settleReadRootGrant } from "./read-root-grant.js";
16
17
  import { askGrantShapeOf } from "./inherited-ask-grants.js";
17
18
  import { createPermissionRuleLanes, inheritedAskRuleEvidence, createRuleOffersOf } from "./permission-rule-lanes.js";
18
19
  import { createLiveSessionRules, createSessionRulePolicy } from "./session-rule-policy.js";
@@ -360,6 +361,7 @@ export async function preparePolicyChain(input) {
360
361
  ...inheritedAskCarry(judged, isLiveApproverSeat(onAskOf), ancestorTracker),
361
362
  ruleEvidence: inheritedAskEvidence,
362
363
  }, onAskOf, csignal ?? abortController.signal, lateAskSettlementObserver({ toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
364
+ void settleReadRootGrant(rr.readRootGrant, undefined, { toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice });
363
365
  settleDenialLimitFallback({ fallback: editFallback, mintedHere: editMintedHere, tracker: ancestorTracker, resolved: rr, headless: headlessDenyAtRecheck, stop: stopForDenialLimit, toolName: creq.toolName, toolCallId: creq.toolCallId });
364
366
  if (rr.action !== "allow")
365
367
  return rr;
@@ -436,6 +438,7 @@ export async function preparePolicyChain(input) {
436
438
  ...inheritedAskCarry(judged, isLiveApproverSeat(pc.onAsk), pc.autoMode?.denialTracking),
437
439
  ruleEvidence: inheritedAskEvidence,
438
440
  }, pc.onAsk, csignal ?? abortController.signal, lateAskSettlementObserver({ toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
441
+ void settleReadRootGrant(resolved.readRootGrant, undefined, { toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice });
439
442
  const askWaitMs = Math.max(0, now() - askT0);
440
443
  if (resolved.action === "deny" && resolved.approverUnavailable === true) {
441
444
  if (markInheritedUnavailable(creq.toolCallId))
@@ -506,6 +509,7 @@ export async function preparePolicyChain(input) {
506
509
  ...inheritedAskCarry(judged, isLiveApproverSeat(pc.onAsk), pc.autoMode?.denialTracking),
507
510
  ruleEvidence: inheritedAskEvidence,
508
511
  }, pc.onAsk, csignal ?? abortController.signal, lateAskSettlementObserver({ toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
512
+ void settleReadRootGrant(resolved.readRootGrant, undefined, { toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice });
509
513
  const askWaitMs = Math.max(0, now() - askT0);
510
514
  if (resolved.action === "deny" && resolved.approverUnavailable === true) {
511
515
  if (markInheritedUnavailable(creq.toolCallId))
@@ -7,6 +7,7 @@ import { uuidv7 } from "../../engine/session/uuid.js";
7
7
  import { brainToRuntime } from "../runtime.js";
8
8
  import { hasSessionFork } from "../session.js";
9
9
  import { SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
10
+ import { callerToolMembership } from "../../agents/inherited-tool-pool.js";
10
11
  import { loadAnnounceOnceLedger } from "./announce-once-ledger.js";
11
12
  import { isValidReminderMark, mintReminderMark } from "../reminder-mint.js";
12
13
  import { refuseInvalidApproverPosture } from "../auto-mode-directive.js";
@@ -271,6 +272,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
271
272
  const runId = uuidv7();
272
273
  if (runIdSink !== undefined)
273
274
  runIdSink.runId = runId;
275
+ const mountsCallerTool = callerToolMembership(spec.tools);
274
276
  const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
275
277
  spec = doors.spec;
276
278
  const checkpointStore = resolveCheckpointStore(spec, deps), checkpointSeat = checkpointSeatOf(spec, checkpointStore);
@@ -390,7 +392,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
390
392
  let readDenyAdditionsNormalized = [];
391
393
  let handsLessResolvedFace;
392
394
  let handsLessResolvedWriteFace;
393
- const { liveInheritedGate, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, inheritedParentConstraints, autoModeIntent, approverPosture: approverPostureResolved, inheritedAdmittedOrgScopes, priorOwnOrgVerdict, orgGovernedProvenance, effectiveShellGate, ownSessionRulesRef, ancestorSessionRulesRef, ancestorRulesNow, memoryAdmittedOrgScopesRef, ownOrgVerdictRef, orgAdmissionCheckpointState, faceCheckpointSection, faceCheckpointState, writeFaceCheckpointSection, f012CheckpointState, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, frozenOnQuestion, parentSpecSeat, provenanceForChildrenRef, delegationSettlementRef, inheritedGateForChildren, carrierReadFace, carrierWriteFace, fullShellReachable, gateStopRef, stopForDenialLimit, enrichSpecToolCtx } = prepareInheritedGate({ spec, deps, internals, resume, sessions, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, ownedEnv, lockedPreflight, checkpointSeat, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, autoModeSeat, approverPostureSeat, handsEnabled, reportUsage, harnessRef, skillScope, forwardEvent, subagentRetain, worktreeIsolation, requestReview, requestStopAfterTurn, enterPlanMode, reminderMark, reminderDisclosureCounts, fileHistoryEnabled, historyScope, historyRoot, historyFs, abortRun: () => { abortController.abort(); void harness.abort(); }, runtimeCaps: () => runtimeCaps, hooks: () => hooks, preToolUseObservational: () => preToolUseObservational, hookTimeoutMs: () => hookTimeoutMs, hookEnvFace: () => hookEnvFace, autoModeDecider: () => autoModeDecider, autoModeDenialTracking: () => autoModeDenialTracking, autoModeArming: () => autoModeArming, agentForkDenial: () => agentForkDenial, observersActive: () => observersActive, resolvedReadFace: () => resolvedReadFace, resolvedWriteFace: () => resolvedWriteFace, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized, handsLessResolvedFace: () => handsLessResolvedFace, handsLessResolvedWriteFace: () => handsLessResolvedWriteFace, centerAdoption: () => centerAdoptionRef.current, memoryEngineSession: () => memoryEngineSession });
395
+ const { liveInheritedGate, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, inheritedParentConstraints, autoModeIntent, approverPosture: approverPostureResolved, inheritedAdmittedOrgScopes, priorOwnOrgVerdict, orgGovernedProvenance, effectiveShellGate, ownSessionRulesRef, ancestorSessionRulesRef, ancestorRulesNow, memoryAdmittedOrgScopesRef, ownOrgVerdictRef, orgAdmissionCheckpointState, faceCheckpointSection, faceCheckpointState, writeFaceCheckpointSection, f012CheckpointState, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, frozenOnQuestion, parentSpecSeat, provenanceForChildrenRef, delegationSettlementRef, inheritedGateForChildren, carrierReadFace, carrierWriteFace, fullShellReachable, gateStopRef, stopForDenialLimit, enrichSpecToolCtx } = prepareInheritedGate({ spec, deps, internals, resume, sessions, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, ownedEnv, lockedPreflight, checkpointSeat, toolFaceSnapshot, mountsCallerTool, promptProfile, resolvedInteractionPosture, autoModeSeat, approverPostureSeat, handsEnabled, reportUsage, harnessRef, skillScope, forwardEvent, subagentRetain, worktreeIsolation, requestReview, requestStopAfterTurn, enterPlanMode, reminderMark, reminderDisclosureCounts, fileHistoryEnabled, historyScope, historyRoot, historyFs, abortRun: () => { abortController.abort(); void harness.abort(); }, runtimeCaps: () => runtimeCaps, hooks: () => hooks, preToolUseObservational: () => preToolUseObservational, hookTimeoutMs: () => hookTimeoutMs, hookEnvFace: () => hookEnvFace, autoModeDecider: () => autoModeDecider, autoModeDenialTracking: () => autoModeDenialTracking, autoModeArming: () => autoModeArming, agentForkDenial: () => agentForkDenial, observersActive: () => observersActive, resolvedReadFace: () => resolvedReadFace, resolvedWriteFace: () => resolvedWriteFace, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized, handsLessResolvedFace: () => handsLessResolvedFace, handsLessResolvedWriteFace: () => handsLessResolvedWriteFace, centerAdoption: () => centerAdoptionRef.current, memoryEngineSession: () => memoryEngineSession });
394
396
  const { roster, blockedRef, runtimeCaps, runtimeCapsFaulted, complianceDenies, complianceDegraded, agentForkDenial, observersActive, autoModeArmReason, autoModeDecider, autoModeDenialTracking, autoModeArming, selfOrchestrationActive, workflowToolsActive, workflowSizeGuideline } = await prepareCapsAndWorkflow({ spec, deps, internals, session, sessionId, runId, hostTaskId, taskScope, taskRootFinal, model, thinking, lockedPreflight, checkpointSeat, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, frozenOnAsk, parentSpecSeat, carrierReadFace, carrierWriteFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, autoModeIntent, peerLaneActive, peerSendMessageBuiltIn, runnerSelf, memoryEngineSession: () => memoryEngineSession, centerAdoption: () => centerAdoptionRef.current, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized });
395
397
  const tools = roster.tools;
396
398
  const pausedRef = {};
@@ -557,7 +559,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
557
559
  `(per-principal entitlement governance, not a misconfiguration — Agent(subagent_type:"fork") will refuse honestly)`), { phase: "config", sessionId });
558
560
  }
559
561
  const { lspDiagnostics, nudgeLspOnEdit, lspRunIdent, managerWired: lspManagerWired } = prepareLsp({ spec, deps, executionEnv, taskRootFinal, handsEnabled, sessionId, handsCwdRef, roster, envHandToolNames });
560
- prepareArtifact({ deps, runId, taskId: hostTaskId, sessionId, agentName: internals?.explicitAgentName, onAsk: frozenOnAsk, executionEnv, rootCanonical: attachmentRootCanonical, additionalRootsCanonical: [...additionalRootsCanonical, ...additionalReadRootsCanonical], readDenyMatcher, resolvedReadFace, handsCwdRef, roster, firstPartyOffload, envHandToolNames });
562
+ prepareArtifact({ deps, runId, taskId: hostTaskId, sessionId, agentName: internals?.explicitAgentName, onAsk: frozenOnAsk, executionEnv, rootCanonical: attachmentRootCanonical, additionalRootsCanonical: () => [...additionalRootsCanonical, ...additionalReadRootsCanonical], readDenyMatcher, resolvedReadFace, handsCwdRef, roster, firstPartyOffload, envHandToolNames, ...(handsReadFace.artifactStoreRef !== undefined ? { artifactStoreRef: handsReadFace.artifactStoreRef } : {}) });
561
563
  if (internals?.delegationProvenance !== undefined && resume !== undefined) {
562
564
  const live = internals.delegationProvenance.ref.current;
563
565
  const seed = resume.seed.delegationProvenance;
@@ -716,7 +718,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
716
718
  return undefined;
717
719
  });
718
720
  }
719
- const { askLane } = prepareAskLane({ gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash });
721
+ const { askLane } = prepareAskLane({ gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash, grantReadRoot: handsReadFace.grantReadRoot });
720
722
  const { saga } = prepareSuspendSaga({ gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, writeFaceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, undeliveredContinuationRef, inheritedParentConstraints, seedInheritedGate, ancestorRulesNow, inheritedShellGate, autoModeIntent, approverPosture: approverPostureResolved, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter });
721
723
  const { suspendForResource, suspendForPlatformLimit, suspendForReview } = prepareBoundaryParks({ saga, spec, checkpointStore, abortController, harness, session, sessions, sessionId, deps, priorLedger, maxSlices, maxSuspends, resourceTotal, priorSuspendCount, suspendChainBase, humanReviewRef, liveSpendRef, now, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, pausedRef, remoteEnvFailures, resourceSuspendEligible, durableSuspendInfraReady, incompleteSuspendAdapter });
722
724
  const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef });
@@ -0,0 +1,66 @@
1
+ /** The run's IN-FLIGHT read-root grant: the one place a screened grant ({@link ReadRootGrantScreen}) widens this run's
2
+ * read directories (through the hands phase's {@link GrantReadRoot}, built by {@link readRootGrantOver}), the one
3
+ * composer of the two notices, and the home of the store-isolation rule both root admission points judge by (the
4
+ * Artifact phase's mount and the grant) — machinery both phases reach down for. */
5
+ import type { ExecutionEnv } from "../../internal/harness.js";
6
+ import type { ReadRootGrantCandidate } from "../types.js";
7
+ import type { ReadRootGrantScreen } from "../read-root-grant-screen.js";
8
+ import { type RunnerDeps } from "../types.js";
9
+ /**
10
+ * THE store-isolation rule, one judgment for every root admission point (the Artifact mount over the prepare-time
11
+ * roots, an in-flight read-root grant over the granted root): a root that CONTAINS the local artifact store, lies
12
+ * INSIDE it, or IS it hands the file tools the store's ledger and stored files. Returns the first such root, or
13
+ * undefined.
14
+ *
15
+ * `storePhysical` is the store's physical path on THIS process's filesystem (`physicalPathOfExisting`, resolved
16
+ * once per run by the Artifact phase); a root is the execution env's canonical key. The judgment touches no
17
+ * filesystem — the grant runs after the approver's wait under the call's cut, and a synchronous lookup on a stalled
18
+ * store mount would block the event loop past that cut. The two are compared as identities only when both spell the
19
+ * same family (`within` reads both under the root's family); a root of the other family names a filesystem the store
20
+ * cannot be on, and no claim is made against it.
21
+ */
22
+ export declare function rootAdmittingArtifactStore(storePhysical: string, rootsCanonical: readonly string[]): string | undefined;
23
+ /** The run's cell holding the local artifact store a granted root must never admit (its physical path, as the
24
+ * Artifact phase resolved it): minted with the grant by the hands phase, written by the Artifact phase (the one
25
+ * reader of the hosting seat) once the declaration validates. */
26
+ export interface ArtifactStoreRef {
27
+ current: string | undefined;
28
+ }
29
+ /** Why the run's station applied no grant: the spelling did not resolve (or its lookup was cut), it no longer
30
+ * resolves to itself (a directory swapped for a link while the card waited), or it would admit the local artifact
31
+ * store (the store-isolation rule the Artifact phase judges its mount by). */
32
+ type GrantRefusal = "unresolvable" | "resolves_elsewhere" | "admits_artifact_store";
33
+ /** Widen THIS run's live read roots by one granted candidate: the offered spelling must still be its own canonical
34
+ * form and must not admit the local artifact store, then one deduplicated push; `root` = the spelling held. Nothing
35
+ * is pushed once `signal` has fired (a late lookup never widens). */
36
+ export type GrantReadRoot = (candidate: ReadRootGrantCandidate, signal: AbortSignal) => Promise<{
37
+ ok: true;
38
+ root: string;
39
+ } | {
40
+ ok: false;
41
+ reason: GrantRefusal;
42
+ cause: string;
43
+ }>;
44
+ /** The hands phase's {@link GrantReadRoot} over the run's live root arrays: the offered spelling must still be its own
45
+ * canonical form, must not admit the store in `artifactStore` ({@link rootAdmittingArtifactStore}), then ONE
46
+ * deduplicated push onto `readRoots` (the live read set every read face reads per call); the write roots are only
47
+ * consulted for the dedup, never widened. */
48
+ export declare function readRootGrantOver(seats: {
49
+ env: Pick<ExecutionEnv, "canonicalPath">;
50
+ writeRoots: readonly string[];
51
+ readRoots: string[];
52
+ artifactStore: ArtifactStoreRef;
53
+ }): GrantReadRoot;
54
+ /** Act on one screened grant: `granted` widens through `grant` (absent on a station that cannot widen — an inherited
55
+ * ask, a hands-less leg) and is announced; every other outcome changes nothing and is announced too — never silent.
56
+ * Returns a promise ONLY while a grant is applied, so an answer with no grant costs no extra event-loop turn. */
57
+ export declare function settleReadRootGrant(screen: ReadRootGrantScreen | undefined, grant: GrantReadRoot | undefined, at: {
58
+ toolName: string;
59
+ toolCallId: string;
60
+ sessionId: string;
61
+ runId: string;
62
+ taskId?: string;
63
+ onNotice: RunnerDeps["onNotice"];
64
+ signal?: AbortSignal;
65
+ }): Promise<void> | undefined;
66
+ export {};
@@ -0,0 +1,77 @@
1
+ import { deliverEngineNotice } from "../types.js";
2
+ import { READ_ROOT_CANDIDATE_DIR_MAX } from "../read-root-candidate.js";
3
+ import { DEFAULT_GATE_AWAIT_TIMEOUT_MS } from "../gate-bounded-await.js";
4
+ import { thrownText } from "../thrown-value.js";
5
+ import { pathFamilyOf, withinAnyRoot } from "../../tools/fs/safety.js";
6
+ export function rootAdmittingArtifactStore(storePhysical, rootsCanonical) {
7
+ const storeFamily = pathFamilyOf({ root: storePhysical });
8
+ return rootsCanonical.find((root) => pathFamilyOf({ root }) === storeFamily && (withinAnyRoot([root], storePhysical) || withinAnyRoot([storePhysical], root)));
9
+ }
10
+ export function readRootGrantOver(seats) {
11
+ const { env, writeRoots, readRoots, artifactStore } = seats;
12
+ return async (candidate, signal) => {
13
+ const c = await env.canonicalPath(candidate.dir);
14
+ if (signal.aborted)
15
+ return { ok: false, reason: "unresolvable", cause: "the lookup was cut" };
16
+ if (!c.ok)
17
+ return { ok: false, reason: "unresolvable", cause: `${c.error.code}: ${c.error.message}` };
18
+ if (c.value !== candidate.dir)
19
+ return { ok: false, reason: "resolves_elsewhere", cause: `it resolves to ${c.value}` };
20
+ const store = artifactStore.current;
21
+ if (store !== undefined && rootAdmittingArtifactStore(store, [c.value]) !== undefined)
22
+ return { ok: false, reason: "admits_artifact_store", cause: "the Artifact tool's ledger and stored files stay outside every root the file tools reach" };
23
+ if (!writeRoots.includes(c.value) && !readRoots.includes(c.value))
24
+ readRoots.push(c.value);
25
+ return { ok: true, root: c.value };
26
+ };
27
+ }
28
+ export function settleReadRootGrant(screen, grant, at) {
29
+ if (screen === undefined)
30
+ return undefined;
31
+ const ids = { toolName: at.toolName, toolCallId: at.toolCallId, sessionId: at.sessionId, runId: at.runId, ...(at.taskId !== undefined ? { taskId: at.taskId } : {}) };
32
+ const reject = (reason, offered, requested, why) => deliverEngineNotice(at.onNotice, {
33
+ code: "approval.read_root_grant_rejected",
34
+ message: `the approval for "${at.toolName}" asked to add a read directory, and nothing was added: ${why}. The approval's own verdict is unchanged.`,
35
+ detail: { reason, ...(offered !== undefined ? { offered } : {}), ...(requested !== undefined ? { requested: requested.slice(0, READ_ROOT_CANDIDATE_DIR_MAX) } : {}), ...ids },
36
+ });
37
+ if (screen.verdict === "rejected") {
38
+ const why = {
39
+ on_refusal: "a directory grant rides an approval, and this answer refused the call",
40
+ not_a_string: "the grant is not a string",
41
+ no_candidate: "this approval card offered no directory",
42
+ mismatch: "the grant is not the directory the card offered (it must be sent back exactly as offered)",
43
+ }[screen.reason];
44
+ reject(screen.reason, screen.offered, screen.requested, why);
45
+ return undefined;
46
+ }
47
+ const { candidate } = screen;
48
+ if (grant === undefined) {
49
+ reject("no_read_roots", candidate.dir, candidate.dir, "this run has no read directories to widen");
50
+ return undefined;
51
+ }
52
+ const cut = AbortSignal.any([...(at.signal !== undefined ? [at.signal] : []), AbortSignal.timeout(DEFAULT_GATE_AWAIT_TIMEOUT_MS)]);
53
+ const cutShort = new Promise((resolve) => {
54
+ const onCut = () => resolve({ ok: false, reason: "unresolvable", cause: at.signal?.aborted === true ? "the call ended before it resolved" : `it did not resolve within ${DEFAULT_GATE_AWAIT_TIMEOUT_MS}ms` });
55
+ if (cut.aborted)
56
+ onCut();
57
+ else
58
+ cut.addEventListener("abort", onCut, { once: true });
59
+ });
60
+ const attempt = grant(candidate, cut).catch((err) => ({ ok: false, reason: "unresolvable", cause: thrownText(err) }));
61
+ return Promise.race([attempt, cutShort]).then((applied) => {
62
+ if (!applied.ok) {
63
+ const why = {
64
+ unresolvable: `the directory could not be resolved (${applied.cause})`,
65
+ resolves_elsewhere: `the offered path no longer resolves to itself (${applied.cause})`,
66
+ admits_artifact_store: `the directory would admit the artifact store (${applied.cause})`,
67
+ }[applied.reason];
68
+ reject(applied.reason, candidate.dir, candidate.dir, why);
69
+ return;
70
+ }
71
+ deliverEngineNotice(at.onNotice, {
72
+ code: "approval.read_root_granted",
73
+ message: `the approval for "${at.toolName}" added ${candidate.covers === "exact" ? "the path" : "the directory"} ${candidate.dir} to this run's read directories (reads only; for the rest of this run).`,
74
+ detail: { dir: candidate.dir, ...(candidate.covers !== undefined ? { covers: candidate.covers } : {}), root: applied.root, ...ids },
75
+ });
76
+ });
77
+ }
@@ -3,7 +3,7 @@ import type { PushQueue } from "../push-queue.js";
3
3
  import type { SessionStore } from "../session.js";
4
4
  import { type NotificationInjectionOutcome, type PendingSessionNotifications, type SystemInjectionPriority, type TaskNotificationPayload } from "../task-notification.js";
5
5
  import type { RunnerDeps, TaskEvent, TaskSpec } from "../types.js";
6
- import type { SessionReadFileStates } from "./prepare-hands-readface.js";
6
+ import type { SessionReadFileStates } from "./session-read-file-states.js";
7
7
  import type { NotificationLaneBindings, Prepared, PrepareTaskFn, ResumeRun, RunInternals, RunnerDepsSeat, RunnerSelfSeat, TaskIdRef } from "./contracts.js";
8
8
  export interface RunNotificationLaneInput<R> {
9
9
  /** borrowed-readonly — the task spec: prepare's input, and the principal the peer scope is minted from. */
@@ -2,7 +2,7 @@ import { MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
2
2
  import { isSyntheticApiErrorMessage } from "../../internal/harness.js";
3
3
  import { adjudicateDerivedRoute, fallbackToPrimaryNotice, sameRouteIdentity } from "../../brain/route-adjudicator.js";
4
4
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
5
- import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
5
+ import { hasBackgroundShell, retainsBackgroundProcesses, sweepBackgroundShells } from "../background-shell.js";
6
6
  import { resolveCheckpointStore } from "../checkpoint-store.js";
7
7
  import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
8
8
  import { modelCostToPricing } from "../pricing.js";
@@ -315,16 +315,10 @@ export async function runSettleAndTeardown(input) {
315
315
  retainProcesses: spec.retainBackgroundProcesses === true,
316
316
  }), "taskRegistry.settleKilledForOwner", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
317
317
  const bgEnv = prepared.ownedEnv ?? runner.deps.executionEnv;
318
- const retainBgBySpec = spec.retainBackgroundProcesses === true && !ownedEnvDying;
318
+ const retainBgBySpec = retainsBackgroundProcesses({ task: spec.retainBackgroundProcesses === true, envDying: ownedEnvDying });
319
319
  if (bgEnv && hasBackgroundShell(bgEnv) && !retainBgBySpec) {
320
320
  try {
321
- const keepAlive = ownedEnvDying && bgEnv === prepared.ownedEnv
322
- ? []
323
- : [
324
- ...defaultTaskRegistry.sessionResidentShellIds(prepared.sessionId, bgEnv),
325
- ...defaultTaskRegistry.timeoutResidentShellIds(bgEnv),
326
- ...defaultTaskRegistry.retainedShellIds(bgEnv),
327
- ];
321
+ const keepAlive = ownedEnvDying && bgEnv === prepared.ownedEnv ? [] : defaultTaskRegistry.runningShellIds(bgEnv);
328
322
  await settleTeardownLeg(() => sweepBackgroundShells(bgEnv, defaultTaskRegistry, keepAlive.length > 0 ? { except: keepAlive } : undefined), "sweepBackgroundShells", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
329
323
  }
330
324
  catch (e) {
@@ -15,7 +15,7 @@ import { PushQueue } from "../push-queue.js";
15
15
  import { TtlSessionStore } from "../session-store.js";
16
16
  import { errorCodeOf } from "./assemble-result.js";
17
17
  import { effectiveDelegationFacts, prepareTask } from "./prepare-task.js";
18
- import { SessionReadFileStates } from "./prepare-hands-readface.js";
18
+ import { SessionReadFileStates } from "./session-read-file-states.js";
19
19
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
20
20
  import { PAUSE_REGISTRY } from "../pause-registry.js";
21
21
  import { toolPolicyNameSets } from "../tool-policy.js";
@@ -0,0 +1,45 @@
1
+ import type { RunnerDeps, ToolSpec } from "../types.js";
2
+ import type { RunnerSelfSeat } from "./contracts.js";
3
+ import type { SubagentTranscriptTier } from "../wiring-manifest.js";
4
+ /**
5
+ * The tier-3 revival spawner of the SendMessage mount: re-enters the
6
+ * MOUNTED delegation tool (identified by object, never by wire name) through the raw ToolSpec execute +
7
+ * the leg's ctx enricher, with a fresh toolCallId and the trusted `ctx.reviveClaim`, so a revival is the
8
+ * complete spawn chain. `undefined` when no delegation tool is mounted. Module-level: closes over
9
+ * nothing of the leg but the two values it is handed.
10
+ */
11
+ declare function buildTier3ReviveSpawn(delegationForRevive: ToolSpec | undefined, enrichSpecToolCtx: (ctx: import("../types.js").ToolExecuteContext) => import("../types.js").ToolExecuteContext): ((req: {
12
+ row: import("../background-agent-store.js").BackgroundAgentRecord;
13
+ rev: number;
14
+ prompt: string;
15
+ peerSeed?: {
16
+ hopChain: string[];
17
+ };
18
+ }) => Promise<{
19
+ isError?: boolean;
20
+ content: string;
21
+ details?: unknown;
22
+ }>) | undefined;
23
+ /**
24
+ * The tier-3 revival spawner: identify the mounted delegation
25
+ * tool by OBJECT via its `agentListing` marker (filled only by createSubagentTool; a caller's
26
+ * same-named shadow tool never carries it, so the trusted ctx.reviveClaim can never reach
27
+ * one). The call goes through the RAW ToolSpec execute + `enrichSpecToolCtx` (the same
28
+ * enrichment the injection map applies — the AgentTool adapter face forwards only
29
+ * {toolCallId, signal} and would strip the claim), so the revival is the complete spawn
30
+ * chain: notify lane, observer sink, roster, durable store. The request is built by
31
+ * SendMessage AT EXECUTE TIME (ctx-first identity); this closure holds only the tool object
32
+ * reference. Fresh toolCallId per revival (F-11③ — never SendMessage's own call id).
33
+ * `tier` is {@link sendMessageContinuationTier} over the same seats the mount receives.
34
+ */
35
+ export declare function sendMessageContinuationSeats(seats: {
36
+ specTools: readonly ToolSpec[];
37
+ agentStore: RunnerDeps["backgroundAgentStore"];
38
+ mailbox: RunnerDeps["mailboxStore"];
39
+ runner: RunnerSelfSeat;
40
+ enrichSpecToolCtx: (ctx: import("../types.js").ToolExecuteContext) => import("../types.js").ToolExecuteContext;
41
+ }): {
42
+ reviveSpawn: ReturnType<typeof buildTier3ReviveSpawn>;
43
+ tier: SubagentTranscriptTier;
44
+ };
45
+ export {};
@@ -0,0 +1,27 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { sendMessageContinuationTier } from "../../agents/send-message-tool.js";
3
+ function buildTier3ReviveSpawn(delegationForRevive, enrichSpecToolCtx) {
4
+ if (delegationForRevive === undefined)
5
+ return undefined;
6
+ return async (req) => {
7
+ const out = await delegationForRevive.execute({
8
+ description: req.row.description ?? "revived teammate",
9
+ prompt: req.prompt,
10
+ run_in_background: true,
11
+ ...(req.row.agentType !== undefined ? { subagent_type: req.row.agentType } : {}),
12
+ ...(req.row.name !== undefined ? { name: req.row.name } : {}),
13
+ ...(req.row.model !== undefined ? { model: req.row.model } : {}),
14
+ }, enrichSpecToolCtx({ toolCallId: `rv-${randomBytes(8).toString("hex")}`, reviveClaim: { row: req.row, rev: req.rev, ...(req.peerSeed !== undefined ? { peerSeed: req.peerSeed } : {}) } }));
15
+ const o = (typeof out === "string" ? { content: out } : out);
16
+ return {
17
+ content: typeof o.content === "string" ? o.content : JSON.stringify(o.content),
18
+ ...(o.isError === true ? { isError: true } : {}),
19
+ ...(o.details !== undefined ? { details: o.details } : {}),
20
+ };
21
+ };
22
+ }
23
+ export function sendMessageContinuationSeats(seats) {
24
+ const delegationForRevive = seats.specTools.find((t) => t.agentListing !== undefined);
25
+ const reviveSpawn = seats.agentStore !== undefined && seats.mailbox !== undefined ? buildTier3ReviveSpawn(delegationForRevive, seats.enrichSpecToolCtx) : undefined;
26
+ return { reviveSpawn, tier: sendMessageContinuationTier({ agentStore: seats.agentStore, mailbox: seats.mailbox, reviveSpawn, runner: seats.runner }) };
27
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The Runner's session-scoped read-file-state CACHE and the licence to inherit from it: session-scoped cache
3
+ * machinery, not a phase step — the Runner owns the instance for its lifetime, the run internals carry it to the
4
+ * hands phase, and the hands phase adopts, inherits and seals seats through it.
5
+ */
6
+ import type { ReadFileState } from "../../tools/fs/safety.js";
7
+ /**
8
+ * The Runner's per-session read-file-state seats (CC parity: readFileState is minted
9
+ * once per REPL session and mutated across turns; it has NO turn-boundary and NO abort clear site —
10
+ * its only clears are compaction/fork/subagent-cleanup/dispose). sema's map stays per-task (the §11
11
+ * ruling-A closure wiring), so the SESSION scope lives here instead: prepare adopts each task's live
12
+ * map under its sessionId, and the next same-session, same-Runner task INHERITS it (through the same
13
+ * containment judgment the transcript-replay seed applies — see `inheritSessionReadFileState`).
14
+ *
15
+ * This seat is a CACHE, never an authority:
16
+ * - a durable resume's checkpoint seed stays the sole authority on that leg (the seed was cut
17
+ * atomically with the leaf; this seat may be older OR newer than it);
18
+ * - a cross-process continuation misses here and falls back to the transcript replay (rung-2);
19
+ * - eviction (the LRU cap, a Runner restart) degrades to that same replay — never to wrong state.
20
+ *
21
+ * Bounded: an LRU over sessionIds (insertion-order refresh on read), because the SessionStore's own
22
+ * eviction is not observable from here — a dropped seat costs one transcript replay, nothing else.
23
+ * The stored value is the task's LIVE map (object identity), so in-turn mutations — tool write-backs,
24
+ * and above all the compaction clear — are the seat's truth by construction; a snapshot copy
25
+ * here would resurrect summarized-away reads on the next turn.
26
+ *
27
+ * BRANCH BINDING: a sessionId alone does not name a transcript — the same id
28
+ * can be rebranched (`resumeAt`), rewound out-of-band, or DELETED and re-created (`release()` + the
29
+ * default create-on-miss acquire), and the seat's map would then vouch for reads the CURRENT branch
30
+ * never saw. Each seat therefore records `anchorLeafId` — a leaf on the lineage its task produced —
31
+ * and inheritance must first prove the CURRENT branch still descends from that anchor
32
+ * (see {@link sessionSeatAnchorHolds}). The anchor is written twice: at ADOPT (the leaf at prepare;
33
+ * `null` when the branch was still empty) and SEALED at the task's settle to the task's own tail
34
+ * (`reanchor`). A `null` anchor is NEVER inheritable ("the branch is non-empty now" proves
35
+ * nothing about WHOSE messages those are — another Runner repopulating a released id, or a session
36
+ * import, makes a non-empty branch that never contained this seat's reads). An unprovable anchor
37
+ * falls back to the transcript replay, which reads the current branch by construction — cost, never
38
+ * wrongness.
39
+ */
40
+ export declare class SessionReadFileStates {
41
+ private readonly seats;
42
+ private static readonly CAP;
43
+ /** The session's seat row, or undefined (cross-process / evicted / first turn) ⇒ caller replays.
44
+ * The caller must verify the anchor against the session's CURRENT branch before using `state`. */
45
+ get(sessionId: string): {
46
+ state: ReadFileState;
47
+ anchorLeafId: string | null;
48
+ } | undefined;
49
+ /** Adopt THIS task's live map as the session's seat (every hands leg: fresh, continuation, resume).
50
+ * `anchorLeafId` = the session's leaf as read at THIS prepare (null = the branch was empty; a
51
+ * null-anchored seat is not inheritable until the settle-time {@link reanchor} names a real leaf). */
52
+ adopt(sessionId: string, state: ReadFileState, anchorLeafId: string | null): void;
53
+ /** The settle-time SEAL: re-anchor the seat to the finished task's own tail, so a
54
+ * first-task (null-anchor) seat becomes inheritable only through a leaf its task provably wrote.
55
+ * Identity-guarded: touches the row only while it still holds THIS task's map (a later prepare on
56
+ * the same session has already replaced the seat and owns its own anchor). */
57
+ reanchor(sessionId: string, state: ReadFileState, anchorLeafId: string): void;
58
+ }
59
+ /**
60
+ * Does the session's CURRENT branch still descend from the seat's anchor?
61
+ * TRUE is the licence to inherit; anything unprovable answers FALSE (the caller then replays the
62
+ * transcript — the fail-closed direction costs a rebuild, never admits a wrong entry).
63
+ *
64
+ * - anchor `null`: NEVER inheritable (r2). The seat was adopted on an empty branch and its task
65
+ * never sealed (crashed pre-settle, or the leaf read failed) — "the branch is non-empty now"
66
+ * would also be true of a released-and-repopulated id or an imported log, whose messages are not
67
+ * this seat's lineage. The settle-time reanchor is what upgrades such a seat to a real leaf.
68
+ * - anchor non-null: holds iff walking parentIds from the current leaf reaches the anchor within
69
+ * a bounded number of steps (the walk length is one task's appends in the ordinary continuation).
70
+ * A rebranch/rewind moved the leaf OFF the anchor's subtree ⇒ the walk misses ⇒ no inherit; a
71
+ * recreated/foreign log does not contain the anchor ⇒ same answer; a re-IMPORT of the same log
72
+ * keeps the anchor's lineage ⇒ inheritance stays correct there by the same test.
73
+ * - a COMPACTION on the walked segment (A-082 idx2): refuses, even though the branch is perfectly
74
+ * intact. The walked segment is exactly what landed AFTER this seat was anchored, and a
75
+ * compaction there summarized older Read results OUT of the context the model will be shown —
76
+ * the very state the compaction clear removes from the live map at the landing site. That clear reaches the seat
77
+ * only when the SAME Runner did both (the seat IS that map, by object identity); a second Runner
78
+ * on the shared durable session clears its own map and commits the compaction as an ORDINARY
79
+ * LINEAR APPEND, which every other test here passes. Inheriting across it would let requireRead
80
+ * and checkStale both pass on a read whose card the model no longer has — a blind edit from a
81
+ * summarized-away recollection, the exact failure the compaction clear exists to prevent. Refusing
82
+ * costs one transcript replay, and the replay reads the POST-compaction branch by construction,
83
+ * so the fail-closed direction is also the CORRECT state. A compaction at or ABOVE the anchor is
84
+ * not on the walked segment and does not refuse: the seat that observed it already carries its
85
+ * clear (P-483-4), and an ordinary foreign turn with no compaction still inherits (G-483-5) —
86
+ * dropping every seat a foreign Runner touched would restore the cross-turn read-state amnesia this seat-anchor check already ended.
87
+ */
88
+ export declare function sessionSeatAnchorHolds(getEntry: (id: string) => Promise<{
89
+ parentId: string | null;
90
+ type?: string;
91
+ } | null | undefined>, anchorLeafId: string | null, currentLeafId: string | null): Promise<boolean>;
@@ -0,0 +1,43 @@
1
+ export class SessionReadFileStates {
2
+ seats = new Map();
3
+ static CAP = 1024;
4
+ get(sessionId) {
5
+ const seat = this.seats.get(sessionId);
6
+ if (seat !== undefined) {
7
+ this.seats.delete(sessionId);
8
+ this.seats.set(sessionId, seat);
9
+ }
10
+ return seat;
11
+ }
12
+ adopt(sessionId, state, anchorLeafId) {
13
+ this.seats.delete(sessionId);
14
+ this.seats.set(sessionId, { state, anchorLeafId });
15
+ while (this.seats.size > SessionReadFileStates.CAP) {
16
+ const oldest = this.seats.keys().next().value;
17
+ if (oldest === undefined)
18
+ break;
19
+ this.seats.delete(oldest);
20
+ }
21
+ }
22
+ reanchor(sessionId, state, anchorLeafId) {
23
+ const seat = this.seats.get(sessionId);
24
+ if (seat !== undefined && seat.state === state)
25
+ seat.anchorLeafId = anchorLeafId;
26
+ }
27
+ }
28
+ export async function sessionSeatAnchorHolds(getEntry, anchorLeafId, currentLeafId) {
29
+ if (anchorLeafId === null)
30
+ return false;
31
+ let id = currentLeafId;
32
+ for (let steps = 0; id !== null && steps < 100_000; steps++) {
33
+ if (id === anchorLeafId)
34
+ return true;
35
+ const entry = await getEntry(id);
36
+ if (entry === null || entry === undefined)
37
+ return false;
38
+ if (entry.type === "compaction")
39
+ return false;
40
+ id = entry.parentId;
41
+ }
42
+ return false;
43
+ }
@@ -19,7 +19,7 @@
19
19
  import type { RedactionConfidence, RedactionSource } from "./arg-summary.js";
20
20
  import { type EngineNotice } from "./engine-notice.js";
21
21
  /** Which rule classified a key as secret-shaped — the env arm's `kind` vocabulary. */
22
- export type SecretEnvFindingKind = "suffix-rule" | "exact-name";
22
+ export type SecretEnvFindingKind = "suffix-rule" | "namespace-rule" | "exact-name";
23
23
  /** One env entry the `scrub` policy dropped: the key (the VALUE is never carried — this record is meant
24
24
  * to be loggable), the rule that classified it, how strong that claim is, and the minting module. */
25
25
  export interface SecretEnvFinding {
@@ -1,6 +1,8 @@
1
1
  import { deliverEngineNotice } from "./engine-notice.js";
2
2
  const TOKEN_COUNT_WORDS = ["MAX", "OUTPUT", "INPUT", "THINKING", "NUM", "TOTAL"];
3
- const SECRET_ENV_RE = new RegExp(`(^|_)(KEY|KEYS|TOKEN|(?<!(?:^|_)(?:${TOKEN_COUNT_WORDS.join("|")})_)TOKENS|SECRET|SECRETS|PASSWORD|PASSWORDS|PASSWD|CREDENTIAL|CREDENTIALS|PRIVATEKEY|APIKEY|ACCESS_KEY|SESSION_TOKEN)(?:_(?:\\d+|ID))?$`, "i");
3
+ const CREDENTIAL_WORD = `KEY|KEYS|TOKEN|(?<!(?:^|_)(?:${TOKEN_COUNT_WORDS.join("|")})_)TOKENS|SECRET|SECRETS|PASSWORD|PASSWORDS|PASSWD|CREDENTIAL|CREDENTIALS|PRIVATEKEY|APIKEY`;
4
+ const SECRET_ENV_RE = new RegExp(`(^|_)(${CREDENTIAL_WORD})(?:_(?:\\d+|ID))?$`, "i");
5
+ const SEMA_NAMESPACE_SECRET_RE = new RegExp(`^SEMA_(?:.*_)?(?:${CREDENTIAL_WORD})(?:_|$)`, "is");
4
6
  const SECRET_ENV_EXACT_NAMES = new Set([
5
7
  "DATABASE_URL", "CONNECTION_STRING", "DSN", "NPM_AUTH",
6
8
  "PGPASSWORD", "MYSQL_PWD", "SSHPASS", "GPG_PASSPHRASE", "GH_PAT", "GITHUB_PAT",
@@ -16,6 +18,7 @@ const NON_SECRET_ENV_EXACT_NAMES = new Set([
16
18
  const SECRET_ENV_KIND_CONFIDENCE = {
17
19
  "exact-name": "high",
18
20
  "suffix-rule": "medium",
21
+ "namespace-rule": "medium",
19
22
  };
20
23
  export function classifyExactEnvName(key, credentialNames, exemptNames) {
21
24
  const upper = key.toUpperCase();
@@ -31,7 +34,9 @@ function classifySecretEnvKey(key) {
31
34
  return "exact-name";
32
35
  if (byExactName === "exempt")
33
36
  return undefined;
34
- return SECRET_ENV_RE.test(key) ? "suffix-rule" : undefined;
37
+ if (SECRET_ENV_RE.test(key))
38
+ return "suffix-rule";
39
+ return SEMA_NAMESPACE_SECRET_RE.test(key) ? "namespace-rule" : undefined;
35
40
  }
36
41
  export function isSecretEnvKey(key) {
37
42
  return classifySecretEnvKey(key) !== undefined;
@@ -51,6 +51,7 @@ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
51
51
  toolName: string;
52
52
  toolInput: string;
53
53
  mandated: boolean;
54
+ mandate: string;
54
55
  origin: string;
55
56
  spentMicroUsd?: undefined;
56
57
  } | {
@@ -68,5 +69,6 @@ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
68
69
  toolName?: undefined;
69
70
  toolInput?: undefined;
70
71
  mandated?: undefined;
72
+ mandate?: undefined;
71
73
  origin?: undefined;
72
74
  })[];