@sema-agent/core 7.12.0 → 7.14.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 (122) hide show
  1. package/CHANGELOG.md +124 -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 +83 -9
  11. package/dist/core/ask-origin.js +31 -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 +9 -16
  16. package/dist/core/checkpoint-store.js +7 -4
  17. package/dist/core/engine-notice.d.ts +21 -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.d.ts +0 -40
  22. package/dist/core/gate-lanes.js +24 -29
  23. package/dist/core/gate-outcome.d.ts +42 -3
  24. package/dist/core/gate-outcome.js +22 -0
  25. package/dist/core/governance-codes.d.ts +1 -1
  26. package/dist/core/governance-codes.js +3 -1
  27. package/dist/core/hooks.d.ts +6 -17
  28. package/dist/core/hooks.js +7 -5
  29. package/dist/core/mcp.d.ts +3 -2
  30. package/dist/core/mcp.js +5 -11
  31. package/dist/core/memory-engine/engine.d.ts +2 -2
  32. package/dist/core/runner/advertised-writable-dirs.d.ts +74 -0
  33. package/dist/core/runner/advertised-writable-dirs.js +48 -0
  34. package/dist/core/runner/assemble-result.d.ts +50 -1
  35. package/dist/core/runner/assemble-result.js +4 -1
  36. package/dist/core/runner/compaction-seams.d.ts +42 -0
  37. package/dist/core/runner/compaction-seams.js +80 -0
  38. package/dist/core/runner/contracts.d.ts +44 -6
  39. package/dist/core/runner/denial-limit-arms.d.ts +7 -9
  40. package/dist/core/runner/denial-limit-arms.js +11 -12
  41. package/dist/core/runner/gate-exit.d.ts +15 -3
  42. package/dist/core/runner/gate-exit.js +6 -4
  43. package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
  44. package/dist/core/runner/permission-rule-lanes.js +2 -1
  45. package/dist/core/runner/prepare-caps-and-workflow.d.ts +34 -4
  46. package/dist/core/runner/prepare-caps-and-workflow.js +47 -12
  47. package/dist/core/runner/prepare-gate-stations.d.ts +1 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +10 -4
  49. package/dist/core/runner/prepare-hands-readface.js +5 -14
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -3
  51. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  52. package/dist/core/runner/prepare-memory.d.ts +31 -34
  53. package/dist/core/runner/prepare-memory.js +73 -51
  54. package/dist/core/runner/prepare-policy-chain.js +8 -4
  55. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -0
  56. package/dist/core/runner/prepare-prompt-inputs.js +2 -2
  57. package/dist/core/runner/prepare-protocol-tools.js +5 -0
  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 +11 -16
  61. package/dist/core/runner/prepare-task.d.ts +9 -1
  62. package/dist/core/runner/prepare-task.js +63 -49
  63. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  64. package/dist/core/runner/prepare-wiring-manifest.js +1 -1
  65. package/dist/core/runner/resume-claim.d.ts +2 -2
  66. package/dist/core/runner/resume-preflight.d.ts +2 -2
  67. package/dist/core/runner/run-attachment-seats.d.ts +2 -2
  68. package/dist/core/runner/run-compaction-machinery.js +2 -0
  69. package/dist/core/runner/run-git-lane.d.ts +1 -1
  70. package/dist/core/runner/run-harness-handlers.js +5 -1
  71. package/dist/core/runner/run-identity-wiring.d.ts +7 -0
  72. package/dist/core/runner/run-identity-wiring.js +3 -1
  73. package/dist/core/runner/run-leg.d.ts +7 -5
  74. package/dist/core/runner/run-leg.js +256 -5
  75. package/dist/core/runner/run-notification-lane.d.ts +4 -3
  76. package/dist/core/runner/run-recovery-lanes.d.ts +4 -17
  77. package/dist/core/runner/run-recovery-lanes.js +5 -4
  78. package/dist/core/runner/run-settle-and-teardown.d.ts +14 -10
  79. package/dist/core/runner/run-settle-and-teardown.js +112 -4
  80. package/dist/core/runner/run-stop-and-final-verify.d.ts +2 -2
  81. package/dist/core/runner/run-terminal-adoption.d.ts +16 -16
  82. package/dist/core/runner/run-terminal-adoption.js +87 -7
  83. package/dist/core/runner/run-turn-boundary.js +5 -4
  84. package/dist/core/runner/runtask.d.ts +10 -90
  85. package/dist/core/runner/runtask.js +77 -596
  86. package/dist/core/runner/stream-lifecycle-verbs.js +10 -1
  87. package/dist/core/runner/stream-settle-backstop.js +1 -1
  88. package/dist/core/runtime-caps.d.ts +21 -0
  89. package/dist/core/runtime-caps.js +5 -1
  90. package/dist/core/task-event.d.ts +11 -2
  91. package/dist/core/task-registry-shared.js +8 -6
  92. package/dist/core/task-result.d.ts +15 -0
  93. package/dist/core/task-stream.d.ts +10 -1
  94. package/dist/core/terminal-cause.d.ts +6 -2
  95. package/dist/core/tool-face.d.ts +8 -0
  96. package/dist/core/tool-face.js +1 -0
  97. package/dist/core/tool-policy.d.ts +39 -31
  98. package/dist/core/tool-policy.js +29 -5
  99. package/dist/core/tool-registry.d.ts +11 -3
  100. package/dist/core/tool-registry.js +7 -1
  101. package/dist/core/tool-roster.d.ts +26 -0
  102. package/dist/core/tool-roster.js +40 -7
  103. package/dist/core/tool-spec.d.ts +13 -12
  104. package/dist/core/types.d.ts +2 -1
  105. package/dist/core/types.js +1 -0
  106. package/dist/core/wiring-manifest.d.ts +16 -9
  107. package/dist/core/wiring-manifest.js +8 -3
  108. package/dist/index.d.ts +8 -7
  109. package/dist/index.js +7 -6
  110. package/dist/orchestration/run-workflow-tool.d.ts +8 -8
  111. package/dist/orchestration/run-workflow-tool.js +1 -1
  112. package/dist/orchestration/workflow-primitives.d.ts +4 -3
  113. package/dist/orchestration/workflow-primitives.js +3 -3
  114. package/dist/orchestration/workflow-types.d.ts +14 -0
  115. package/dist/orchestration/workflow.d.ts +41 -4
  116. package/dist/orchestration/workflow.js +23 -5
  117. package/dist/server/http.js +2 -1
  118. package/dist/tools/fs/fs-shared.js +9 -3
  119. package/dist/tools/fs/read-deny.d.ts +24 -8
  120. package/dist/tools/fs/read-deny.js +20 -1
  121. package/package.json +2 -1
  122. package/test/export-surface.snapshot.json +35 -1
