@sema-agent/core 7.13.0 → 7.15.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 (102) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/agents/subagent.d.ts +4 -9
  3. package/dist/agents/subagent.js +16 -12
  4. package/dist/agents/teacher.js +1 -1
  5. package/dist/brain/reasoning.d.ts +23 -0
  6. package/dist/brain/reasoning.js +35 -4
  7. package/dist/brain/stream-engine.js +35 -17
  8. package/dist/brain/timeout.d.ts +25 -4
  9. package/dist/brain/timeout.js +1 -1
  10. package/dist/core/ask-origin.d.ts +30 -11
  11. package/dist/core/ask-origin.js +10 -5
  12. package/dist/core/auto-mode.d.ts +3 -3
  13. package/dist/core/checkpoint-seat.d.ts +18 -0
  14. package/dist/core/checkpoint-seat.js +6 -0
  15. package/dist/core/checkpoint-store.d.ts +23 -31
  16. package/dist/core/checkpoint-store.js +7 -4
  17. package/dist/core/engine-notice.d.ts +3 -1
  18. package/dist/core/env-knob-announce.d.ts +16 -0
  19. package/dist/core/env-knob-announce.js +14 -0
  20. package/dist/core/gate-fold.js +2 -0
  21. package/dist/core/gate-lanes.js +21 -12
  22. package/dist/core/gate-outcome.d.ts +42 -3
  23. package/dist/core/gate-outcome.js +22 -0
  24. package/dist/core/governance-codes.js +1 -1
  25. package/dist/core/hooks.d.ts +9 -19
  26. package/dist/core/hooks.js +6 -4
  27. package/dist/core/mcp.d.ts +3 -2
  28. package/dist/core/mcp.js +5 -11
  29. package/dist/core/memory-engine/engine.d.ts +2 -2
  30. package/dist/core/runner/advertised-writable-dirs.d.ts +74 -0
  31. package/dist/core/runner/advertised-writable-dirs.js +48 -0
  32. package/dist/core/runner/assemble-result.d.ts +5 -0
  33. package/dist/core/runner/contracts.d.ts +2 -2
  34. package/dist/core/runner/denial-limit-arms.d.ts +7 -9
  35. package/dist/core/runner/denial-limit-arms.js +8 -10
  36. package/dist/core/runner/gate-exit.d.ts +15 -3
  37. package/dist/core/runner/gate-exit.js +6 -4
  38. package/dist/core/runner/permission-rule-lanes.d.ts +3 -1
  39. package/dist/core/runner/permission-rule-lanes.js +1 -1
  40. package/dist/core/runner/prepare-caps-and-workflow.d.ts +34 -4
  41. package/dist/core/runner/prepare-caps-and-workflow.js +47 -12
  42. package/dist/core/runner/prepare-defer-classify.d.ts +1 -1
  43. package/dist/core/runner/prepare-defer-classify.js +12 -10
  44. package/dist/core/runner/prepare-gate-stations.d.ts +4 -5
  45. package/dist/core/runner/prepare-gate-stations.js +2 -2
  46. package/dist/core/runner/prepare-hands-readface.d.ts +18 -12
  47. package/dist/core/runner/prepare-hands-readface.js +32 -43
  48. package/dist/core/runner/prepare-inherited-gate.d.ts +7 -5
  49. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  50. package/dist/core/runner/prepare-memory.d.ts +31 -34
  51. package/dist/core/runner/prepare-memory.js +73 -51
  52. package/dist/core/runner/prepare-park-ask.d.ts +5 -6
  53. package/dist/core/runner/prepare-park-ask.js +3 -3
  54. package/dist/core/runner/prepare-policy-chain.d.ts +2 -2
  55. package/dist/core/runner/prepare-policy-chain.js +7 -4
  56. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -0
  57. package/dist/core/runner/prepare-prompt-inputs.js +2 -2
  58. package/dist/core/runner/prepare-question-face.d.ts +10 -8
  59. package/dist/core/runner/prepare-question-face.js +1 -3
  60. package/dist/core/runner/prepare-safety-scan.js +1 -1
  61. package/dist/core/runner/prepare-task.js +60 -53
  62. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  63. package/dist/core/runner/prepare-wiring-manifest.js +1 -1
  64. package/dist/core/runner/run-compaction-machinery.js +2 -0
  65. package/dist/core/runner/run-harness-handlers.js +3 -1
  66. package/dist/core/runner/stream-settle-backstop.js +1 -1
  67. package/dist/core/runner/tool-defer-gate.d.ts +86 -0
  68. package/dist/core/runner/tool-defer-gate.js +57 -0
  69. package/dist/core/runner/tool-disclosure.d.ts +0 -36
  70. package/dist/core/runner/tool-disclosure.js +0 -43
  71. package/dist/core/runner-deps.d.ts +16 -9
  72. package/dist/core/runtime-caps.d.ts +21 -0
  73. package/dist/core/runtime-caps.js +5 -1
  74. package/dist/core/task-event.d.ts +11 -2
  75. package/dist/core/task-registry-shared.js +8 -6
  76. package/dist/core/task-result.d.ts +15 -0
  77. package/dist/core/task-spec.d.ts +8 -2
  78. package/dist/core/terminal-cause.d.ts +6 -2
  79. package/dist/core/tool-policy.d.ts +34 -28
  80. package/dist/core/tool-policy.js +29 -5
  81. package/dist/core/tool-roster.js +2 -0
  82. package/dist/core/tool-spec.d.ts +20 -16
  83. package/dist/core/types.d.ts +3 -2
  84. package/dist/core/types.js +1 -0
  85. package/dist/core/wiring-manifest.d.ts +16 -9
  86. package/dist/core/wiring-manifest.js +8 -3
  87. package/dist/index.d.ts +7 -6
  88. package/dist/index.js +5 -4
  89. package/dist/orchestration/run-workflow-tool.d.ts +8 -8
  90. package/dist/orchestration/run-workflow-tool.js +1 -1
  91. package/dist/orchestration/workflow-primitives.d.ts +4 -3
  92. package/dist/orchestration/workflow-primitives.js +3 -3
  93. package/dist/orchestration/workflow-types.d.ts +14 -0
  94. package/dist/orchestration/workflow.d.ts +41 -4
  95. package/dist/orchestration/workflow.js +23 -5
  96. package/dist/tools/fs/fs-bash.d.ts +41 -0
  97. package/dist/tools/fs/fs-bash.js +86 -29
  98. package/dist/tools/fs/fs-shared.js +9 -3
  99. package/dist/tools/fs/read-deny.d.ts +24 -8
  100. package/dist/tools/fs/read-deny.js +20 -1
  101. package/package.json +2 -1
  102. package/test/export-surface.snapshot.json +29 -1
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import type { AgentTool } from "../../internal/harness.js";
20
20
  import { type OnQuestion } from "../ask-question.js";
