@sema-agent/core 5.63.0 → 5.65.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 (56) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/agents/cascade.d.ts +5 -1
  3. package/dist/agents/cascade.js +6 -1
  4. package/dist/agents/subagent.js +1 -0
  5. package/dist/agents/verify.d.ts +5 -1
  6. package/dist/agents/verify.js +5 -2
  7. package/dist/core/checkpoint-store.d.ts +100 -5
  8. package/dist/core/fs-write-gate-policy.d.ts +21 -0
  9. package/dist/core/fs-write-gate-policy.js +14 -3
  10. package/dist/core/hooks.d.ts +9 -1
  11. package/dist/core/hooks.js +1 -1
  12. package/dist/core/memory-engine/dual-root.js +3 -0
  13. package/dist/core/memory-engine/engine.d.ts +1 -0
  14. package/dist/core/memory-engine/engine.js +9 -5
  15. package/dist/core/memory-engine/types.d.ts +16 -0
  16. package/dist/core/remote-env.d.ts +34 -2
  17. package/dist/core/runner/prepare-hands-readface.d.ts +9 -0
  18. package/dist/core/runner/prepare-hands-readface.js +4 -1
  19. package/dist/core/runner/prepare-memory.js +1 -1
  20. package/dist/core/runner/prepare-task.d.ts +31 -0
  21. package/dist/core/runner/prepare-task.js +53 -18
  22. package/dist/core/runner/prepare-workspace-restore.js +13 -0
  23. package/dist/core/runner/runtask.d.ts +18 -0
  24. package/dist/core/runner/runtask.js +173 -20
  25. package/dist/core/stub-env.d.ts +4 -0
  26. package/dist/core/stub-env.js +1 -0
  27. package/dist/core/task-registry-shared.js +20 -2
  28. package/dist/core/types.d.ts +104 -11
  29. package/dist/core/usage-window-store.d.ts +44 -12
  30. package/dist/core/usage-window-store.js +11 -3
  31. package/dist/core/workflow-run-store-contract.js +30 -0
  32. package/dist/core/workflow-run-store.d.ts +39 -1
  33. package/dist/core/workflow-run-store.js +2 -0
  34. package/dist/engine/harness/agent-harness.d.ts +8 -3
  35. package/dist/engine/harness/agent-harness.js +9 -4
  36. package/dist/engine/harness/types.d.ts +107 -3
  37. package/dist/engine/loop/agent-loop.js +39 -15
  38. package/dist/engine/loop/types.d.ts +43 -22
  39. package/dist/index.d.ts +2 -2
  40. package/dist/internal/harness-types.d.ts +1 -1
  41. package/dist/orchestration/run-workflow-tool.d.ts +6 -0
  42. package/dist/orchestration/run-workflow-tool.js +1 -0
  43. package/dist/orchestration/workflow-types.d.ts +35 -0
  44. package/dist/orchestration/workflow-types.js +2 -2
  45. package/dist/orchestration/workflow.d.ts +6 -0
  46. package/dist/orchestration/workflow.js +21 -1
  47. package/dist/prompts/default.d.ts +8 -0
  48. package/dist/prompts/default.js +3 -0
  49. package/dist/tools/fs/bash-readonly-classifier.d.ts +44 -1
  50. package/dist/tools/fs/bash-readonly-classifier.js +132 -5
  51. package/dist/tools/fs/fs-bash.js +9 -2
  52. package/dist/tools/fs/fs-write.js +19 -8
  53. package/dist/tools/fs/index.d.ts +1 -0
  54. package/dist/tools/fs/index.js +1 -0
  55. package/package.json +1 -1
  56. package/test/export-surface.snapshot.json +9 -1