@@ -5,11 +5,12 @@ import { resolveCheckpointStore } from "../checkpoint-store.js";
5
5
  import { cloneObserverInput, resolveHookTimeoutMs } from "../hooks.js";
6
6
  import { isNamespacedCoveringRuleName, namespacedRuleNameCovers } from "../permission-rules.js";
7
7
  import { isIsolated } from "../remote-env.js";
8
- import { askApproverIdentity, checkToolPolicyProjection, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, isLiveApproverSeat, resolveAsk, toolPolicyNameSets } from "../tool-policy.js";
8
+ import { askApproverIdentity, carryEngineAttestations, carryPrecedingSettlement, checkToolPolicyProjection, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, isLiveApproverSeat, resolveAsk, toolPolicyNameSets } from "../tool-policy.js";
9
9
  import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
10
10
  import { NAMESPACED_NAME_SHAPES, protocolOf } from "../protocol-table.js";
11
11
  import { emitTrace } from "../trace.js";
12
12
  import { createActiveSkillScopePolicy } from "./active-skill-scope.js";
13
+ import { probeMandatedAsk } from "../ask-origin.js";
13
14
  import { headlessDenyAtFold, headlessDenyAtRecheck, inheritedAskCarry, judgeInheritedClassifier, lateAskSettlementObserver, settleDenialLimitFallback } from "./denial-limit-arms.js";
14
15
  import { askGrantShapeOf } from "./inherited-ask-grants.js";
15
16
  import { createPermissionRuleLanes, inheritedAskRuleEvidence, createRuleOffersOf } from "./permission-rule-lanes.js";
@@ -374,7 +375,7 @@ export async function preparePolicyChain(input) {
374
375
  if (judged.kind === "deny")
375
376
  return judged.result;
376
377
  const { ask: inheritedAsk, fallback, mintedHere } = judged;
377
- if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName)) {
378
+ if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName) && !probeMandatedAsk(inheritedAsk)) {
378
379
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
379
380
  return { action: "allow" };
380
381
  }
@@ -454,7 +455,7 @@ export async function preparePolicyChain(input) {
454
455
  if (judged.kind === "deny")
455
456
  return judged.result;
456
457
  const { ask: inheritedAsk, fallback, mintedHere } = judged;
457
- if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName)) {
458
+ if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName) && !probeMandatedAsk(inheritedAsk)) {
458
459
  recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
459
460
  return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput } : { action: "allow" };
460
461
  }