21
- import { type CheckpointStore } from "../checkpoint-store.js";
21
+ import type { CheckpointStore } from "../checkpoint-store.js";
22
22
  import type { RunnerDeps, RuntimeCaps, TaskSpec } from "../types.js";
23
23
  import type { PrepareResume } from "./contracts.js";
24
24
  import { type ContentAskBinding } from "./content-ask-bindings.js";
@@ -28,13 +28,15 @@ export interface PrepareQuestionFaceInput {
28
28
  /** borrowed-readonly — the run's frozen question seat (spec > deps), resolved once at the top of prepare;
29
29
  * the mount predicate and the live-face derivation both read this one value. */
30
30
  frozenOnQuestion: OnQuestion | undefined;
31
- /** borrowed-readonly — the REBOUND spec. Read: `checkpointStore` (the per-run off switch), `durableApproval`
32
- * (the durable face's opt-in), `interactiveTools` (the override), `principal` (the tool's source identity),
33
- * `interactiveQuestionFallback` (restores the continuation under a resolved posture). Never mutated. */
34
- spec: Pick<TaskSpec, "checkpointStore" | "durableApproval" | "interactiveTools" | "principal" | "interactiveQuestionFallback">;
35
- /** borrowed-readonly — the deployment seats: `checkpointStore` (the store the leg snapshot resolves against)
36
- * and `onError` (the stranded-answer and synthetic-continuation operator lines; swallow-guarded). */
37
- deps: Pick<RunnerDeps, "checkpointStore" | "onError">;
31
+ /** borrowed-readonly — the REBOUND spec. Read: `durableApproval` (the durable face's opt-in), `interactiveTools`
32
+ * (the override), `principal` (the tool's source identity), `interactiveQuestionFallback` (restores the
33
+ * continuation under a resolved posture). Never mutated. */
34
+ spec: Pick<TaskSpec, "durableApproval" | "interactiveTools" | "principal" | "interactiveQuestionFallback">;
35
+ /** borrowed-readonly — the deployment seat `onError` (the stranded-answer and synthetic-continuation operator
36
+ * lines; swallow-guarded). */
37
+ deps: Pick<RunnerDeps, "onError">;
38
+ /** borrowed-readonly — THE leg's checkpoint seat (minted once by the driver after the config doors); the mount face, the routability predicate, the manifest and suspendAsk all read this one value through the result. */
39
+ checkpointStore: CheckpointStore | undefined;
38
40
  /** borrowed-readonly — the principal's resolved runtime caps as of this point (`forceDurableGate` is the
39
41
  * mandate that outranks a live face only where a store exists to park the question). */
40
42
  runtimeCaps: Pick<RuntimeCaps, "forceDurableGate"> | undefined;
@@ -1,16 +1,14 @@
1
1
  import { createAskUserQuestionTool, isLiveQuestionFace } from "../ask-question.js";
2
2
  import { boundInputHashOf } from "../canonical-json.js";
3
- import { resolveCheckpointStore } from "../checkpoint-store.js";
4
3
  import { CONTENT_ASK_BINDING_CAP } from "./content-ask-bindings.js";
5
4
  import { RosterBuilder } from "../tool-roster.js";
6
5
  export { CONTENT_ASK_BINDING_CAP } from "./content-ask-bindings.js";
7
6
  export function prepareQuestionFace(input) {
8
- const { frozenOnQuestion, spec, deps, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks } = input;
7
+ const { frozenOnQuestion, spec, deps, checkpointStore, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks } = input;
9
8
  const tools = roster.tools;
10
9
  const onQuestion = frozenOnQuestion;
11
10
  const liveQuestionFace = isLiveQuestionFace(onQuestion) ? onQuestion : undefined;
12
11
  const contentAskBindings = new Map();
13
- const checkpointStore = resolveCheckpointStore(spec, deps);
14
12
  const questionParkStoreWired = checkpointStore !== undefined;
15
13
  const questionDurableMandateBinding = runtimeCaps?.forceDurableGate === true && questionParkStoreWired;
16
14
  const contentAskRoutable = (toolCallId) => liveQuestionFace !== undefined &&
@@ -23,7 +23,7 @@ export function prepareSafetyScan(input) {
23
23
  ...(typeof t.modelGate === "string" ? { modelGate: t.modelGate } : {}),
24
24
  });
25
25
  if (bound !== undefined) {
26
- const e = new Error(`Tool "${t.name.slice(0, 40)}" cannot ride a roster row: ${bound.message}.`);
26
+ const e = new Error(`A caller tool cannot ride a roster row: ${bound.message}.`);
27
27
  e.code = bound.code;
28
28
  throw e;
29
29
  }
@@ -68,12 +68,14 @@ import { defaultTaskRegistry } from "../task-registry.js";
68
68
  import { announcePeerLaneMount } from "../../agents/peer-session-drain.js";
69
69
  import { createSchedulerTools } from "../../tools/scheduler-tools.js";
70
70
  import { selfOrchestrationFailClosedReason } from "../../orchestration/workflow-script-runner.js";
71
- import { remainingBudgetMicroUsd } from "../checkpoint-store.js";
71
+ import { remainingBudgetMicroUsd, resolveCheckpointStore } from "../checkpoint-store.js";
72
+ import { checkpointSeatOf } from "../checkpoint-seat.js";
72
73
  import { durableParkGapFor } from "../park-selfcheck.js";
73
74
  import { deliverEngineNotice } from "../types.js";
74
75
  import { ToolRosterDeltaSeat } from "../tool-roster.js";
75
76
  import { faceOf } from "../tool-roster.js";
76
77
  import { catalogEntriesNamed, pathTargetOf } from "../tool-registry.js";
78
+ import { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
77
79
  import { createStructuredProjector } from "./tool-output-projection.js";
78
80
  function warnCompactionWindowHazard(tracer, spec, model, compModel, hostTaskId) {
79
81
  if (compModel === undefined)
@@ -257,13 +259,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
257
259
  runIdSink.runId = runId;
258
260
  const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
259
261
  spec = doors.spec;
262
+ const checkpointStore = resolveCheckpointStore(spec, deps), checkpointSeat = checkpointSeatOf(spec, checkpointStore);
260
263
  const autoModeSeat = refuseInvalidAutoModeSeat(spec);
261
264
  const { toolFaceSnapshot, promptProfile, lockedPreflight, resolvedInteractionPosture, microCompactKnob, resolvedRole, model, thinking, compModel, fableMitigations, memoryDelegationEvidence, memoryProvenance, memoryCapturePolicy, usageWindows, brainCallGuardrailRef, brainCallGuardrailMs } = doors;
262
265
  announceToolModelGate(deps.onNotice, model.id, doors.modelGate);
263
266
  const modelGateManifest = toolModelGateManifest(doors.modelGate);
264
267
  const { toolEffects, egressTools, irreversibleTools, irreversibilityTier, axisExplicitNegatives, reversibilityProbes, ownToolNames } = prepareSafetyScan({ spec, deps });
265
- let shellGatedBash = false;
266
- let shellGatedMonitor = false;
268
+ let shellGatedTools = new Set();
267
269
  refuseRequireExistingWithoutSession(spec);
268
270
  const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects, ...(internals?.sessionPlacement !== undefined ? { placement: internals.sessionPlacement } : {}), ...(() => { const g = durableParkGapFor(deps, spec); return g !== undefined ? { durableParkGap: g } : {}; })() });
269
271
  const sessionId = acquired.sessionId;
@@ -366,8 +368,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
366
368
  let resolvedReadFace;
367
369
  let readDenyAdditionsNormalized = [];
368
370
  let handsLessResolvedFace;
369
- const { liveInheritedGate, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, inheritedParentConstraints, autoModeIntent, inheritedAdmittedOrgScopes, priorOwnOrgVerdict, orgGovernedProvenance, effectiveShellGate, ownSessionRulesRef, memoryAdmittedOrgScopesRef, ownOrgVerdictRef, orgAdmissionCheckpointState, faceCheckpointSection, faceCheckpointState, f012CheckpointState, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, frozenOnQuestion, provenanceForChildrenRef, delegationSettlementRef, inheritedGateForChildren, carrierReadFace, fullShellReachable, gateStopRef, stopForDenialLimit, enrichSpecToolCtx } = prepareInheritedGate({ spec, deps, internals, resume, sessions, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, ownedEnv, lockedPreflight, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, autoModeSeat, 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, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized, handsLessResolvedFace: () => handsLessResolvedFace, centerAdoption: () => centerAdoptionRef.current, memoryEngineSession: () => memoryEngineSession });
370
- 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, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, frozenOnAsk, carrierReadFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, autoModeIntent, peerLaneActive, peerSendMessageBuiltIn, runnerSelf, memoryEngineSession: () => memoryEngineSession, centerAdoption: () => centerAdoptionRef.current, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized });
371
+ const { liveInheritedGate, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, inheritedParentConstraints, autoModeIntent, inheritedAdmittedOrgScopes, priorOwnOrgVerdict, orgGovernedProvenance, effectiveShellGate, ownSessionRulesRef, memoryAdmittedOrgScopesRef, ownOrgVerdictRef, orgAdmissionCheckpointState, faceCheckpointSection, faceCheckpointState, f012CheckpointState, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, frozenOnQuestion, provenanceForChildrenRef, delegationSettlementRef, inheritedGateForChildren, carrierReadFace, fullShellReachable, gateStopRef, stopForDenialLimit, enrichSpecToolCtx } = prepareInheritedGate({ spec, deps, internals, resume, sessions, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, ownedEnv, lockedPreflight, checkpointSeat, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, autoModeSeat, 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, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized, handsLessResolvedFace: () => handsLessResolvedFace, centerAdoption: () => centerAdoptionRef.current, memoryEngineSession: () => memoryEngineSession });
372
+ 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, carrierReadFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, autoModeIntent, peerLaneActive, peerSendMessageBuiltIn, runnerSelf, memoryEngineSession: () => memoryEngineSession, centerAdoption: () => centerAdoptionRef.current, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized });
371
373
  const tools = roster.tools;