@@ -181,6 +181,15 @@ export interface PrepareHandsReadFaceResult {
181
181
  shellGatedBash: boolean;
182
182
  /** write-back — the Monitor twin of the bit above. */
183
183
  shellGatedMonitor: boolean;
184
+ /** owned (design/380 rescan C4) — the ENV-HAND tool names, minted AT THE MOUNT: every tool this
185
+ * phase mounted over the ExecutionEnv (the fs/bash band), later grown by the driver's own env
186
+ * mounts (Monitor, the worktree pair — the SAME Set identity, mutated at those mount sites).
187
+ * Consumed by the O9a external-content-target fold: these are the tools whose RESULTS are the
188
+ * target's bytes, so membership must be what actually mounted on the env — never a static table
189
+ * (the wake/resume reconciliation table lists TaskOutput/TaskStop, which are orchestration, and
190
+ * misses Monitor/worktree, which are hands — both directions wrong for this consumer). Empty on
191
+ * a hands-less leg (nothing mounts on the env; the driver's env mounts are hands-gated too). */
192
+ envHandToolNames: Set<string>;
184
193
  }
185
194
  export declare function resolveHandsLessReadFace(input: PrepareHandsReadFaceInput): PrepareHandsReadFaceResult;
186
195
  /** The hands-MOUNT branch — the async half of the phase (see the module header for the contract).
@@ -71,11 +71,13 @@ export function resolveHandsLessReadFace(input) {
71
71
  additionalReadRootsCanonical: [],
72
72
  attachmentRootCanonical: undefined,
73
73
  readDenyMatcher: undefined,
74
+ envHandToolNames: new Set(),
74
75
  shellGatedBash: false,
75
76
  shellGatedMonitor: false,
76
77
  };
77
78
  }
78
79
  export async function prepareHandsMount(input) {
80
+ const envHandToolNames = new Set();
79
81
  const { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, tools, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes } = input;
80
82
  assertStringArraySeat(spec.additionalDirectories, "TaskSpec.additionalDirectories", "strings");
81
83
  assertStringArraySeat(spec.additionalReadDirectories, "TaskSpec.additionalReadDirectories", "strings");
@@ -257,7 +259,7 @@ export async function prepareHandsMount(input) {
257
259
  deps.onError?.(new Error(`spec.tools ${shadowed.length > 1 ? "entries" : "entry"} ${names} collide${shadowed.length > 1 ? "" : "s"} with the built-in hands tool band — the built-in wins (registered later; the harness tool Map is last-write-wins) and the caller tool never dispatches this task. Rename the caller tool, or don't inject executionEnv if you intend to replace the band.`), { phase: "config", sessionId });
258
260
  }
259
261
  }
260
- tools.push(...band.map((t) => firstPartyOffload(t)));
262
+ tools.push(...band.map((t) => (envHandToolNames.add(t.name), firstPartyOffload(t))));
261
263
  const shellGate = effectiveShellGate;
262
264
  if (shellGate === "off" && !(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
263
265
  deps.onError?.(new Error(`shell gate doctrine is "off" while a real writable shell (Bash) is mounted — no shell safety-axis ` +
@@ -304,6 +306,7 @@ export async function prepareHandsMount(input) {
304
306
  additionalReadRootsCanonical,
305
307
  attachmentRootCanonical,
306
308
  readDenyMatcher,
309
+ envHandToolNames,
307
310
  shellGatedBash,
308
311
  shellGatedMonitor,
309
312
  };
@@ -314,7 +314,7 @@ export async function prepareMemory(input) {
314
314
  }
315
315
  catch {
316
316
  }
317
- deliverEngineNotice(deps.onNotice, memoryHarvestQuarantinedNotice({ count, moved, escalated, ...(reason !== undefined ? { reason } : {}), sessionId, provenance: input.memoryProvenance, ...(indexOnly ? { indexRolledBack: true } : {}) }));
317
+ deliverEngineNotice(deps.onNotice, memoryHarvestQuarantinedNotice({ count, moved, escalated, ...(report.reportId !== undefined ? { reportId: report.reportId } : {}), ...(reason !== undefined ? { reason } : {}), sessionId, provenance: input.memoryProvenance, ...(indexOnly ? { indexRolledBack: true } : {}) }));
318
318
  }
319
319
  for (const notice of memoryHoldNotices(report, sessionId))
320
320
  deliverEngineNotice(deps.onNotice, notice);
@@ -1498,6 +1498,27 @@ export interface RunInternals {
1498
1498
  * after a restart those intermediate sessions are dead ends, and a recovery face enumerating
1499
1499
  * "everything under this host session" needs the root anchor, not an alias walk. */