@@ -556,7 +557,10 @@ export async function preparePolicyChain(input) {
556
557
  for (const layer of recheckLayers) {
557
558
  const d = await layer.check(finalReq, signal);
558
559
  if (d.action === "deny") {
559
- return { ...d, updatedInput: decision.updatedInput };
560
+ const carried = { ...d, updatedInput: decision.updatedInput };
561
+ carryEngineAttestations(d, carried);
562
+ carryPrecedingSettlement(decision, carried);
563
+ return carried;
560
564
  }
561
565
  if (d.updatedInput !== undefined) {
562
566
  return {
@@ -28,6 +28,7 @@ import type { MaterializedMcp } from "../mcp.js";
28
28
  import type { ResolvedRole } from "../roles.js";
29
29
  import type { RunnerDeps, TaskSpec } from "../types.js";
30
30
  import type { PrepareResume, PromptFeatureFlags, RunInternals, ToolFaceSnapshot } from "./contracts.js";
31
+ import type { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
31
32
  import { type GitStatusLaneRef } from "./git-status-frame.js";
32
33
  export interface PreparePromptInputsInput {
33
34
  /** borrowed-readonly — the REBOUND spec (`spec′`). Read: `promptProvider`, `hooks` (flag), `systemPrompt`
@@ -90,6 +91,9 @@ export interface PreparePromptInputsInput {
90
91
  additionalRootsCanonical: readonly string[];
91
92
  /** borrowed-readonly — the canonical extra read-only roots (copied into the env facts). */
92
93
  additionalReadRootsCanonical: readonly string[];
94
+ /** borrowed-readonly — #691: the advertised-writable-directory seat (hands only). The scratchpad section is
95
+ * rendered only when the seat did not SKIP the scratchpad — a directory the fence refused is not advertised. */
96
+ advertisedWritableDirs?: AdvertisedWritableDirs;
93
97
  /** borrowed-readonly — the acquired session's id (operator-line context). */
94
98
  sessionId: string;
95
99
  }
@@ -4,7 +4,7 @@ import { probeGitStatusLane } from "./git-status-frame.js";
4
4
  import { fenceMcpServerInstructions } from "./turn-attachments.js";
5
5
  const ULTRA_REASONING_TIERS = new Set(["xhigh", "max"]);
6
6
  export async function preparePromptInputs(input) {
7
- const { spec, deps, failClosedReason, lockedPreflight, selfOrchestrationActive, executionEnv, reminderMark, resolvedReadFace, toolFaceSnapshot, promptProfile, fableMitigations, internals, thinking, ownedEnv, delegation, resolvedRole, mcp, model, externalContentTargetActive, resume, handsEnabled, taskRootFinal, additionalRootsCanonical, additionalReadRootsCanonical, sessionId } = input;
7
+ const { spec, deps, failClosedReason, lockedPreflight, selfOrchestrationActive, executionEnv, reminderMark, resolvedReadFace, toolFaceSnapshot, promptProfile, fableMitigations, internals, thinking, ownedEnv, delegation, resolvedRole, mcp, model, externalContentTargetActive, resume, handsEnabled, taskRootFinal, additionalRootsCanonical, additionalReadRootsCanonical, sessionId, advertisedWritableDirs } = input;
8
8
  const provider = spec.promptProvider ?? deps.promptProvider ?? defaultPromptProvider;
9
9
  const promptPolicyEnabled = Boolean(lockedPreflight.toolPolicy);
10
10
  const promptHooks = spec.hooks ?? deps.hooks;
@@ -51,7 +51,7 @@ export async function preparePromptInputs(input) {
51
51
  envFacts.sandboxPkgSource = spec.envFacts.pkgSource;
52
52
  if (spec.envFacts?.egress === "none" || spec.envFacts?.egress === "allowlist" || spec.envFacts?.egress === "full")
53
53
  envFacts.sandboxEgress = spec.envFacts.egress;
54
- if (spec.envFacts?.scratchpadDir)
54
+ if (spec.envFacts?.scratchpadDir && advertisedWritableDirs?.verdictOf("scratchpad") !== "skipped")
55
55
  envFacts.scratchpadDir = spec.envFacts.scratchpadDir;
56
56
  if (externalContentTargetActive)
57
57
  envFacts.externalContentTarget = true;
@@ -3,12 +3,17 @@ import { materializeMcpTools } from "../mcp.js";
3
3
  import { materializeA2aTools } from "../a2a.js";
4
4
  import { deliverEngineNotice } from "../types.js";
5
5
  import { RosterBuilder } from "../tool-roster.js";
6
+ import { TOOL_WIRE_NAME_MAX_CHARS } from "../tool-face.js";
6
7
  import { REFRESH_MCP_TOOLS_TOOL_NAME, toolFace } from "../tool-catalog-entries.js";
7
8
  import { MCP_NAMESPACE, A2A_NAMESPACE } from "../protocol-table.js";
8
9
  import { mintNamespacePrefix } from "../protocol-naming.js";
9
10
  import { applyMcpToolFaces } from "./tool-face-overlay.js";
10
11
  export async function prepareProtocolTools(input) {
11
12
  const { lockedPreflight, spec, deps, reminderMark, reminderDisclosureCounts, runId, sessionId, onceLedger, roster, rosterSeat, remoteToolOffload, toolFaceSnapshot, toolEffects, axisExplicitNegatives, irreversibilityTier, irreversibleTools, egressTools, abortController, rollback } = input;
13
+ const overPeer = (lockedPreflight.mcp ?? []).find((s) => s.name.length > TOOL_WIRE_NAME_MAX_CHARS);
14
+ if (overPeer !== undefined) {
15
+ throw Object.assign(new Error(`MCP server name ${JSON.stringify(overPeer.name.slice(0, 40) + "…")} is ${overPeer.name.length} characters; a peer rides every roster row of its tools and the roster's bound on a name is ${TOOL_WIRE_NAME_MAX_CHARS}.`), { code: "config.tool_name_too_long" });
16
+ }
12
17
  const mcp = lockedPreflight.mcp?.length
13
18
  ? await materializeMcpTools(lockedPreflight.mcp, spec.principal, deps.onElicit, deps.mcpImageResizer, { reminderMark, counts: reminderDisclosureCounts }, mcpRevocationWiring(deps, runId))
14
19
  : { tools: [], toolAxes: [], warnings: [], serverInstructions: [], instructionsDelta: { pendingAdds: [], pendingRemovals: [] }, droppedTools: [], statuses: [], refresh: async () => [], dispose: async () => { } };
@@ -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 &&
@@ -1,7 +1,6 @@
1
1
  import { NAMESPACED_NAME_SHAPES } from "../protocol-table.js";
2
2
  import { engineCardTypes, handBandEffects } from "../tool-registry.js";
3
- import { TOOL_CONTRACT_MAX_CHARS, TOOL_WIRE_NAME_MAX_CHARS } from "../tool-face.js";
4
- import { callerCardIdOf, offendingResultCard, schemaKeyBoundProblem } from "../tool-roster.js";
3
+ import { callerCardIdOf, offendingResultCard, rosterMemberBoundProblem, schemaKeyBoundProblem } from "../tool-roster.js";
5
4
  import { toolFaceProblem } from "./tool-face-overlay.js";
6
5
  import { OFFLOAD_TOOL_NAME } from "../tool-result-store.js";
7
6
  import { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "../present-plan-tool.js";
@@ -16,23 +15,19 @@ export function prepareSafetyScan(input) {
16
15
  const axisExplicitNegatives = new Map();
17
16
  const reversibilityProbes = new Map();
18
17
  for (const t of spec.tools ?? []) {
19
- if (t.name.length > TOOL_WIRE_NAME_MAX_CHARS) {
20
- const e = new Error(`Tool name "${t.name.slice(0, 40)}…" is invalid: ${t.name.length} characters, the roster's bound is ${TOOL_WIRE_NAME_MAX_CHARS}.`);
21
- e.code = "config.tool_name_too_long";
22
- throw e;
23
- }
24
- for (const alias of t.aliases ?? []) {
25
- if (alias.length > TOOL_WIRE_NAME_MAX_CHARS) {
26
- const e = new Error(`Tool alias "${alias.slice(0, 40)}…" (of "${t.name}") is invalid: ${alias.length} characters, the roster's bound is ${TOOL_WIRE_NAME_MAX_CHARS}.`);
27
- e.code = "config.tool_name_too_long";
18
+ {
19
+ const bound = rosterMemberBoundProblem({
20
+ name: t.name,
21
+ aliases: t.aliases,
22
+ ...(t.contract !== undefined ? { contractId: t.contract.contractId, implementationRevision: t.contract.implementationRevision } : {}),
23
+ ...(typeof t.modelGate === "string" ? { modelGate: t.modelGate } : {}),
24
+ });
25
+ if (bound !== undefined) {
26
+ const e = new Error(`A caller tool cannot ride a roster row: ${bound.message}.`);
27
+ e.code = bound.code;
28
28
  throw e;
29
29
  }
30
30
  }
31
- if (t.contract !== undefined && (t.contract.contractId.length > TOOL_CONTRACT_MAX_CHARS || t.contract.implementationRevision.length > TOOL_CONTRACT_MAX_CHARS)) {
32
- const e = new Error(`Tool "${t.name}" declares a contract member over ${TOOL_CONTRACT_MAX_CHARS} characters (contractId ${t.contract.contractId.length}, implementationRevision ${t.contract.implementationRevision.length}) — the roster's bound.`);
33
- e.code = "config.tool_contract_too_long";
34
- throw e;
35
- }
36
31
  {
37
32
  const keyProblem = schemaKeyBoundProblem(t.parameters);
38
33
  if (keyProblem !== undefined) {
@@ -8,7 +8,7 @@ export { mcpManifestEntries } from "./prepare-wiring-manifest.js";
8
8
  export { __resetMaterializeEnvAnnouncements } from "./prepare-tool-disclosure-mount.js";
9
9
  export { fileHistoryFilesystemIdentity, resolveFileHistoryScope } from "./prepare-file-history.js";
10
10
  import type { RunnerSelfSeat } from "./contracts.js";
11
- import type { Prepared, PrepareResume, RunInternals } from "./contracts.js";
11
+ import type { Prepared, PrepareResume, PrepareTaskFn, RunInternals } from "./contracts.js";
12
12
  export type { FileHistoryBoundarySeat, InheritedGate, Prepared, PreparedMicroCompact, PrepareResume, ResolvedWorkspace, RunInternals, UsageGovernance } from "./contracts.js";
13
13
  import type { ExecutionEnv } from "../../internal/harness.js";
14
14
  import type { RunnerDeps, TaskSpec } from "../types.js";
@@ -114,3 +114,11 @@ runnerSelf?: RunnerSelfSeat,
114
114
  runIdSink?: {
115
115
  runId?: string;
116
116
  }): Promise<Prepared>;
117
+ /** design/393 S7 (#675) — the entry IS the contract the notification lane's seat names ({@link PrepareTaskFn}, contracts.ts):
118
+ * a signature change reds HERE, at the declaration, as `false` failing the `true` bound. IDENTITY, not assignability (review
119
+ * round 1): the parameter tuple (arity, optionality, each type) and the return type must be the SAME type in both directions —
120
+ * one-way `extends` let a dropped trailing parameter, an added optional one or a narrowed return slip through. Type-level only —
121
+ * nothing is emitted. */
122
+ type Holds<T extends true> = T;
123
+ type Same<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
124
+ export type PrepareTaskIsTheContract = Holds<Same<Parameters<typeof prepareTask>, Parameters<PrepareTaskFn>>> & Holds<Same<ReturnType<typeof prepareTask>, ReturnType<PrepareTaskFn>>> & Holds<Same<ThisParameterType<typeof prepareTask>, ThisParameterType<PrepareTaskFn>>> & Holds<Same<typeof prepareTask, PrepareTaskFn>>;
@@ -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,6 +259,7 @@ 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);
@@ -366,8 +369,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
366
369
  let resolvedReadFace;
367
370
  let readDenyAdditionsNormalized = [];
368
371
  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 });
372
+ 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 });
373
+ 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
374
  const tools = roster.tools;
372
375
  const pausedRef = {};
373
376
  const remoteEnvFailures = [];
@@ -429,35 +432,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
429
432
  const rosterSeat = new ToolRosterDeltaSeat();
430
433
  const { mcp, a2a, rebuildHarnessToolsRef, contentOriginWrapRef, toolCallGateArmedRef } = await prepareProtocolTools({ lockedPreflight, spec, deps, reminderMark, reminderDisclosureCounts, runId, sessionId, onceLedger, roster, rosterSeat, remoteToolOffload, toolFaceSnapshot, toolEffects, axisExplicitNegatives, irreversibilityTier, irreversibleTools, egressTools, abortController, rollback });
431
434
  const callIssuedAtRef = {};
435
+ const workCutRef = { continuation: false, abortedBeforeTurnEnd: false };
432
436
  const memoryWriteGateRef = {};
433
- 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 } : {}) };
434
- const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
435
- const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher, envHandToolNames, sealReadStateSeat } = handsReadFace;
436
- resolvedReadFace = handsReadFace.resolvedReadFace;
437
- readDenyAdditionsNormalized = handsReadFace.readDenyAdditionsNormalized;
438
- handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
439
- shellGatedBash = handsReadFace.shellGatedBash;
440
- shellGatedMonitor = handsReadFace.shellGatedMonitor;
441
- 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 });
442
- const { checkpointStore, liveQuestionFace, contentAskBindings, contentAskRoutable, lateStrandedAnswers, discloseStrandedAnswers, settleContentAskBindings, durableQuestionFace, questionToolMounted, mountedQuestionTool } = prepareQuestionFace({ frozenOnQuestion, spec, deps, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks: () => inheritedUnavailableAsks });
443
- if (spec.handsReadOnly !== true) {
444
- const sessionScope = sessionId ?? spec.principal ?? spec.taskId ?? "default";
445
- roster.mountAll(createSchedulerTools(executionEnv, {
446
- scope: sessionScope,
447
- ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
448
- ...(sessionId !== undefined ? { sessionId } : {}),
449
- requestStopAfterTurn,
450
- }).map((t) => (envHandToolNames.add(t.name), t)), { source: "builtin", mountedBy: "scheduler" });
451
- }
452
- if (internals?.insideFork !== true &&
453
- runnerSelf &&
454
- sessionId !== undefined &&
455
- hasSessionFork(runnerSelf.sessions) &&
456
- agentForkDenial === "principal") {
457
- deps.onError?.(new Error(`Fork DENIED for principal "${spec.principal ?? ""}" by runtimeCaps.allowFork=false ` +
458
- `(per-principal entitlement governance, not a misconfiguration — Agent(subagent_type:"fork") will refuse honestly)`), { phase: "config", sessionId });
459
- }
460
- const { lspDiagnostics, nudgeLspOnEdit, lspRunIdent } = prepareLsp({ spec, deps, executionEnv, taskRootFinal, handsEnabled, sessionId, handsCwdRef, roster, envHandToolNames });
437
+ const advertisedWritableDirs = handsEnabled
438
+ ? new AdvertisedWritableDirs(executionEnv, (dir, reason) => deps.onError?.(new Error(`${dir.advertiser} advertised writable directory skipped (cannot canonicalize): ${dir.path} — ${reason}`), { phase: "config", sessionId }))
439
+ : undefined;
440
+ const handsWriteHalfPlanned = handsEnabled && spec.handsReadOnly !== true;
461
441
  const memoryPairNameDomain = new Set();
462
442
  for (const t of tools) {
463
443
  memoryPairNameDomain.add(t.name);
@@ -476,14 +456,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
476
456
  return { fault: true };
477
457
  }
478
458
  })();
479
- const { memoryEngineSession, memoryBlock: memoryBlockFromEngine, memoryTools, recallDisciplineSegment: memoryRecallSegment, seedFiles: memorySeedFiles, admittedOrgScopes: memoryAdmittedOrgScopes, ownOrgVerdict: memoryOwnOrgVerdict, effectiveMemoryScopes: memoryEffectiveScopes, } = await prepareMemory({
459
+ const { memoryEngineSession, memoryTools, composeInjection: composeMemoryInjection, admittedOrgScopes: memoryAdmittedOrgScopes, ownOrgVerdict: memoryOwnOrgVerdict, effectiveMemoryScopes: memoryEffectiveScopes, } = await prepareMemory({
480
460
  spec,
481
461
  deps,
482
462
  sessionId,
483
463
  taskRootPath: taskRootFinal,
484
464
  memoryWriteGateRef,
485
465
  reminderMark,
486
- writeToolsMounted: tools.some((t) => t.name === "Write") &&
466
+ writeToolsMounted: (handsWriteHalfPlanned || tools.some((t) => t.name === "Write")) &&
487
467
  !(toolFaceSnapshot.exclude?.includes("Write") ?? false) &&
488
468
  !(handsEnabled && isRemoteExecutionEnv(executionEnv) && spec.memoryPersistenceCapable !== true),
489
469
  memoryProvenance,
@@ -502,20 +482,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
502
482
  ...(internals?.memoryCaptureQueryDir !== undefined ? { parentCaptureQueryDir: internals.memoryCaptureQueryDir } : {}),
503
483
  ...(internals?.memoryCaptureAncestors !== undefined ? { captureAncestors: internals.memoryCaptureAncestors } : {}),
504
484
  memoryPersistenceDeclared: spec.memoryPersistenceCapable,
505
- rosterCanPersist: spec.memoryPersistenceCapable ??
506
- tools.some((t) => {
507
- if (toolFaceSnapshot.exclude?.includes(t.name) ?? false)
508
- return false;
509
- const effect = (t.effect ?? toolEffects.get(t.name) ?? "write");
510
- if (effect === "read")
511
- return false;
512
- const face = faceOf(t);
513
- const pt = face.pathTarget ?? pathTargetOf(t.name);
514
- const family = face.family ?? catalogEntriesNamed(t.name)[0]?.face.family;
515
- if (!((pt !== undefined && pt.access !== "read") || family === "shell"))
516
- return false;
517
- return !(handsEnabled && isRemoteExecutionEnv(executionEnv));
518
- }),
485
+ ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}),
519
486
  memorySearchToolsPlanned,
520
487
  admissionCtx: {
521
488
  orgMemoryDenied: complianceDenies.has("org_memory_mount"),
@@ -539,6 +506,34 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
539
506
  if (internals?.ownOrgAdmissionRef !== undefined && ownOrgVerdictRef.current !== undefined) {
540
507
  internals.ownOrgAdmissionRef.current = ownOrgVerdictRef.current;
541
508
  }
509
+ 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 } : {}) };
510
+ const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
511
+ const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher, envHandToolNames, sealReadStateSeat } = handsReadFace;
512
+ resolvedReadFace = handsReadFace.resolvedReadFace;
513
+ readDenyAdditionsNormalized = handsReadFace.readDenyAdditionsNormalized;
514
+ handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
515
+ shellGatedBash = handsReadFace.shellGatedBash;
516
+ shellGatedMonitor = handsReadFace.shellGatedMonitor;
517
+ 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 });
518
+ const { liveQuestionFace, contentAskBindings, contentAskRoutable, lateStrandedAnswers, discloseStrandedAnswers, settleContentAskBindings, durableQuestionFace, questionToolMounted, mountedQuestionTool } = prepareQuestionFace({ frozenOnQuestion, spec, deps, checkpointStore, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks: () => inheritedUnavailableAsks });
519
+ if (spec.handsReadOnly !== true) {
520
+ const sessionScope = sessionId ?? spec.principal ?? spec.taskId ?? "default";
521
+ roster.mountAll(createSchedulerTools(executionEnv, {
522
+ scope: sessionScope,
523
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
524
+ ...(sessionId !== undefined ? { sessionId } : {}),
525
+ requestStopAfterTurn,
526
+ }).map((t) => (envHandToolNames.add(t.name), t)), { source: "builtin", mountedBy: "scheduler" });
527
+ }
528
+ if (internals?.insideFork !== true &&
529
+ runnerSelf &&
530
+ sessionId !== undefined &&
531
+ hasSessionFork(runnerSelf.sessions) &&
532
+ agentForkDenial === "principal") {
533
+ deps.onError?.(new Error(`Fork DENIED for principal "${spec.principal ?? ""}" by runtimeCaps.allowFork=false ` +
534
+ `(per-principal entitlement governance, not a misconfiguration — Agent(subagent_type:"fork") will refuse honestly)`), { phase: "config", sessionId });
535
+ }
536
+ const { lspDiagnostics, nudgeLspOnEdit, lspRunIdent } = prepareLsp({ spec, deps, executionEnv, taskRootFinal, handsEnabled, sessionId, handsCwdRef, roster, envHandToolNames });
542
537
  if (internals?.delegationProvenance !== undefined && resume !== undefined) {
543
538
  const live = internals.delegationProvenance.ref.current;
544
539
  const seed = resume.seed.delegationProvenance;
@@ -552,6 +547,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
552
547
  live.incomplete = true;
553
548
  }