372
374
  const pausedRef = {};
373
375
  const remoteEnvFailures = [];
@@ -431,34 +433,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
431
433
  const callIssuedAtRef = {};
432
434
  const workCutRef = { continuation: false, abortedBeforeTurnEnd: false };
433
435
  const memoryWriteGateRef = {};
434
- const handsReadFaceInput = { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes, reminderMark, reminderDisclosureCounts, ...(trackFileEdit !== undefined ? { trackFileEdit } : {}), onFileEdited: noteFileEdited, ...(restoredFilePaths.length > 0 ? { restoredFilePaths } : {}) };
435
- const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
436
- const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher, envHandToolNames, sealReadStateSeat } = handsReadFace;
437
- resolvedReadFace = handsReadFace.resolvedReadFace;
438
- readDenyAdditionsNormalized = handsReadFace.readDenyAdditionsNormalized;
439
- handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
440
- shellGatedBash = handsReadFace.shellGatedBash;
441
- shellGatedMonitor = handsReadFace.shellGatedMonitor;
442
- const { worktreeSessionRef } = prepareDelegationSurface({ spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, executionEnv, runnerSelf, backgroundTaskToolsActive, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle });
443
- const { checkpointStore, liveQuestionFace, contentAskBindings, contentAskRoutable, lateStrandedAnswers, discloseStrandedAnswers, settleContentAskBindings, durableQuestionFace, questionToolMounted, mountedQuestionTool } = prepareQuestionFace({ frozenOnQuestion, spec, deps, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks: () => inheritedUnavailableAsks });
444
- if (spec.handsReadOnly !== true) {
445
- const sessionScope = sessionId ?? spec.principal ?? spec.taskId ?? "default";
446
- roster.mountAll(createSchedulerTools(executionEnv, {
447
- scope: sessionScope,
448
- ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
449
- ...(sessionId !== undefined ? { sessionId } : {}),
450
- requestStopAfterTurn,
451
- }).map((t) => (envHandToolNames.add(t.name), t)), { source: "builtin", mountedBy: "scheduler" });
452
- }
453
- if (internals?.insideFork !== true &&
454
- runnerSelf &&
455
- sessionId !== undefined &&
456
- hasSessionFork(runnerSelf.sessions) &&
457
- agentForkDenial === "principal") {
458
- deps.onError?.(new Error(`Fork DENIED for principal "${spec.principal ?? ""}" by runtimeCaps.allowFork=false ` +
459
- `(per-principal entitlement governance, not a misconfiguration — Agent(subagent_type:"fork") will refuse honestly)`), { phase: "config", sessionId });
460
- }
461
- const { lspDiagnostics, nudgeLspOnEdit, lspRunIdent } = prepareLsp({ spec, deps, executionEnv, taskRootFinal, handsEnabled, sessionId, handsCwdRef, roster, envHandToolNames });
436
+ const advertisedWritableDirs = handsEnabled
437
+ ? new AdvertisedWritableDirs(executionEnv, (dir, reason) => deps.onError?.(new Error(`${dir.advertiser} advertised writable directory skipped (cannot canonicalize): ${dir.path} — ${reason}`), { phase: "config", sessionId }))
438
+ : undefined;
439
+ const handsWriteHalfPlanned = handsEnabled && spec.handsReadOnly !== true;
462
440
  const memoryPairNameDomain = new Set();
463
441
  for (const t of tools) {
464
442
  memoryPairNameDomain.add(t.name);
@@ -477,14 +455,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
477
455
  return { fault: true };
478
456
  }
479
457
  })();