1500
1500
  rootSessionId?: string;
1501
+ /**
1502
+ * design/380 O1② — the run tree's PLACEMENT root: the fixed point a target-bound env factory keys
1503
+ * its placement lookup on ({@link import("../remote-env.js").ExecutionEnvFactoryContext.placementRootSessionId}).
1504
+ * A SEPARATE axis from {@link rootSessionId} deliberately: that field means "member of this host
1505
+ * session's DELEGATION tree" and is consumed by the registry access/recovery faces — cascade rungs
1506
+ * and verification legs are intentionally NOT members of that tree (independent cold re-runs in
1507
+ * their own sessions), so widening `rootSessionId` to cover them would corrupt the recovery faces'
1508
+ * reading. This member says only "place me where this session was placed". Producers: the
1509
+ * orchestration entries (`runCascade` / `runWithVerification` family) after their first leg's
1510
+ * sessionId receipt, and — C12 — the delegation/workflow spawn chains, which re-thread a parent's
1511
+ * EXPLICIT value verbatim into child internals (ToolExecuteContext.placementRoot → childInternals;
1512
+ * workflow deps → shared internals base), so every descendant of a placed leg keeps the fixed
1513
+ * point; prepare's mint reads it first (`placementRoot ?? rootSessionId ?? sessionId`). Absent
1514
+ * everywhere else — the delegation lanes' `rootSessionId` fixed point then becomes the placement
1515
+ * root through the middle segment, unchanged. DURABLE since design/380 O1③ (the former C12
1516
+ * residual, fulfilled): the suspend mint stamps the resolved fixed point as
1517
+ * `CheckpointState.placementRootSessionId`, and a bare durable resume restores it into this
1518
+ * member (live internals win when re-supplied; a CONTRADICTING re-supply refuses pre-CAS,
1519
+ * `resume.placement_mismatch`). TRUSTED run-scoped channel (never a {@link TaskSpec} field).
1520
+ */
1521
+ placementRoot?: string;
1501
1522
  /**
1502
1523
  * RB-429 — the REGISTRY SCOPE this run's own background row lives in: the domain its registry-facing
1503
1524
  * tools (TaskOutput / TaskStop / SendMessage / AgentTranscript / Monitor, and the announce listing)
@@ -1739,6 +1760,16 @@ export declare function batchContextAt(messages: AgentMessage[], currentId: stri
1739
1760
  batchToolCallIds: string[];
1740
1761
  completedCallIds: string[];
1741
1762
  };
1763
+ /**
1764
+ * rescan C5 — the ONE spelling of the placement fields' empty-string discipline: `""` is absence
1765
+ * wearing clothes (the resume entry's principal-rung posture), and every placement read that must
1766
+ * treat it so — the resume rung's two sides (runtask), the restore fold's seed and the suspend
1767
+ * stamp — goes through THIS helper, so three sites cannot drift into three readings. Deliberately
1768
+ * NOT applied to the live `internals.placementRoot`/`rootSessionId` reads of the RESOLUTION fold:
1769
+ * an empty supplied claim there is an assembly error {@link mintPlacementRootSessionId} refuses
1770
+ * loudly on the factory path, and normalizing it away would silently repair what should be loud.
1771
+ */
1772
+ export declare function placementValueOrAbsent(value: string | undefined): string | undefined;
1742
1773
  /**
1743
1774
  * Build everything a task run needs (council design/34 ②: a free function with EXPLICIT deps, not a
1744
1775
  * Runner method — testable and decoupled). Resolves the model/role/thinking, acquires + reconciles the
@@ -681,6 +681,35 @@ async function derivedRouteFallsBack(args) {
681
681
  return false;
682
682
  }
683
683
  }
684
+ function mintPlacementRootSessionId(placementRootResolved) {
685
+ if (placementRootResolved === "") {
686
+ const e = new Error("placement root resolved EMPTY (internals.placementRoot / internals.rootSessionId carries an empty string) — an empty fixed point cannot key a placement lookup; fix the spawning lane instead of defaulting around it.");
687
+ e.code = "config.placement_root_invalid";
688
+ throw e;
689
+ }
690
+ return placementRootResolved;
691
+ }
692
+ function restorePlacementRoot(internals, seedPlacementRoot) {
693
+ if (internals?.placementRoot !== undefined)
694
+ return internals;
695
+ const seed = placementValueOrAbsent(seedPlacementRoot);
696
+ if (seed === undefined)
697
+ return internals;
698
+ return { ...(internals ?? {}), placementRoot: seed };
699
+ }
700
+ export function placementValueOrAbsent(value) {
701
+ return value || undefined;
702
+ }
703
+ function stampPlacementRootSessionId(placementRootResolved) {
704
+ return placementValueOrAbsent(placementRootResolved);
705
+ }
706
+ function refuseRequireExistingWithoutSession(spec) {
707
+ if (spec.requireExistingSession && !spec.sessionId) {
708
+ const e = new Error(`requireExistingSession requires a sessionId — cannot require an existing session without one (design/114 Phase3)`);
709
+ e.code = "resume.session_not_found";
710
+ throw e;
711
+ }
712
+ }
684
713
  export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
685
714
  const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
686
715
  spec = doors.spec;
@@ -689,15 +718,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
689
718
  const { toolEffects, egressTools, irreversibleTools, irreversibilityTier, axisExplicitNegatives, reversibilityProbes, ownToolNames } = prepareSafetyScan({ spec, deps });
690
719
  let shellGatedBash = false;
691
720
  let shellGatedMonitor = false;
692
- if (spec.requireExistingSession && !spec.sessionId) {
693
- const e = new Error(`requireExistingSession requires a sessionId — cannot require an existing session without one (design/114 Phase3)`);
694
- e.code = "resume.session_not_found";
695
- throw e;
696
- }
721
+ refuseRequireExistingWithoutSession(spec);
697
722
  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 } : {}; })() });
698
723
  const sessionId = acquired.sessionId;
699
724
  const hostTaskId = spec.taskId ?? sessionId;
700
725
  const delegation = effectiveDelegationFacts(internals, resume?.seed.isDelegatedChild);
726
+ const placementRootResolved = (internals = restorePlacementRoot(internals, resume?.seed.placementRootSessionId))?.placementRoot ?? internals?.rootSessionId ?? sessionId;
701
727
  let effectiveCompModel = compModel;
702
728
  if (spec.compactionModel === undefined &&
703
729
  compModel !== undefined &&
@@ -789,7 +815,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
789
815
  try {
790
816
  ownedEnv = deps.executionEnvFactory
791
817
  ? await deps.executionEnvFactory({
792
- sessionId,
818
+ sessionId, placementRootSessionId: mintPlacementRootSessionId(placementRootResolved),
793
819
  taskId: spec.taskId,
794
820
  ...(internals?.isolation ? { isolation: internals.isolation } : {}),
795
821
  ...(internals?.parentCwd ? { parentCwd: internals.parentCwd } : {}),
@@ -803,6 +829,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
803
829
  throw factoryErr;
804
830
  }
805
831
  const executionEnv = ownedEnv ?? deps.executionEnv ?? new StubExecutionEnv();
832
+ const externalContentTargetActive = executionEnv.externalContentTarget === true || resume?.seed.externalContentTarget === true;
806
833
  if (internals?.requestedCwd !== undefined) {
807
834
  const canon = async (p) => {
808
835
  const r = await executionEnv.canonicalPath(p);
@@ -1173,7 +1200,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1173
1200
  taskId: hostTaskId,
1174
1201
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
1175
1202
  ...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}),
1176
- ...(internals?.rootSessionId !== undefined ? { rootSessionId: internals.rootSessionId } : {}),
1203
+ ...(internals?.rootSessionId !== undefined ? { rootSessionId: internals.rootSessionId } : {}), ...(internals?.placementRoot !== undefined ? { placementRoot: internals.placementRoot } : {}),
1177
1204
  ...(internals?.explicitAgentName !== undefined ? { spawnedAgentName: internals.explicitAgentName } : {}),
1178
1205
  ...(internals?.peerSelfRef !== undefined ? { peerSelfRef: internals.peerSelfRef } : {}),
1179
1206
  ...(internals?.peerInboundChainRef !== undefined ? { peerInboundChainRef: internals.peerInboundChainRef } : {}),
@@ -1348,7 +1375,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1348
1375
  scope: taskScope,
1349
1376
  notifier: deps.workflowCompletionNotifier,
1350
1377
  originatingSessionId: sessionId,
1351
- rootSessionId: internals?.rootSessionId ?? sessionId,
1378
+ rootSessionId: internals?.rootSessionId ?? sessionId, ...(internals?.placementRoot !== undefined ? { placementRoot: internals.placementRoot } : {}),
1352
1379
  taskRegistry: defaultTaskRegistry,
1353
1380
  taskNotification: internals?.onTaskNotification,
1354
1381
  taskOwner: hostTaskId,
@@ -1596,7 +1623,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1596
1623
  const memoryWriteGateRef = {};
1597
1624
  const handsReadFaceInput = { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, tools, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes, reminderMark, reminderDisclosureCounts };
1598
1625
  const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
1599
- const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher } = handsReadFace;
1626
+ const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher, envHandToolNames } = handsReadFace;
1600
1627
  resolvedReadFace = handsReadFace.resolvedReadFace;
1601
1628
  readDenyAdditionsNormalized = handsReadFace.readDenyAdditionsNormalized;
1602
1629
  handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
@@ -1680,6 +1707,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1680
1707
  }
1681
1708
  if (backgroundTaskToolsActive) {
1682
1709
  toolEffects.set("Monitor", "write");
1710
+ envHandToolNames.add("Monitor");
1683
1711
  tools.push(firstPartyOffload(createMonitorTool(executionEnv, {
1684
1712
  registry: defaultTaskRegistry,
1685
1713
  owner: hostTaskId,
@@ -1715,7 +1743,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1715
1743
  };
1716
1744
  if (workspaceStateSettle !== undefined)
1717
1745
  workspaceStateSettle.restoredWorktreeDir = worktreeSessionRef.current?.worktreeDir;
1718
- tools.push(...createWorktreeTools(executionEnv, { repoRoot: taskRootFinal, cwdRef: handsCwdRef, session: worktreeSessionRef }).map((t) => firstPartyOffload(t)));
1746
+ tools.push(...createWorktreeTools(executionEnv, { repoRoot: taskRootFinal, cwdRef: handsCwdRef, session: worktreeSessionRef }).map((t) => (envHandToolNames.add(t.name), firstPartyOffload(t))));
1719
1747
  }
1720
1748
  if (offloadStore)
1721
1749
  tools.push(createReadToolResultTool(offloadStore));
@@ -1800,7 +1828,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1800
1828
  ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
1801
1829
  ...(sessionId !== undefined ? { sessionId } : {}),
1802
1830
  requestStopAfterTurn,
1803
- }));
1831
+ }).map((t) => (envHandToolNames.add(t.name), t)));
1804
1832
  }
1805
1833
  if (internals?.insideFork !== true &&
1806
1834
  runnerSelf &&
@@ -1814,6 +1842,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1814
1842
  if (lspManager) {
1815
1843
  const lspRoot = taskRootFinal;
1816
1844
  tools.push(createLspTool(lspManager, { isPathIgnored: gitCheckIgnoreFilter(executionEnv, lspRoot), env: executionEnv }));
1845
+ envHandToolNames.add("LSP");
1817
1846
  }
1818
1847
  const lspDiagnostics = spec.lspDiagnostics !== false && lspManager?.diagnostics !== undefined && handsEnabled && spec.handsReadOnly !== true
1819
1848
  ? lspManager.diagnostics
@@ -2089,6 +2118,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2089
2118
  envFacts.sandboxEgress = spec.envFacts.egress;
2090
2119
  if (spec.envFacts?.scratchpadDir)
2091
2120
  envFacts.scratchpadDir = spec.envFacts.scratchpadDir;
2121
+ if (externalContentTargetActive)
2122
+ envFacts.externalContentTarget = true;
2092
2123
  if (resume !== undefined && spec.envFacts?.resumeFacts) {
2093
2124
  const rf = spec.envFacts.resumeFacts;
2094
2125
  const copy = {};
@@ -2269,7 +2300,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2269
2300
  let staticMarkWaiverAnnounced = false;
2270
2301
  const effectiveSafety = narrowContentSafety(delegationProvenanceChannel?.contentSafety, memoryEngineSession?.contentSafety);
2271
2302
  const trustedTools = effectiveSafety.trustedTools;
2272
- const execIsExternalContent = effectiveSafety.execIsExternalContent;
2303
+ const execIsExternalContent = effectiveSafety.execIsExternalContent || externalContentTargetActive;
2273
2304
  provenanceForChildrenRef.current = { trustedTools: [...trustedTools], execIsExternalContent };
2274
2305
  announceDeclaredMcpContentClasses({
2275
2306
  entries: lockedPreflight.mcp,
@@ -2291,12 +2322,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2291
2322
  };
2292
2323
  const callerToolNames = new Set((spec.tools ?? []).map((t) => t.name));
2293
2324
  const contentOriginWrapped = new WeakSet();
2294
- const classOf = (t) => classifyToolContentOrigin({
2295
- declared: t.contentOrigin,
2296
- isProtocolTool: protocolOf(t.name) !== undefined,
2297
- isCallerTool: callerToolNames.has(t.name),
2298
- trusted: trustedTools.has(t.name),
2299
- });
2325
+ const classOf = (t) => externalContentTargetActive && envHandToolNames.has(t.name)
2326
+ ? "external"
2327
+ : classifyToolContentOrigin({
2328
+ declared: t.contentOrigin,
2329
+ isProtocolTool: protocolOf(t.name) !== undefined,
2330
+ isCallerTool: callerToolNames.has(t.name),
2331
+ trusted: trustedTools.has(t.name),
2332
+ });
2300
2333
  const delegationByName = new Map();
2301
2334
  for (const t of spec.tools ?? []) {
2302
2335
  const withFaces = t;
@@ -3888,6 +3921,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3888
3921
  gitAnnouncement: gitStatusRef.announced !== undefined ? { ...gitStatusRef.announced } : undefined,
3889
3922
  delegationProvenance: internals?.delegationProvenance !== undefined ? { ...internals.delegationProvenance.ref.current } : undefined,
3890
3923
  isDelegatedChild: hookIdentity.isDelegatedChild ? true : undefined,
3924
+ placementRootSessionId: stampPlacementRootSessionId(placementRootResolved),
3925
+ externalContentTarget: externalContentTargetActive ? true : undefined,
3891
3926
  });
3892
3927
  const commitSuspendSaga = async (token, cp, remoteEnv, remoteHandle) => {
3893
3928
  if (!checkpointStore)
@@ -49,6 +49,16 @@ export async function prepareWorkspaceRestore(input) {
49
49
  throw e;
50
50
  };
51
51
  const handle = resume.workspaceHandle;
52
+ const refuseIdentityMismatch = (resumedHandle) => {
53
+ if (resumedHandle.provider !== handle.provider) {
54
+ failResume(`resume workspace identity mismatch: the checkpoint records provider "${handle.provider}" but the resumed env reports "${resumedHandle.provider}" — refusing to continue a workspace on a different backend (wire the factory to rebuild the checkpointed lane and re-resume)`);
55
+ }
56
+ if (handle.deviceId !== undefined && resumedHandle.deviceId !== handle.deviceId) {
57
+ failResume(resumedHandle.deviceId === undefined
58
+ ? `resume workspace identity mismatch: the checkpoint records deviceId "${handle.deviceId}" but the resumed env reports none — a device-bound workspace cannot be verified on a handle that does not state its device (absence is not a match)`
59
+ : `resume workspace identity mismatch: the checkpoint records deviceId "${handle.deviceId}" but the resumed env reports "${resumedHandle.deviceId}" — refusing to continue on a different physical device`);
60
+ }
61
+ };
52
62
  if (ownedEnv === undefined || !isRemoteExecutionEnv(ownedEnv)) {
53
63
  failResume("resume needs a RemoteExecutionEnv from executionEnvFactory to restore the workspace snapshot");
54
64
  }
@@ -56,6 +66,7 @@ export async function prepareWorkspaceRestore(input) {
56
66
  if (handle.restoreMode !== "park_only" && ownedEnv.capabilities.suspendable) {
57
67
  failResume("checkpoint workspaceHandle has no snapshotId and is not a park_only handle, but the resumed env is suspendable — refusing to resume on a possibly-unrestored workspace (corrupt checkpoint?)");
58
68
  }
69
+ refuseIdentityMismatch(ownedEnv.workspaceHandle());
59
70
  if (handle.mountPath && handle.mountPath !== taskRootPath) {
60
71
  taskRootPath = handle.mountPath;
61
72
  }
@@ -68,6 +79,7 @@ export async function prepareWorkspaceRestore(input) {
68
79
  if (missingHere.length > 0) {
69
80
  failResume(`the resumed execution env cannot restore a workspace snapshot: its adapter does not implement ${missingHere.join(" or ")}. The checkpoint holds snapshot "${handle.snapshotId}" — wire an adapter that implements the full RemoteExecutionEnv restore surface and re-resume.`);
70
81
  }
82
+ refuseIdentityMismatch(ownedEnv.workspaceHandle());
71
83
  const { outcome: restored, attempts: restoreAttempts } = await restoreWorkspaceWithRetry(ownedEnv, handle.snapshotId, {
72
84
  abortSignal: restoreSignal,
73
85
  priorHandle: handle,
@@ -76,6 +88,7 @@ export async function prepareWorkspaceRestore(input) {
76
88
  failResume(`resumeVM failed after ${restoreAttempts} attempt(s) (${restored.error.code}): ${restored.error.message}`, restored.error, remoteEnvFailureNote("resumeVM", restored.error, restoreAttempts));
77
89
  }
78
90
  else {
91
+ refuseIdentityMismatch(restored.value);
79
92
  const restoredEnv = ownedEnv;
80
93
  const canonicalInEnv = async (p) => {
81
94
  try {
@@ -179,6 +179,24 @@ export declare class Runner {
179
179
  * observed `already_resolved` and by the FIFO cap — an evicted entry only ever degrades to the explicit
180
180
  * re-supply requirement (fail-loud), never fail-open. */