554
549
  }
550
+ const rosterCanPersist = spec.memoryPersistenceCapable ??
551
+ tools.some((t) => {
552
+ if (toolFaceSnapshot.exclude?.includes(t.name) ?? false)
553
+ return false;
554
+ const effect = (t.effect ?? toolEffects.get(t.name) ?? "write");
555
+ if (effect === "read")
556
+ return false;
557
+ const face = faceOf(t);
558
+ const pt = face.pathTarget ?? pathTargetOf(t.name);
559
+ const family = face.family ?? catalogEntriesNamed(t.name)[0]?.face.family;
560
+ if (!((pt !== undefined && pt.access !== "read") || family === "shell"))
561
+ return false;
562
+ return !(handsEnabled && isRemoteExecutionEnv(executionEnv));
563
+ });
564
+ const { memoryBlock: memoryBlockFromEngine, recallDisciplineSegment: memoryRecallSegment, seedFiles: memorySeedFiles } = composeMemoryInjection({ rosterCanPersist });
555
565
  if (memorySeedFiles?.length && seedContextFiles) {
556
566
  try {
557
567
  await seedContextFiles(memorySeedFiles);
@@ -562,7 +572,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
562
572
  }
563
573
  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 });
564
574
  const { memoryBlock: memoryBlockWithProject, instructionSources, projectInstructionContent, hasSkillManifest, skillsListing, sharedMemoryPairMounted, memoryEnginePairMounted } = projectContext;