480
- const { memoryEngineSession, memoryBlock: memoryBlockFromEngine, memoryTools, recallDisciplineSegment: memoryRecallSegment, seedFiles: memorySeedFiles, admittedOrgScopes: memoryAdmittedOrgScopes, ownOrgVerdict: memoryOwnOrgVerdict, effectiveMemoryScopes: memoryEffectiveScopes, } = await prepareMemory({
458
+ const { memoryEngineSession, memoryTools, composeInjection: composeMemoryInjection, admittedOrgScopes: memoryAdmittedOrgScopes, ownOrgVerdict: memoryOwnOrgVerdict, effectiveMemoryScopes: memoryEffectiveScopes, } = await prepareMemory({
481
459
  spec,
482
460
  deps,
483
461
  sessionId,
484
462
  taskRootPath: taskRootFinal,
485
463
  memoryWriteGateRef,
486
464
  reminderMark,
487
- writeToolsMounted: tools.some((t) => t.name === "Write") &&
465
+ writeToolsMounted: (handsWriteHalfPlanned || tools.some((t) => t.name === "Write")) &&
488
466
  !(toolFaceSnapshot.exclude?.includes("Write") ?? false) &&
489
467
  !(handsEnabled && isRemoteExecutionEnv(executionEnv) && spec.memoryPersistenceCapable !== true),
490
468
  memoryProvenance,
@@ -503,20 +481,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
503
481
  ...(internals?.memoryCaptureQueryDir !== undefined ? { parentCaptureQueryDir: internals.memoryCaptureQueryDir } : {}),
504
482
  ...(internals?.memoryCaptureAncestors !== undefined ? { captureAncestors: internals.memoryCaptureAncestors } : {}),
505
483
  memoryPersistenceDeclared: spec.memoryPersistenceCapable,
506
- rosterCanPersist: spec.memoryPersistenceCapable ??
507
- tools.some((t) => {
508
- if (toolFaceSnapshot.exclude?.includes(t.name) ?? false)
509
- return false;
510
- const effect = (t.effect ?? toolEffects.get(t.name) ?? "write");
511
- if (effect === "read")
512
- return false;
513
- const face = faceOf(t);
514
- const pt = face.pathTarget ?? pathTargetOf(t.name);
515
- const family = face.family ?? catalogEntriesNamed(t.name)[0]?.face.family;
516
- if (!((pt !== undefined && pt.access !== "read") || family === "shell"))
517
- return false;
518
- return !(handsEnabled && isRemoteExecutionEnv(executionEnv));
519
- }),
484
+ ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}),
520
485
  memorySearchToolsPlanned,
521
486
  admissionCtx: {
522
487
  orgMemoryDenied: complianceDenies.has("org_memory_mount"),
@@ -540,6 +505,33 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
540
505
  if (internals?.ownOrgAdmissionRef !== undefined && ownOrgVerdictRef.current !== undefined) {
541
506
  internals.ownOrgAdmissionRef.current = ownOrgVerdictRef.current;
542
507
  }
508
+ const handsReadFaceInput = { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}), firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes, reminderMark, reminderDisclosureCounts, ...(trackFileEdit !== undefined ? { trackFileEdit } : {}), onFileEdited: noteFileEdited, ...(restoredFilePaths.length > 0 ? { restoredFilePaths } : {}) };
509
+ const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
510
+ const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher, envHandToolNames, sealReadStateSeat } = handsReadFace;
511
+ resolvedReadFace = handsReadFace.resolvedReadFace;
512
+ readDenyAdditionsNormalized = handsReadFace.readDenyAdditionsNormalized;
513
+ handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
514
+ shellGatedTools = handsReadFace.shellGatedTools;
515
+ const { worktreeSessionRef } = prepareDelegationSurface({ spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, executionEnv, runnerSelf, backgroundTaskToolsActive, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle });
516
+ const { liveQuestionFace, contentAskBindings, contentAskRoutable, lateStrandedAnswers, discloseStrandedAnswers, settleContentAskBindings, durableQuestionFace, questionToolMounted, mountedQuestionTool } = prepareQuestionFace({ frozenOnQuestion, spec, deps, checkpointStore, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks: () => inheritedUnavailableAsks });
517
+ if (spec.handsReadOnly !== true) {
518
+ const sessionScope = sessionId ?? spec.principal ?? spec.taskId ?? "default";
519
+ roster.mountAll(createSchedulerTools(executionEnv, {
520
+ scope: sessionScope,
521
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
522
+ ...(sessionId !== undefined ? { sessionId } : {}),
523
+ requestStopAfterTurn,
524
+ }).map((t) => (envHandToolNames.add(t.name), t)), { source: "builtin", mountedBy: "scheduler" });
525
+ }
526
+ if (internals?.insideFork !== true &&
527
+ runnerSelf &&
528
+ sessionId !== undefined &&
529
+ hasSessionFork(runnerSelf.sessions) &&
530
+ agentForkDenial === "principal") {
531
+ deps.onError?.(new Error(`Fork DENIED for principal "${spec.principal ?? ""}" by runtimeCaps.allowFork=false ` +
532
+ `(per-principal entitlement governance, not a misconfiguration — Agent(subagent_type:"fork") will refuse honestly)`), { phase: "config", sessionId });
533
+ }
534
+ const { lspDiagnostics, nudgeLspOnEdit, lspRunIdent } = prepareLsp({ spec, deps, executionEnv, taskRootFinal, handsEnabled, sessionId, handsCwdRef, roster, envHandToolNames });
543
535
  if (internals?.delegationProvenance !== undefined && resume !== undefined) {
544
536
  const live = internals.delegationProvenance.ref.current;
545
537
  const seed = resume.seed.delegationProvenance;
@@ -553,6 +545,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
553
545
  live.incomplete = true;
554
546
  }
555
547
  }