181
181
  private readonly parentConstraintRegistry;
182
+ /** rescan C2 — same-process suspended-env reap handles by token: the design/51 fence contract is
183
+ * "only the expire WINNER destroys the paused env", and the terminal resume-preflight arm
184
+ * (resumeStream) can now BE that winner — without this registry the losing stream-side
185
+ * reapSuspended returned early and the paused env this process holds leaked forever. Registered
186
+ * when a suspend publishes its reap handle; retired when the stream's own reap runs, when a
187
+ * resume wins the resolve CAS (the restore path owns the env from there — a later reopen keeps
188
+ * the ORIGINAL stream's closure as the reaping owner, deliberately), and when the terminal arm
189
+ * wins. Bounded by live suspended streams (the closures hold the same handles). */
190
+ private readonly suspendedEnvReaps;
191
+ /** rescan C2 review r3 — SAME-RUNNER claim marks for {@link suspendedEnvReaps}: a token appears
192
+ * here from the moment an in-process actor ATTEMPTS its settling CAS (the terminal preflight's
193
+ * expire; a resume's resolve) until that actor's duty decision is done. The losing stream-side
194
+ * reap preserves a record ONLY while a local claim is in flight — a CROSS-Runner settlement
195
+ * leaves no local claim, so the loser retires the record (drops the strong ref; physical cleanup
196
+ * belongs to the winning owner, unprovable from here) instead of retaining env handles without
197
+ * bound in a long-lived Runner. A stale claim's failure direction is retention (the status quo),
198
+ * never a wrong destroy. */
199
+ private readonly locallyClaimedTokens;
182
200
  private static readonly PARENT_CONSTRAINT_REGISTRY_CAP;
183
201
  constructor(deps: RunnerDeps);
184
202
  /** design/73 §1 v1 — fire `RunnerDeps.onTaskOutcome` through the single swallow-guarded chokepoint