565
- 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 });
575
+ 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 } : {}) });
566
576
  const { deferred, memoryBlock } = prepareDeferClassify({ spec, deps, roster, toolFaceSnapshot, handsEnabled, fullShellReachable, promptProfile, model, mcp, a2a, sharedMemoryPairMounted, memoryEnginePairMounted, memoryBlock: memoryBlockWithProject, memoryRecallSegment, onceLedger, sessionId });
567
577
  prepareMemoryEngineSession({ memoryEngineSession, internals, memoryDelegationEvidence, externalContentTargetActive, lockedPreflight, mcp, sessionId, runId, deps, provenanceForChildrenRef, delegationSettlementRef, spec, envHandToolNames, roster, contentOriginWrapRef });
568
578
  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 });
@@ -617,6 +627,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
617
627
  },
618
628
  ...(spec.resilience !== undefined ? { resilience: spec.resilience } : {}),
619
629
  loopTrace: (step) => {
630
+ if (step.kind === "continue")
631
+ workCutRef.continuation = true;
632
+ else
633
+ workCutRef.continuation = workCutRef.continuation && step.reason === "aborted_before_stream";
620
634
  if (step.kind !== "continue")
621
635
  return;
622
636
  if (step.reason === "next_turn" || step.reason === "steer_injected" || step.reason === "follow_up_injected")
@@ -686,7 +700,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
686
700
  const preparedHolder = {};
687
701
  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 });