548
+ const rosterCanPersist = spec.memoryPersistenceCapable ??
549
+ tools.some((t) => {
550
+ if (toolFaceSnapshot.exclude?.includes(t.name) ?? false)
551
+ return false;
552
+ const effect = (t.effect ?? toolEffects.get(t.name) ?? "write");
553
+ if (effect === "read")
554
+ return false;
555
+ const face = faceOf(t);
556
+ const pt = face.pathTarget ?? pathTargetOf(t.name);
557
+ const family = face.family ?? catalogEntriesNamed(t.name)[0]?.face.family;
558
+ if (!((pt !== undefined && pt.access !== "read") || family === "shell"))
559
+ return false;
560
+ return !(handsEnabled && isRemoteExecutionEnv(executionEnv));
561
+ });
562
+ const { memoryBlock: memoryBlockFromEngine, recallDisciplineSegment: memoryRecallSegment, seedFiles: memorySeedFiles } = composeMemoryInjection({ rosterCanPersist });
556
563
  if (memorySeedFiles?.length && seedContextFiles) {
557
564
  try {
558
565
  await seedContextFiles(memorySeedFiles);
@@ -563,7 +570,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
563
570
  }
564
571
  const projectContext = await prepareProjectContext({ deps, spec, internals, session, sessionId, hostTaskId, taskRootFinal, handsEnabled, delegation, seedContextFiles, memoryBlockFromEngine, skillScope, roster, toolFaceSnapshot, onceLedger, memoryTools, memoryEngineSession, memorySearchToolsPlanned, memoryPairExcludedName, memoryPairOccupiedName, resume });
565
572
  const { memoryBlock: memoryBlockWithProject, instructionSources, projectInstructionContent, hasSkillManifest, skillsListing, sharedMemoryPairMounted, memoryEnginePairMounted } = projectContext;
566
- const { provider, featureFlags, userSystemPrompt, userAppendSystemPrompt, mcpInstructionsBlock, envFacts, tzValid, userTz, gitStatusRef } = await preparePromptInputs({ spec, deps, failClosedReason: selfOrchestrationFailClosedReason(spec, deps), lockedPreflight, selfOrchestrationActive, executionEnv, reminderMark, resolvedReadFace, toolFaceSnapshot, promptProfile, fableMitigations, internals, thinking, ownedEnv, delegation, resolvedRole, mcp, model, externalContentTargetActive, resume, handsEnabled, taskRootFinal, additionalRootsCanonical, additionalReadRootsCanonical, sessionId });
573
+ const { provider, featureFlags, userSystemPrompt, userAppendSystemPrompt, mcpInstructionsBlock, envFacts, tzValid, userTz, gitStatusRef } = await preparePromptInputs({ spec, deps, failClosedReason: selfOrchestrationFailClosedReason(spec, deps), lockedPreflight, selfOrchestrationActive, executionEnv, reminderMark, resolvedReadFace, toolFaceSnapshot, promptProfile, fableMitigations, internals, thinking, ownedEnv, delegation, resolvedRole, mcp, model, externalContentTargetActive, resume, handsEnabled, taskRootFinal, additionalRootsCanonical, additionalReadRootsCanonical, sessionId, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}) });
567
574
  const { deferred, memoryBlock } = prepareDeferClassify({ spec, deps, roster, toolFaceSnapshot, handsEnabled, fullShellReachable, promptProfile, model, mcp, a2a, sharedMemoryPairMounted, memoryEnginePairMounted, memoryBlock: memoryBlockWithProject, memoryRecallSegment, onceLedger, sessionId });
568
575
  prepareMemoryEngineSession({ memoryEngineSession, internals, memoryDelegationEvidence, externalContentTargetActive, lockedPreflight, mcp, sessionId, runId, deps, provenanceForChildrenRef, delegationSettlementRef, spec, envHandToolNames, roster, contentOriginWrapRef });
569
576
  const { systemPromptSeat, renderWithDate, systemBlocks, promptManifest, epochDeclaredSections, epochArtifactDigestForSnapshot, buildAssembleInputs, centerAdoptionRef } = await preparePromptAssembly({ session, deps, internals, resume, provider, userSystemPrompt, userAppendSystemPrompt, featureFlags, mcpInstructionsBlock, memoryBlock, envFacts, promptProfile, fableMitigations, tools, model, sessionId, reminderMark, onceLedger });
@@ -674,7 +681,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
674
681
  releaseSignal = () => sig.removeEventListener("abort", onExternalAbort);
675
682
  }
676
683
  }
677
- const { hooks, hookTimeoutMs, preToolUseObservational, ownGatePreToolUse, basePolicyForResumeEdit, denyNarrowingPolicy, policyLayers, effectivePolicy, inheritedUnavailableAsks, inheritedAskGrants, askSourceIdentity, riskAxesOf, ruleOffersOf, permissionRuleLane, permissionRuleOrgLane, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, foldAskClasses, ancestorSandboxAdmissions } = await preparePolicyChain({ spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, ownToolNames, shellGatedBash, handsCwdRef, readDenyMatcher, questionToolMounted, runtimeCaps, inheritedAncestorRules, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef });
684
+ const { hooks, hookTimeoutMs, preToolUseObservational, ownGatePreToolUse, basePolicyForResumeEdit, denyNarrowingPolicy, policyLayers, effectivePolicy, inheritedUnavailableAsks, inheritedAskGrants, askSourceIdentity, riskAxesOf, ruleOffersOf, permissionRuleLane, permissionRuleOrgLane, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, foldAskClasses, ancestorSandboxAdmissions } = await preparePolicyChain({ spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, ownToolNames, shellGatedTools, handsCwdRef, readDenyMatcher, questionToolMounted, runtimeCaps, inheritedAncestorRules, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef });
678
685
  const { onAsk, preToolContexts, blockedToolCalls, gateOutcomes, batchHaltRef, blockedTracked, incompleteSuspendAdapter, durableSuspendInfraReady, resourceSuspendEligible, usageGovernance, durableApproval, wiringManifest, parkLaneArmed, gateMachineryActive, hookIdentity, hookEnvFace } = prepareWiringManifest({ spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, effectivePolicy, handsEnabled, mcp, irreversibleTools, egressTools, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, lockedPreflight, peerLaneRefs, peerLaneActive, harness, pausedRef, frozenOnAsk, liveInheritedGate, delegation, toolRoster });
679
686
  if (fileHistoryBoundary !== undefined) {
680
687
  harness.on("tool_call", async () => {
@@ -685,8 +692,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
685
692
  const { askLane } = prepareAskLane({ gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash });
686
693
  const { saga } = prepareSuspendSaga({ gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, inheritedParentConstraints, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, autoModeIntent, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter });
687
694
  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 });
688
- const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedBash, shellGatedMonitor, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef });
689
- prepareGateStations({ askLane, parkAsk, tools, toolRosterDeltas: rosterSeat, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, irreversibilityTier, reversibilityProbes, abortController, shellGatedBash, shellGatedMonitor, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash });
695
+ const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef });
696
+ prepareGateStations({ askLane, parkAsk, tools, toolRosterDeltas: rosterSeat, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash });
690
697
  const { microCompact, charsPerToken } = prepareContextLane({ model, deps, sessionId, runId, hostTaskId, offloadStore, microCompactKnob, harness, batchHaltRef, gitStatusRef, requestLossyRef, trimPressureRef });
691
698
  const preparedHolder = {};
692
699
  const { cacheBreakDetector, cacheFingerprint, promptOverheadTokens, readTaskFile, normalizeAttachmentPath, isDedupStubResult, recentlyReadFiles, onCompactionApplied, detectExternalChanges, listBackgroundTasks, centerCompactionCandidate, effectiveReadFaceObserved, effectiveReadDenyObserved } = prepareTurnWiring({ deps, systemPromptSeat, harnessTools, fpRef, model, epochArtifactDigestForSnapshot, promptProfile, fableMitigations, systemBlocks, thinking, spec, sessionId, turnSnapshotRef, promptManifest, charsPerToken, handsEnabled, executionEnv, attachmentRootCanonical, additionalRootsCanonical, additionalReadRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, readFileStateForCheckpoint, denyNarrowingPolicy, abortController, hostTaskId, taskScope, buildAssembleInputs, centerAdoptionRef, harnessRef, epochDeclaredSections, carrierReadFace, readDenyAdditionsNormalized, preparedHolder });
@@ -30,7 +30,7 @@ import type { GateOutcome } from "../gate-outcome.js";
30
30
  import type { RunnerDeps, RuntimeCaps, TaskSpec, ToolEffect, ToolExecuteContext } from "../types.js";
31
31
  import { type UsageWindow } from "../usage-window-store.js";
32
32
  import type { ToolRoster } from "../tool-roster.js";
33
- import { type AutoModeArmReason, type WiringManifest, type WiringManifestMcpEntry } from "../wiring-manifest.js";
33
+ import { type AutoModeArmFact, type WiringManifest, type WiringManifestMcpEntry } from "../wiring-manifest.js";
34
34
  import type { AnnounceOnceSink, Prepared, PrepareResume, RunInternals, UsageGovernance } from "./contracts.js";
35
35
  export interface PrepareWiringManifestInput {
36
36
  /** borrowed-readonly — the REBOUND spec. Read: `onAsk` (the own seat), `onQuestion` (the question seam), `handsReadOnly` / `tools` (the ungated scan),
@@ -107,7 +107,7 @@ export interface PrepareWiringManifestInput {
107
107
  * source of the hand band's write-capable names for the ungated-write warning (what actually MOUNTED). */
108
108
  toolRoster: ToolRoster;
109
109
  /** borrowed-readonly — the arming outcome the caps phase minted (the manifest's auto-mode face). */
110
- autoModeArmReason: AutoModeArmReason;
110
+ autoModeArmReason: AutoModeArmFact;
111
111
  /** borrowed-readonly — the frozen preflight; only `mcp` (the declared servers the manifest face joins). */
112
112
  lockedPreflight: Pick<LockedPreflight, "mcp">;
113
113
  /** borrowed-mutable — the peer lane's late-bound facts. Writer here: `askEffective` ← the manifest's ask derivation (the
@@ -199,7 +199,7 @@ export function prepareWiringManifest(input) {
199
199
  memoryAdmissionWired: deps.memoryScopeAdmission !== undefined,
200
200
  retentionPolicyWired: deps.retentionPolicy !== undefined,
201
201
  ...(modelGateManifest !== undefined ? { modelGate: modelGateManifest } : {}),
202
- autoMode: { armed: autoModeArmReason === "armed", reason: autoModeArmReason },
202
+ autoMode: { ...autoModeArmReason },
203
203
  mcp: mcpManifestEntries(lockedPreflight.mcp, mcp.statuses),
204
204
  tools: toolRoster,
205
205
  });
@@ -11,6 +11,8 @@ export function runCompactionMachinery(input) {
11
11
  const u = msg?.usage;
12
12
  if (!u)
13
13
  return;
14
+ if (msg.usageMissing === true)
15
+ stats.usageMissing = true;
14
16
  const fam = cacheFamilyOf(m);
15
17
  const price = runner.deps.pricing?.[m.id] ?? modelCostToPricing(m.cost);
16
18
  const priced = isModelPriced(m, runner.deps.pricing);
@@ -77,8 +77,10 @@ export function createHarnessHandlers(input) {
77
77
  rs.degrade.recordDegraded({ from: deg.from, to: deg.to, reason: deg.reason, atTurn: stats.turns + 1, ...(deg.chain && { chain: deg.chain }) });
78
78
  }
79
79
  if (m.role === "assistant" && "usage" in m && m.usage) {
80
- if (m.usageMissing)
80
+ if (m.usageMissing) {
81
81
  rs.turn.turnUsageMissing = true;
82
+ stats.usageMissing = true;
83
+ }
82
84
  const u = m.usage;
83
85
  stats.tokens += u.totalTokens || 0;
84
86
  const { totalInputTokens: turnTotal, uncachedInputTokens: turnUncached, costMicroUsd: turnCostMicroUsd, } = usageCostMicroUsd(rs.telemetry.cacheFamily, u, rs.telemetry.pricing);
@@ -83,7 +83,7 @@ export function streamSettleBackstop(input) {
83
83
  ? { retryAfterMs: hinted }
84
84
  : {};
85
85
  })(),
86
- stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0 },
86
+ stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0, usageMissing: true },
87
87
  };
88
88
  emitTrace(entryTracer.tracer, () => ({
89
89
  kind: "task.end",
@@ -0,0 +1,86 @@
1
+ import type { Model } from "../../internal/llm.js";
2
+ import type { ToolSpec } from "../types.js";
3
+ import type { ToolFingerprintInput } from "../cache-break-detector.js";
4
+ /**
5
+ * The deferral GATE (design/36): which mounted tool names ship as placeholders on a request. Pure
6
+ * registry logic over names and sizes — no harness, no I/O. `tool-disclosure.ts` owns the placeholder
7
+ * and ToolSearch mechanics that act on the set this module decides; `prepare-defer-classify.ts` is the
8
+ * phase that assembles the inputs from the mounted face.
9
+ */
10
+ /** The deferral gate's threshold: the engine's candidate set is deferred when its inlined size reaches
11
+ * this fraction of the model's context window (that set measured, never the rest of the face). */
12
+ export declare const DEFER_THRESHOLD_FRACTION = 0.1;
13
+ /** The typed refusal a `deferMode` outside its declared domain (`"auto"` or absent) raises at the classifier —
14
+ * a bad value never selects an arm silently. */
15
+ export declare const DEFER_MODE_INVALID_CODE = "config.defer_mode_invalid";
16
+ /** The deferral gate's verdict — the two deferral lists, what the gate measured, and whether it opened. */
17
+ export interface DeferralGate {
18
+ /** The caller's LITERAL list: `TaskSpec.deferTools` names mounted on this face, minus the inline pins.
19
+ * Deferred on every request, whatever the gate says — an intentional deferral is not a size question. */
20
+ explicit: Set<string>;
21
+ /** The engine's SELF-SELECTED candidates: `ToolSpec.defer === true`, every remote-protocol name, built-ins
22
+ * that declare their own `defer`, and — under `deferMode: "auto"` — every caller spec; minus the inline
23
+ * pins and minus `explicit`. Deferred iff the gate opens. */
24
+ gated: Set<string>;
25
+ /** Σ inlined chars (name + description + serialized schema) over `gated` — the bytes the gate could reclaim. */
26
+ gatedChars: number;
27
+ /** `floor(contextTokens × fraction) × CHARS_PER_TOKEN`; the char reading of the token threshold. */
28
+ thresholdChars: number;
29
+ /** Whether `gated` is deferred on this request: `gatedChars ≥ thresholdChars`. */
30
+ open: boolean;
31
+ }
32
+ /**
33
+ * Deferral has TWO rules, and only two.
34
+ *
35
+ * ① THE CALLER'S LIST IS LITERAL. `TaskSpec.deferTools` (arriving as `callerDeferNames`, pre-filtered to
36
+ * mounted wire names, built-ins included) is deferred on every request; `alwaysLoad` (`ToolSpec.alwaysLoad`,
37
+ * `alwaysLoadNames`) is inlined on every request and is judged FIRST — a pinned name is in neither list
38
+ * and is never measured. Neither depends on the gate: they are the deployment's stated intent.
39
+ *
40
+ * ② THE ENGINE'S OWN CANDIDATES SIT BEHIND ONE THRESHOLD GATE. The candidates are declared: explicit
41
+ * `ToolSpec.defer === true` on a caller spec, every REMOTE-PROTOCOL tool name (MCP servers AND A2A peers —
42
+ * the caller unions them), and `builtinDeferNames` (mounted BUILT-INS that declare their own `defer`; a
43
+ * built-in ToolSpec never reaches `specs`, so its declaration arrives as a mounted name). `deferMode:
44
+ * "auto"` WIDENS the candidate set to every caller spec — undeclared caller tools included — and nothing
45
+ * else: it is not a second gate, and engine built-ins the deployment did not name are candidates under no
46
+ * setting. The gate decides the set all or nothing: deferred iff `gatedChars ≥ thresholdChars`, where the
47
+ * threshold is {@link DEFER_THRESHOLD_FRACTION} of the model's context window (`contextTokens ??
48
+ * contextWindow`, in chars at {@link CHARS_PER_TOKEN}). The measure is the candidate set ONLY — the bytes
49
+ * the gate could reclaim — never the caller's list, the pins, or non-candidates: a request whose weight
50
+ * sits elsewhere gains nothing from a ToolSearch round-trip, so nothing about it argues for one. A window
51
+ * of zero reads as a zero threshold (every candidate byte is over budget), so the candidates defer.
52
+ *
53
+ * Invariants: `explicit ∩ gated = ∅`; `deferred = explicit ∪ (open ? gated : ∅)`; a pinned name is in no
54
+ * list; the same inputs always yield the same verdict (a pure function of the request).
55
+ */
56
+ export declare function deferralGate(opts: {
57
+ specs: ReadonlyArray<ToolSpec>;
58
+ /** Every namespaced tool a remote protocol materialized for this task (MCP servers AND A2A peers —
59
+ * the caller unions them). The remote-roster shape (many tools, schemas the deployment does not
60
+ * control, drift between tasks) is the same for every protocol, so every protocol's names are
61
+ * candidates by being remote. Only an inline pin (or the caller's own list) takes a name out. */
62
+ protocolToolNames: ReadonlyArray<string>;
63
+ /** The MOUNTED face — the tools whose full definitions the request would carry. The measure reads
64
+ * each candidate's inlined size from here; a candidate absent from it weighs nothing. */
65
+ fullTools: ReadonlyArray<ToolFingerprintInput>;
66
+ /** `"auto"` widens the candidate set to every caller spec (see above). Any other value refuses. */
67
+ deferMode?: "auto";
68
+ model: Model;
69
+ /** Rule ① — `TaskSpec.deferTools`: ALREADY-MOUNTED wire names (built-ins included; `ToolSpec.defer`
70
+ * only reaches caller specs) the caller defers on every request. Caller pre-filters to mounted names. */
71
+ callerDeferNames?: ReadonlyArray<string>;
72
+ /** Rule ② — mounted BUILT-INS that declare their own `defer` (the engine's declaration, not the
73
+ * caller's), so they are candidates. Caller pre-filters to mounted names. */
74
+ builtinDeferNames?: ReadonlyArray<string>;
75
+ /** Rule ① — inline-pin names (`TaskSpec.alwaysLoadTools` + the MCP tools whose server declared
76
+ * `_meta["anthropic/alwaysLoad"]`, caller-collected; A2A has no such per-skill declaration, so the
77
+ * task-level list is a peer's only inline-keep channel): exempt from BOTH lists.
78
+ * `ToolSpec.alwaysLoad` rides the specs directly and needs no entry here. */
79
+ alwaysLoadNames?: ReadonlyArray<string>;
80
+ }): DeferralGate;
81
+ /**
82
+ * Decide which tool names are deferred: the caller's literal list, plus the gated candidates when the gate
83
+ * opens. THIS CONTRACT (with {@link deferralGate}) IS THE SINGLE AUTHORITY on the deferral sources and the
84
+ * gate; the `TaskSpec` / `RunnerDeps` JSDoc points here.
85
+ */
86
+ export declare function classifyDeferred(opts: Parameters<typeof deferralGate>[0]): Set<string>;
@@ -0,0 +1,57 @@
1
+ import { DEFAULT_CHARS_PER_TOKEN } from "../../internal/harness.js";
2
+ export const DEFER_THRESHOLD_FRACTION = 0.1;
3
+ const CHARS_PER_TOKEN = DEFAULT_CHARS_PER_TOKEN;
4
+ function inlinedChars(t) {
5
+ let schema = "";
6
+ try {
7
+ schema = JSON.stringify(t.parameters) ?? "";
8
+ }
9
+ catch {
10
+ schema = "";
11
+ }
12
+ return t.name.length + t.description.length + schema.length;
13
+ }
14
+ export const DEFER_MODE_INVALID_CODE = "config.defer_mode_invalid";
15
+ export function deferralGate(opts) {
16
+ if (opts.deferMode !== undefined && opts.deferMode !== "auto") {
17
+ const e = new Error(`RunnerDeps.deferMode: invalid value ${JSON.stringify(opts.deferMode)} — expected "auto" or omitted; a bad value refuses loudly, nothing falls back to a default.`);
18
+ e.code = DEFER_MODE_INVALID_CODE;
19
+ throw e;
20
+ }
21
+ const pinned = new Set(opts.alwaysLoadNames ?? []);
22
+ for (const s of opts.specs) {
23
+ if (s.alwaysLoad === true)
24
+ pinned.add(s.name);
25
+ }
26
+ const explicit = new Set();
27
+ for (const name of opts.callerDeferNames ?? [])
28
+ if (!pinned.has(name))
29
+ explicit.add(name);
30
+ const gated = new Set();
31
+ const select = (name) => {
32
+ if (!pinned.has(name) && !explicit.has(name))
33
+ gated.add(name);
34
+ };
35
+ for (const s of opts.specs)
36
+ if (s.defer === true || opts.deferMode === "auto")
37
+ select(s.name);
38
+ for (const name of opts.protocolToolNames)
39
+ select(name);
40
+ for (const name of opts.builtinDeferNames ?? [])
41
+ select(name);
42
+ let gatedChars = 0;
43
+ for (const t of opts.fullTools)
44
+ if (gated.has(t.name))
45
+ gatedChars += inlinedChars(t);
46
+ const windowTokens = Math.max(0, opts.model.contextTokens ?? opts.model.contextWindow ?? 0);
47
+ const thresholdChars = Math.floor(windowTokens * DEFER_THRESHOLD_FRACTION) * CHARS_PER_TOKEN;
48
+ return { explicit, gated, gatedChars, thresholdChars, open: gatedChars >= thresholdChars };
49
+ }
50
+ export function classifyDeferred(opts) {
51
+ const gate = deferralGate(opts);
52
+ const deferred = new Set(gate.explicit);
53
+ if (gate.open)
54
+ for (const n of gate.gated)
55
+ deferred.add(n);
56
+ return deferred;
57
+ }
@@ -1,8 +1,5 @@
1
1
  import { type TSchema } from "typebox";
2
2
  import type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode } from "../../internal/harness-types.js";