688
702
  await onceLedger.settle(session, announcedListingsRef);
689
- const buildPrepared = () => ({ harness, session, sessionId, runId, reminderMark, reminderDisclosureCounts, editedFilesSnapshot, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, mcp, ...(a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, gateOutcomes, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(fileHistoryBoundary !== undefined ? { fileHistoryBoundary } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), ...(sealReadStateSeat !== undefined ? { sealReadStateSeat } : {}), denyNarrowingPolicy, ...(permissionRuleLane !== undefined ? { persistedRuleLane: permissionRuleLane } : {}), ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, pausedRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, gateStopRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolRoster, toolRosterDeltas: rosterSeat, structuredProjector, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, microCompact, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, stopRequestedRef, announcedSnapshotRecovered: onceLedger.recovered, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(projectInstructionContent !== undefined ? { projectInstructionContent } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
703
+ const buildPrepared = () => ({ harness, session, sessionId, runId, reminderMark, reminderDisclosureCounts, editedFilesSnapshot, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, mcp, ...(a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, gateOutcomes, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(fileHistoryBoundary !== undefined ? { fileHistoryBoundary } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), ...(sealReadStateSeat !== undefined ? { sealReadStateSeat } : {}), denyNarrowingPolicy, ...(permissionRuleLane !== undefined ? { persistedRuleLane: permissionRuleLane } : {}), ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, pausedRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, workCutRef, brainCallGuardrailRef, gateStopRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolRoster, toolRosterDeltas: rosterSeat, structuredProjector, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, microCompact, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, stopRequestedRef, announcedSnapshotRecovered: onceLedger.recovered, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(projectInstructionContent !== undefined ? { projectInstructionContent } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
690
704
  rollback.commit();
691
705
  const prepared = buildPrepared();
692
706
  preparedHolder.current = prepared;
@@ -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
  });
@@ -9,7 +9,7 @@
9
9
  * — the post-consume hook fires on the tick the win was read — and returns what that rung returns.
10
10
  */
11
11
  import { type Checkpoint, type CheckpointStore, type CheckpointToken } from "../checkpoint-store.js";
12
- import type { InheritedGate, ResumeRun } from "./contracts.js";
12
+ import type { InheritedGate, InstallingPhaseResult, ResumeRun } from "./contracts.js";
13
13
  export interface ResumeClaimInput<R> {
14
14
  /** borrowed-readonly — the checkpoint token being claimed. */
15
15
  token: CheckpointToken;
@@ -27,6 +27,6 @@ export interface ResumeClaimInput<R> {
27
27
  next: (claimed: ResumeClaimResult) => Promise<R>;
28
28
  }
29
29
  /** The claim's completion is its whole result: a lost CAS throws, so reaching `next` IS the win. */
30
- export interface ResumeClaimResult {
30
+ export interface ResumeClaimResult extends InstallingPhaseResult {
31
31
  }
32
32
  export declare function resumeClaim<R>(input: ResumeClaimInput<R>): Promise<R>;
@@ -10,7 +10,7 @@
10
10
  * returns what that rung returns.
11
11
  */
12
12
  import { type Checkpoint, type CheckpointStore, type CheckpointToken } from "../checkpoint-store.js";
13
- import type { InheritedGate, RunnerDepsSeat, SuspendReap } from "./contracts.js";
13
+ import type { InheritedGate, InstallingPhaseResult, RunnerDepsSeat, SuspendReap } from "./contracts.js";
14
14
  export interface ResumePreflightInput<R> {
15
15
  /** borrowed-readonly — the checkpoint token (handed to the deployment and to the settling CAS). */
16
16
  token: CheckpointToken;
@@ -35,6 +35,6 @@ export interface ResumePreflightInput<R> {
35
35
  next: (screened: ResumePreflightResult) => Promise<R>;
36
36
  }
37
37
  /** The preflight's completion is its whole result: every refusal throws, so reaching `next` IS the fact. */
38
- export interface ResumePreflightResult {
38
+ export interface ResumePreflightResult extends InstallingPhaseResult {
39
39
  }
40
40
  export declare function resumePreflight<R>(input: ResumePreflightInput<R>): Promise<R>;
@@ -1,5 +1,5 @@
1
1
  import type { TaskSpec } from "../types.js";
2
- import type { Prepared, ResumeRun, RunnerDepsSeat, RunState } from "./contracts.js";
2
+ import type { InstallingPhaseResult, Prepared, ResumeRun, RunnerDepsSeat, RunState } from "./contracts.js";
3
3
  export interface RunAttachmentSeatsInput<R> {
4
4
  /** borrowed-readonly — the task spec: the attachments config and the `finalVerification` opt-in. */
5
5
  spec: TaskSpec;
@@ -17,6 +17,6 @@ export interface RunAttachmentSeatsInput<R> {
17
17
  next: (seats: RunAttachmentSeatsResult) => Promise<R>;
18
18
  }
19
19
  /** Nothing comes back: the lane's products are the `counters` / `attach` groups it wrote on the borrowed run state. */
20
- export interface RunAttachmentSeatsResult {
20
+ export interface RunAttachmentSeatsResult extends InstallingPhaseResult {
21
21
  }
22
22
  export declare function runAttachmentSeats<R>(input: RunAttachmentSeatsInput<R>): Promise<R>;
@@ -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);
@@ -52,7 +52,7 @@ export interface RunGitLaneInput {
52
52
  runner: RunnerDepsSeat;
53
53
  }
54
54
  export interface RunGitLaneResult {
55
- /** The parked-mirror flush — awaited at the serialization points (the owed-frame retry here; prompt settle in the driver). */
55
+ /** The parked-mirror flush — awaited at the serialization points: the `turn_boundary` subscription here is the regular station (the final boundary of a gracefully ending run included); the leg's post-prompt call is the fallback for a terminal that skipped it. */
56
56
  flushGitMirror: () => Promise<void>;
57
57
  /** The owed-frame retry's `turn_boundary` subscription; the driver's teardown releases it first. */
58
58
  unsubGitRetry: () => void;
@@ -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);
@@ -281,6 +283,8 @@ export function createHarnessHandlers(input) {
281
283
  };
282
284
  const onTurnEnd = () => {
283
285
  stats.turns += 1;
286
+ if (prepared.abortController.signal.aborted)
287
+ prepared.workCutRef.abortedBeforeTurnEnd = true;
284
288
  const stopExtra = rs.turn.turnStopReason !== undefined ? { stopReason: rs.turn.turnStopReason } : {};
285
289
  queue.push(rs.turn.turnUsage
286
290
  ? { type: "turn_end", usage: rs.turn.turnUsage, ...(rs.turn.turnUsageMissing ? { usageMissing: true } : {}), ...stopExtra, ...ident() }
@@ -73,5 +73,12 @@ export interface RunIdentityWiringResult {
73
73
  followUp: number;
74
74
  } | undefined;
75
75
  };
76
+ /** #677 — did the run END with a person's steer / follow-up still pending (queued, or drained at the final boundary and
77
+ * never served)? A READ FACE over this lane's own `let`, written by the same undrained-inputs sink on BOTH its arms
78
+ * (park or not): the terminal-adoption lane folds it into the answer-settled verdict — a "stop" answer the person had
79
+ * already asked to continue past is not a settled answer, so a stop that cut that continuation is a stop on work. */
80
+ userInputLostAtEnd: {
81
+ readonly current: boolean;
82
+ };
76
83
  }
77
84
  export declare function runIdentityWiring(input: RunIdentityWiringInput): RunIdentityWiringResult;