3
- import type { Model } from "../../internal/llm.js";
4
- import type { ToolSpec } from "../types.js";
5
- import type { ToolFingerprintInput } from "../cache-break-detector.js";
6
3
  /**
7
4
  * Deferred-tool dynamic disclosure (design/36). A deployment with hundreds of tools / several MCP
8
5
  * servers can't inline every full JSON Schema into every request — turn-1 cache-creation tokens
@@ -56,39 +53,6 @@ export declare function deferHint(description: string, max?: number): string;
56
53
  * announcement, anywhere. A bare interpolation is a prompt-injection hole (council BUG, design/36).
57
54
  */
58
55
  export declare function safeName(name: string): string;
59
- /**
60
- * Decide which tool names are deferred. THIS CONTRACT IS THE SINGLE AUTHORITY on the default-mode
61
- * source list (TaskSpec/RunnerDeps JSDoc points here), so every arm the body implements is named:
62
- * deterministic by default = explicit `ToolSpec.defer === true`, plus every REMOTE-PROTOCOL tool name
63
- * (MCP servers AND A2A peers — most numerous / most drift-prone), plus `deferNames`, i.e. the
64
- * already-mounted wire names from `TaskSpec.deferTools` and from BUILT-INS that declare their own
65
- * `defer` (a built-in ToolSpec never reaches `specs`, so its declared deferral has to arrive here as a
66
- * mounted name — this is why the shared-memory pair defaults to deferred). With `deferMode: "auto"`, ADDITIONALLY
67
- * defer the remaining user tools when their combined inlined schema would exceed ~10% of the model's
68
- * context window — an explicit opt-in (council minor #6), because an implicit threshold makes the same
69
- * TaskSpec yield different tools[] across models/contexts. Explicit `defer` always wins — except
70
- * against an alwaysLoad pin (RB-400-a), which exempts a name from every deferral source.
71
- */
72
- export declare function classifyDeferred(opts: {
73
- specs: ReadonlyArray<ToolSpec>;
74
- /** Every namespaced tool a remote protocol materialized for this task (MCP servers AND A2A peers —
75
- * the caller unions them). The constant-defer arm is about the SHAPE of a remote roster (many tools,
76
- * schemas the deployment does not control, drift between tasks), which is the same for every protocol
77
- * in the table: naming one protocol here left the next one inlined by omission, with nobody deciding
78
- * that. Only an alwaysLoad pin exempts a name. */
79
- protocolToolNames: ReadonlyArray<string>;
80
- fullTools: ReadonlyArray<ToolFingerprintInput>;
81
- deferMode?: "auto";
82
- model: Model;
83
- /** TaskSpec.deferTools — defer ALREADY-MOUNTED tools by wire name (built-ins included;
84
- * `ToolSpec.defer` only reaches caller specs). Caller pre-filters to mounted names. */
85
- deferNames?: ReadonlyArray<string>;
86
- /** RB-400-a — inline-pin names (`TaskSpec.alwaysLoadTools` + the MCP tools whose server declared
87
- * `_meta["anthropic/alwaysLoad"]`, caller-collected; A2A has no such per-skill declaration, so the
88
- * task-level list is a peer's only inline-keep channel): exempt from EVERY deferral source below.
89
- * `ToolSpec.alwaysLoad` rides the specs directly and needs no entry here. */
90
- alwaysLoadNames?: ReadonlyArray<string>;
91
- }): Set<string>;
92
56
  /** Build the registry the search ranks over (deferred tools only — non-deferred are already visible). */
93
57
  export declare function buildDeferredRegistry(deferred: ReadonlySet<string>, tools: ReadonlyArray<{
94
58
  name: string;
@@ -2,11 +2,8 @@ import { Type } from "typebox";
2
2
  import { Value } from "typebox/value";
3
3
  import { defineTool, errorResult } from "../tools.js";
4
4
  import { formatZodValidationError, truncateError } from "../tool-errors.js";
5
- import { DEFAULT_CHARS_PER_TOKEN } from "../../internal/harness.js";
6
5
  import { toolFace } from "../tool-catalog-entries.js";
7
6
  export const TOOL_SEARCH_NAME = "ToolSearch";
8
- const DEFER_AUTO_FRACTION = 0.1;
9
- const CHARS_PER_TOKEN = DEFAULT_CHARS_PER_TOKEN;
10
7
  export const TOOL_SEARCH_DEFAULT_MAX_RESULTS = 5;
11
8
  const MAX_QUERY_RESULTS = 25;
12
9
  export const DEFERRED_NO_PROGRESS_LIMIT = 3;
@@ -19,46 +16,6 @@ export function deferHint(description, max = 120) {
19
16
  export function safeName(name) {
20
17
  return name.replace(/[\x00-\x1f<>"`]/g, "");
21
18
  }
22
- function inlinedChars(t) {
23
- let schema = "";
24
- try {
25
- schema = JSON.stringify(t.parameters) ?? "";
26
- }
27
- catch {
28
- schema = "";
29
- }
30
- return t.description.length + schema.length;
31
- }
32
- export function classifyDeferred(opts) {
33
- const pinned = new Set(opts.alwaysLoadNames ?? []);
34
- for (const s of opts.specs) {
35
- if (s.alwaysLoad === true)
36
- pinned.add(s.name);
37
- }
38
- const deferred = new Set();
39
- for (const s of opts.specs) {
40
- if (s.defer === true && !pinned.has(s.name))
41
- deferred.add(s.name);
42
- }
43
- for (const name of opts.protocolToolNames)
44
- if (!pinned.has(name))
45
- deferred.add(name);
46
- for (const name of opts.deferNames ?? [])
47
- if (!pinned.has(name))
48
- deferred.add(name);
49
- if (opts.deferMode === "auto") {
50
- const callerNames = new Set(opts.specs.map((s) => s.name));
51
- const candidates = opts.fullTools.filter((t) => !deferred.has(t.name) && !pinned.has(t.name) && callerNames.has(t.name));
52
- const inlineFace = opts.fullTools.filter((t) => !deferred.has(t.name));
53
- const total = inlineFace.reduce((n, t) => n + inlinedChars(t), 0);
54
- const window = (opts.model.contextTokens ?? opts.model.contextWindow ?? 0) * CHARS_PER_TOKEN;
55
- if (window > 0 && total > DEFER_AUTO_FRACTION * window) {
56
- for (const t of candidates)
57
- deferred.add(t.name);
58
- }
59
- }
60
- return deferred;
61
- }
62
19
  export function buildDeferredRegistry(deferred, tools) {
63
20
  const reg = new Map();
64
21
  for (const t of tools) {