@sema-agent/core 7.0.2 → 7.2.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 (66) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/agents/cross-session-envelope.d.ts +138 -0
  3. package/dist/agents/cross-session-envelope.js +191 -0
  4. package/dist/agents/cross-session-judge.d.ts +119 -0
  5. package/dist/agents/cross-session-judge.js +184 -0
  6. package/dist/agents/cross-session-ref.d.ts +52 -0
  7. package/dist/agents/cross-session-ref.js +64 -0
  8. package/dist/agents/repair-loop.d.ts +8 -7
  9. package/dist/agents/roster-store.d.ts +7 -2
  10. package/dist/agents/send-message-tool.d.ts +13 -0
  11. package/dist/agents/send-message-tool.js +36 -12
  12. package/dist/brain/errors.d.ts +18 -0
  13. package/dist/brain/errors.js +3 -0
  14. package/dist/brain/stream-engine.js +6 -4
  15. package/dist/core/checkpoint-store.d.ts +189 -3
  16. package/dist/core/checkpoint-store.js +56 -16
  17. package/dist/core/context-edit.d.ts +3 -0
  18. package/dist/core/governance-codes.d.ts +1 -1
  19. package/dist/core/governance-codes.js +4 -0
  20. package/dist/core/hooks.d.ts +34 -7
  21. package/dist/core/hooks.js +14 -8
  22. package/dist/core/image-downsample.d.ts +4 -3
  23. package/dist/core/permission-rule-consent.d.ts +72 -23
  24. package/dist/core/permission-rule-consent.js +115 -26
  25. package/dist/core/permission-rule-model.d.ts +245 -51
  26. package/dist/core/permission-rule-model.js +312 -54
  27. package/dist/core/permission-rule-org.js +13 -6
  28. package/dist/core/remote-env.d.ts +8 -1
  29. package/dist/core/roles.d.ts +30 -8
  30. package/dist/core/roles.js +12 -8
  31. package/dist/core/runner/assemble-result.js +2 -1
  32. package/dist/core/runner/prepare-task.d.ts +41 -2
  33. package/dist/core/runner/prepare-task.js +353 -152
  34. package/dist/core/runner/prepare-workspace-restore.d.ts +6 -1
  35. package/dist/core/runner/prepare-workspace-restore.js +2 -1
  36. package/dist/core/runner/runtask.d.ts +12 -3
  37. package/dist/core/runner/runtask.js +45 -7
  38. package/dist/core/safety-axis-vocab.d.ts +1 -1
  39. package/dist/core/strategy-store.d.ts +4 -1
  40. package/dist/core/task-notification.d.ts +64 -5
  41. package/dist/core/task-notification.js +25 -4
  42. package/dist/core/task-registry-shared.d.ts +7 -3
  43. package/dist/core/tool-errors.d.ts +1 -1
  44. package/dist/core/tool-policy.d.ts +51 -7
  45. package/dist/core/tool-policy.js +63 -9
  46. package/dist/core/types.d.ts +110 -9
  47. package/dist/core/untrusted-text.js +17 -1
  48. package/dist/engine/compaction/compaction.js +6 -2
  49. package/dist/engine/harness/agent-harness.d.ts +28 -6
  50. package/dist/engine/harness/agent-harness.js +34 -2
  51. package/dist/engine/harness/messages.js +4 -0
  52. package/dist/engine/harness/types.d.ts +37 -0
  53. package/dist/engine/harness/types.js +5 -0
  54. package/dist/engine/session/session.js +3 -2
  55. package/dist/index.d.ts +6 -3
  56. package/dist/index.js +5 -2
  57. package/dist/internal/harness.d.ts +1 -0
  58. package/dist/internal/harness.js +1 -0
  59. package/dist/orchestration/builtin-workflows.d.ts +17 -9
  60. package/dist/orchestration/run-workflow-tool.js +7 -2
  61. package/dist/orchestration/workflow-governance.js +1 -1
  62. package/dist/orchestration/workflow-types.d.ts +1 -0
  63. package/dist/orchestration/workflow.js +1 -1
  64. package/dist/stores/file/mailbox-store.d.ts +2 -1
  65. package/package.json +1 -1
  66. package/test/export-surface.snapshot.json +125 -1
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { realpathSync } from "node:fs";
3
3
  import { resolve as resolveFsPath } from "node:path";
4
- import { AgentHarness, DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, estimateContextTokens, readCompactionActiveTools, summaryOutputBudgetTokens } from "../../internal/harness.js";
4
+ import { AgentHarness, DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, estimateContextTokens, isSyntheticApiErrorMessage, readCompactionActiveTools, summaryOutputBudgetTokens } from "../../internal/harness.js";
5
5
  const PROMPT_HASH_SALT = randomBytes(16);
6
6
  import { sanitizeCompactionSettings } from "../auto-compaction.js";
7
7
  import { projectStaleToolResults, resolveStaleToolResultOffload } from "./compaction-call-options.js";
@@ -23,7 +23,8 @@ import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send
23
23
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
24
24
  import { askApproverIdentity, carriesBidiControls, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOfLayer, isApprovalSettledBy, isAskDenyResolution, screenApproverAttribution, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
25
25
  const PERSISTED_RULE_TOOL = "Bash";
26
- import { findAdmittingRule, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
26
+ const DIRECTORY_RULE_TOOL = "Read";
27
+ import { directoryRuleAdmits, eligiblePersisted, findAdmittingRule, lexicalNormalAbsolutePathOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
27
28
  import { normalizePersistedRule } from "../permission-rule-store.js";
28
29
  import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
29
30
  import { CHANGED_FILES_MTIME_EPS_MS, fenceMcpServerInstructions, renderAgentListingDelta } from "./turn-attachments.js";
@@ -76,7 +77,7 @@ import { capAggregateToolResults } from "../tool-result-budget.js";
76
77
  import { capAggregateMediaBytes, AGGREGATE_MEDIA_BUDGET_BYTES } from "../media-byte-cap.js";
77
78
  import { dropOrphanToolResults, guardBudget, insertTrimNotice, trimToBudget } from "../context-guard.js";
78
79
  import { StubExecutionEnv } from "../stub-env.js";
79
- import { hasDestroy, isIsolated, isRemoteExecutionEnv, isSuspendable, missingRestoreSurface } from "../remote-env.js";
80
+ import { hasDestroy, isIsolated, isRemoteExecutionEnv, isSuspendable, missingRestoreSurface, RemoteExecutionError } from "../remote-env.js";
80
81
  import { settleTeardownLeg } from "./teardown-bounded.js";
81
82
  import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
82
83
  import { createTaskOutputTool, createTaskStopTool, defaultTaskRegistry } from "../task-registry.js";
@@ -662,6 +663,144 @@ export function batchContextAt(messages, currentId) {
662
663
  const completedCallIds = batch.filter((id) => id !== currentId && resolved.has(id));
663
664
  return { batchToolCallIds: batch, completedCallIds };
664
665
  }
666
+ function composeCallSignal(runSignal, callSignal) {
667
+ return callSignal !== undefined ? AbortSignal.any([runSignal, callSignal]) : runSignal;
668
+ }
669
+ function lateAskSettlementObserver(args) {
670
+ return (late) => {
671
+ if (late.kind === "approve") {
672
+ deliverEngineNotice(args.onNotice, {
673
+ code: "task.late_approval",
674
+ message: `an approval for "${args.toolName}" was not consumed: a run or turn interrupt released the ask wait, ` +
675
+ `so the tool did NOT run and the approval was not honored (an unconsumed answer is the approver ` +
676
+ `releasing its wait, never a verdict).`,
677
+ detail: {
678
+ toolName: args.toolName,
679
+ toolCallId: args.toolCallId,
680
+ sessionId: args.sessionId,
681
+ runId: args.runId,
682
+ ...(args.taskId !== undefined ? { taskId: args.taskId } : {}),
683
+ },
684
+ });
685
+ return;
686
+ }
687
+ args.onError?.(late.error, { phase: "hook", sessionId: args.sessionId });
688
+ };
689
+ }
690
+ function askGrantShapeOf(args) {
691
+ try {
692
+ return JSON.stringify(args ?? null);
693
+ }
694
+ catch {
695
+ return undefined;
696
+ }
697
+ }
698
+ function consumeInheritedAskGrant(grants, onAsk, humanReviewRef, decision, req) {
699
+ if (decision.decisionReason !== undefined && decision.decisionReason !== "rule")
700
+ return undefined;
701
+ const grant = grants.get(req.toolCallId);
702
+ if (grant === undefined || askApproverIdentity(grant.approver) !== askApproverIdentity(onAsk) || grant.argsJson !== askGrantShapeOf(req.args))
703
+ return undefined;
704
+ grants.delete(req.toolCallId);
705
+ humanReviewRef.count += 1;
706
+ humanReviewRef.totalWaitMs += grant.waitMs;
707
+ const toolArg = primaryActivityArg(req.args);
708
+ humanReviewRef.gates.push({
709
+ kind: "human",
710
+ waitMs: grant.waitMs,
711
+ decision: "allow",
712
+ toolName: req.toolName,
713
+ ...(toolArg !== undefined ? { toolArg } : {}),
714
+ });
715
+ return { action: "allow", presentedInput: grant.presented };
716
+ }
717
+ function raceSettlementAgainstSignal(p, signal) {
718
+ const settled = p.then((value) => ({ tag: "value", value }), (error) => ({ tag: "threw", error }));
719
+ if (signal.aborted)
720
+ return Promise.resolve({ tag: "aborted" });
721
+ return new Promise((resolve) => {
722
+ let done = false;
723
+ const finish = (r) => {
724
+ if (done)
725
+ return;
726
+ done = true;
727
+ signal.removeEventListener("abort", onAbortEvent);
728
+ resolve(r);
729
+ };
730
+ const onAbortEvent = () => finish({ tag: "aborted" });
731
+ signal.addEventListener("abort", onAbortEvent, { once: true });
732
+ void settled.then(finish);
733
+ });
734
+ }
735
+ const PARK_COMPENSATION_TIMEOUT_MS = 30_000;
736
+ export async function compensateUnparkedPause(remoteEnv, snapshotId, io) {
737
+ const boundCtl = new AbortController();
738
+ const boundTimer = setTimeout(() => boundCtl.abort(), io.boundMs);
739
+ const bound = boundCtl.signal;
740
+ let phase = "resumeVM";
741
+ let restoreAttempts = 1;
742
+ let vmRunning = false;
743
+ const work = (async () => {
744
+ const { outcome: back, attempts: backAttempts } = await restoreWorkspaceWithRetry(remoteEnv, snapshotId, { abortSignal: bound }, (attempt) => (restoreAttempts = attempt));
745
+ if (!back.ok) {
746
+ io.noteFailure(remoteEnvFailureNote("resumeVM", back.error, backAttempts));
747
+ io.disclose(back.error);
748
+ return { ok: false, reason: `resumeVM failed (${back.error.code}) while compensating an unparked pause` };
749
+ }
750
+ vmRunning = true;
751
+ if (bound.aborted) {
752
+ return { ok: false, reason: "resumeVM settled only after the decision bound" };
753
+ }
754
+ phase = "postResumeInit";
755
+ const init = await remoteEnv.postResumeInit({ abortSignal: bound });
756
+ if (!init.ok) {
757
+ io.noteFailure(remoteEnvFailureNote("postResumeInit", init.error, 1));
758
+ io.disclose(init.error);
759
+ return { ok: false, reason: `postResumeInit failed (${init.error.code}) while compensating an unparked pause` };
760
+ }
761
+ return { ok: true };
762
+ })();
763
+ const raced = await raceSettlementAgainstSignal(work, bound);
764
+ clearTimeout(boundTimer);
765
+ if (raced.tag === "value")
766
+ return raced.value;
767
+ if (raced.tag === "threw") {
768
+ io.noteFailure(remoteEnvFailureNote(phase, raced.error instanceof RemoteExecutionError
769
+ ? raced.error
770
+ : new RemoteExecutionError("unknown", `the pause compensation's ${phase} call threw: ${raced.error instanceof Error ? raced.error.message : String(raced.error)}`), phase === "resumeVM" ? restoreAttempts : 1));
771
+ io.disclose(raced.error);
772
+ return {
773
+ ok: false,
774
+ reason: "the pause compensation threw (adapter contract violation; the deployment's error face carries the exception)",
775
+ };
776
+ }
777
+ const timedOut = new RemoteExecutionError("timeout", `the pause compensation's ${phase} call did not settle within the ${io.boundMs}ms decision bound`);
778
+ io.noteFailure(remoteEnvFailureNote(phase, timedOut, phase === "resumeVM" ? restoreAttempts : 1));
779
+ io.disclose(timedOut);
780
+ const destroyOwnerlessLate = () => {
781
+ io.disclose(new Error(`park compensation settled after its ${io.boundMs}ms decision bound: the VM is running with no run ` +
782
+ `left to own it — destroying the env (an ownerless running VM must not linger; the run already took ` +
783
+ `the fail-closed arm when the bound fired)`));
784
+ void Promise.resolve()
785
+ .then(() => remoteEnv.destroy())
786
+ .catch((destroyErr) => io.disclose(destroyErr));
787
+ };
788
+ void work.then((late) => {
789
+ if (vmRunning) {
790
+ destroyOwnerlessLate();
791
+ return;
792
+ }
793
+ void late;
794
+ }, (lateErr) => {
795
+ io.disclose(lateErr);
796
+ if (vmRunning)
797
+ destroyOwnerlessLate();
798
+ });
799
+ return {
800
+ ok: false,
801
+ reason: `the pause compensation did not settle within ${io.boundMs}ms — treated as failed (a late-restored VM is destroyed; a late restore failure is disclosed)`,
802
+ };
803
+ }
665
804
  function raceAbort(p, signal, onAbort) {
666
805
  if (signal.aborted)
667
806
  return Promise.resolve(onAbort());
@@ -818,6 +957,87 @@ function persistedRuleHitOf(admitting) {
818
957
  ? undefined
819
958
  : { rules: admitting.map((r) => ({ rule: r.rule, dots: r.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) })) };
820
959
  }
960
+ function directoryRuleLaneAnswer(table, args, ctx) {
961
+ const filePath = args?.file_path;
962
+ if (typeof filePath !== "string" || filePath === "")
963
+ return undefined;
964
+ const spelled = filePath.startsWith("/") ? filePath : `${(ctx.liveCwd ?? ctx.root ?? "").replace(/\/+$/, "")}/${filePath}`;
965
+ const target = lexicalNormalAbsolutePathOf(spelled);
966
+ if (target === undefined)
967
+ return undefined;
968
+ const hit = table.find((r) => eligiblePersisted(r, { tool: DIRECTORY_RULE_TOOL, cwd: ctx.root, sessionId: ctx.sessionId }) && directoryRuleAdmits(r, target));
969
+ return hit !== undefined ? persistedRuleHitOf([hit]) : undefined;
970
+ }
971
+ function makeRuleOffersOf(cfg) {
972
+ return (toolName, args, ask) => {
973
+ if (!cfg.laneArmed || toolName !== PERSISTED_RULE_TOOL)
974
+ return {};
975
+ if ((cfg.principal === undefined || cfg.principal === "") && !cfg.localOwnerDeclared)
976
+ return {};
977
+ const closedDoor = (() => {
978
+ if (ask?.requiresRealApproval === true)
979
+ return "mandated";
980
+ if (ask?.persistedRuleShadowed !== undefined)
981
+ return "shadowed";
982
+ if (ask?.decisionReason === "hook")
983
+ return "mandated";
984
+ if (ask?.matchedAskRule !== undefined)
985
+ return "shadowed";
986
+ if (ask?.inheritedUnresolved === true)
987
+ return "mandated";
988
+ if (ask?.ancestorResolved === true)
989
+ return "mandated";
990
+ return undefined;
991
+ })();
992
+ if (closedDoor !== undefined)
993
+ return { ruleOffersAbsence: closedDoor };
994
+ if (persistedRuleMandateOf({
995
+ egress: cfg.egressTools.has(toolName),
996
+ irreversibility: cfg.irreversibilityTier.get(toolName),
997
+ shellGated: cfg.shellGatedBash,
998
+ probeMandated: ask?.probeMandated === true,
999
+ }) !== undefined) {
1000
+ return { ruleOffersAbsence: "mandated" };
1001
+ }
1002
+ const command = args?.command;
1003
+ if (typeof command !== "string")
1004
+ return { ruleOffersAbsence: "lane_cannot_speak" };
1005
+ const offers = suggestRulesForCommand(command, {
1006
+ ...(ask?.segmentCoverage !== undefined ? { coverage: ask.segmentCoverage } : {}),
1007
+ ...(cfg.taskRoot !== undefined ? { scope: { kind: "project", root: cfg.taskRoot }, cwd: cfg.taskRoot } : {}),
1008
+ ...(cfg.cwdRef?.current !== undefined ? { execCwd: cfg.cwdRef.current } : {}),
1009
+ ...(cfg.deniesDirectoryRead !== undefined ? { deniesDirectoryRead: cfg.deniesDirectoryRead } : {}),
1010
+ });
1011
+ return offers.length > 0
1012
+ ? { ruleOffers: offers, ...(cfg.cwdRef?.current !== undefined ? { execCwd: cfg.cwdRef.current } : {}) }
1013
+ : { ruleOffersAbsence: "lane_cannot_speak" };
1014
+ };
1015
+ }
1016
+ function makeOrgAdjudicationLane(overlay, questionToolMounted, onRevisionDefect) {
1017
+ if (overlay === undefined)
1018
+ return undefined;
1019
+ return {
1020
+ adjudicate: async (req) => {
1021
+ if (req.toolName === ASK_USER_QUESTION_TOOL_NAME && questionToolMounted)
1022
+ return { status: "available" };
1023
+ let resolution;
1024
+ try {
1025
+ resolution = await overlay.resolve();
1026
+ }
1027
+ catch (err) {
1028
+ return { status: "unavailable", disclosures: [`the org rule overlay threw: ${err instanceof Error ? err.message : String(err)}`] };
1029
+ }
1030
+ if (resolution.status === "unavailable")
1031
+ return { status: "unavailable", disclosures: resolution.disclosures };
1032
+ const revisionCell = orgRevisionEvidenceOf(resolution, onRevisionDefect);
1033
+ const command = req.args?.command;
1034
+ if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
1035
+ return { status: "available", ...revisionCell };
1036
+ const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
1037
+ return verdict === undefined ? { status: "available", ...revisionCell } : { status: "available", verdict, ...revisionCell };
1038
+ },
1039
+ };
1040
+ }
821
1041
  function cwdConflictsRestoreError(requestedCwd) {
822
1042
  const e = new Error(`RunInternals.requestedCwd ("${requestedCwd}") cannot be combined with a checkpoint workspace restore — ` +
823
1043
  `the restored workspace's own mount path is authoritative for the task root, so a requested cwd on this leg ` +
@@ -1666,7 +1886,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1666
1886
  ...(am.onBreakerOpen !== undefined ? { onBreakerOpen: am.onBreakerOpen } : {}),
1667
1887
  classify: async (input, signal) => {
1668
1888
  const ctx = await session.buildContext();
1669
- const known = ctx.messages.filter((m) => m.role === "user" || m.role === "assistant" || m.role === "toolResult");
1889
+ const known = ctx.messages.filter((m) => !isSyntheticApiErrorMessage(m) &&
1890
+ (m.role === "user" || m.role === "assistant" || m.role === "toolResult"));
1670
1891
  const userPrompt = renderAutoModeWindow(known, am.window) + renderAutoModeAction(input);
1671
1892
  const classifierAuth = await spec.getApiKeyAndHeaders?.(classifierModel);
1672
1893
  const response = await classifierRuntime.completeSimple(classifierModel, { systemPrompt: classifierSystemPrompt, messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }] }, {
@@ -1985,10 +2206,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1985
2206
  handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
1986
2207
  shellGatedBash = handsReadFace.shellGatedBash;
1987
2208
  shellGatedMonitor = handsReadFace.shellGatedMonitor;
1988
- if (backgroundTaskToolsActive || workflowToolsActive) {
1989
- toolEffects.set("TaskOutput", "read");
1990
- toolEffects.set("TaskStop", "write");
1991
- tools.push(firstPartyOffload(createTaskOutputTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore, notificationWired: internals?.onTaskNotification !== undefined, oneShot: spec.oneShot, toolResultStore: offloadStore })), firstPartyOffload(createTaskStopTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore })));
2209
+ const delegationSurfaceActive = backgroundTaskToolsActive || workflowToolsActive;
2210
+ if (delegationSurfaceActive || internals?.parentNotify !== undefined) {
2211
+ if (delegationSurfaceActive) {
2212
+ toolEffects.set("TaskOutput", "read");
2213
+ toolEffects.set("TaskStop", "write");
2214
+ tools.push(firstPartyOffload(createTaskOutputTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore, notificationWired: internals?.onTaskNotification !== undefined, oneShot: spec.oneShot, toolResultStore: offloadStore })), firstPartyOffload(createTaskStopTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore })));
2215
+ }
1992
2216
  if (runnerSelf && !(spec.tools ?? []).some((t) => t.name === SEND_MESSAGE_TOOL_NAME)) {
1993
2217
  toolEffects.set(SEND_MESSAGE_TOOL_NAME, "write");
1994
2218
  axisExplicitNegatives.set(SEND_MESSAGE_TOOL_NAME, { ...axisExplicitNegatives.get(SEND_MESSAGE_TOOL_NAME), egress: false });
@@ -2018,7 +2242,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2018
2242
  owner: hostTaskId,
2019
2243
  scope: taskScope,
2020
2244
  ...(sessionId !== undefined ? { sessionId } : {}),
2021
- ...(internals?.onTaskNotification !== undefined ? { notify: internals.onTaskNotification } : {}), ...(spec.oneShot !== undefined ? { oneShot: spec.oneShot } : {}),
2245
+ ...(internals?.onTaskNotification !== undefined ? { notify: internals.onTaskNotification } : {}), ...(spec.oneShot !== undefined ? { oneShot: spec.oneShot } : {}), retrievalToolMounted: delegationSurfaceActive,
2022
2246
  ...(internals?.onSubagentSpawn !== undefined ? { sink: internals.onSubagentSpawn } : {}),
2023
2247
  ...(internals?.parentNotify !== undefined
2024
2248
  ? { uplink: internals.parentNotify, ...(internals.parentPeerRef !== undefined ? { uplinkRecipient: internals.parentPeerRef } : {}) }
@@ -2043,7 +2267,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2043
2267
  detail: { handle, ...(scope !== undefined ? { scope } : {}) },
2044
2268
  }),
2045
2269
  })));
2046
- if (!(spec.tools ?? []).some((t) => t.name === AGENT_TRANSCRIPT_TOOL_NAME)) {
2270
+ if (delegationSurfaceActive && !(spec.tools ?? []).some((t) => t.name === AGENT_TRANSCRIPT_TOOL_NAME)) {
2047
2271
  tools.push(firstPartyOffload(createAgentTranscriptTool({
2048
2272
  runner: runnerSelf,
2049
2273
  registry: defaultTaskRegistry,
@@ -3487,14 +3711,6 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3487
3711
  return true;
3488
3712
  };
3489
3713
  const inheritedAskGrants = new Map();
3490
- const askGrantShapeOf = (args) => {
3491
- try {
3492
- return JSON.stringify(args ?? null);
3493
- }
3494
- catch {
3495
- return undefined;
3496
- }
3497
- };
3498
3714
  const recordInheritedAskGrant = (toolCallId, approver, presented, waitMs) => {
3499
3715
  if (typeof approver !== "function")
3500
3716
  return;
@@ -3615,10 +3831,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3615
3831
  const anonymous = spec.principal === undefined || spec.principal === "";
3616
3832
  if (anonymous && !localOwnerDeclared)
3617
3833
  return undefined;
3618
- if (req.toolName !== PERSISTED_RULE_TOOL)
3619
- return undefined;
3620
- const command = req.args?.command;
3621
- if (typeof command !== "string")
3834
+ if (req.toolName !== PERSISTED_RULE_TOOL && req.toolName !== DIRECTORY_RULE_TOOL)
3622
3835
  return undefined;
3623
3836
  let listed;
3624
3837
  try {
@@ -3638,76 +3851,32 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3638
3851
  return { unreadable: true };
3639
3852
  }
3640
3853
  const table = await spliceSessionOverlayRows(deps.sessionPermissionRules, sessionId, listed.rules, deps.tracer, hostTaskId);
3641
- const admitting = findAdmittingRule(table, { tool: req.toolName, command, cwd: root, sessionId });
3854
+ if (req.toolName === DIRECTORY_RULE_TOOL) {
3855
+ return directoryRuleLaneAnswer(table, req.args, { root, sessionId, liveCwd: handsCwdRef?.current });
3856
+ }
3857
+ const command = req.args?.command;
3858
+ if (typeof command !== "string")
3859
+ return undefined;
3860
+ const admitting = findAdmittingRule(table, { tool: req.toolName, command, cwd: root, sessionId, ...(handsCwdRef?.current !== undefined ? { execCwd: handsCwdRef.current } : {}) });
3642
3861
  if (admitting !== undefined)
3643
3862
  return persistedRuleHitOf(admitting);
3644
- const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: root, sessionId });
3863
+ const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: root, sessionId, ...(handsCwdRef?.current !== undefined ? { execCwd: handsCwdRef.current } : {}) });
3645
3864
  return coverage !== undefined ? { segmentCoverage: coverage } : undefined;
3646
3865
  },
3647
3866
  };
3648
3867
  })();
3649
- const permissionRuleOrgLane = (() => {
3650
- const overlay = deps.permissionRuleOrg;
3651
- if (overlay === undefined)
3652
- return undefined;
3653
- return {
3654
- adjudicate: async (req) => {
3655
- if (req.toolName === ASK_USER_QUESTION_TOOL_NAME && questionToolMounted)
3656
- return { status: "available" };
3657
- let resolution;
3658
- try {
3659
- resolution = await overlay.resolve();
3660
- }
3661
- catch (err) {
3662
- return { status: "unavailable", disclosures: [`the org rule overlay threw: ${err instanceof Error ? err.message : String(err)}`] };
3663
- }
3664
- if (resolution.status === "unavailable")
3665
- return { status: "unavailable", disclosures: resolution.disclosures };
3666
- const revisionCell = orgRevisionEvidenceOf(resolution, (message) => deps.onError?.(new Error(message), { phase: "config", sessionId }));
3667
- const command = req.args?.command;
3668
- if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
3669
- return { status: "available", ...revisionCell };
3670
- const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
3671
- return verdict === undefined ? { status: "available", ...revisionCell } : { status: "available", verdict, ...revisionCell };
3672
- },
3673
- };
3674
- })();
3675
- const ruleOffersOf = (toolName, args, ask) => {
3676
- if (permissionRuleLane === undefined || toolName !== PERSISTED_RULE_TOOL)
3677
- return {};
3678
- if ((spec.principal === undefined || spec.principal === "") && deps.localOwnerRules !== true)
3679
- return {};
3680
- const closedDoor = (() => {
3681
- if (ask?.requiresRealApproval === true)
3682
- return "mandated";
3683
- if (ask?.persistedRuleShadowed !== undefined)
3684
- return "shadowed";
3685
- if (ask?.decisionReason === "hook")
3686
- return "mandated";
3687
- if (ask?.matchedAskRule !== undefined)
3688
- return "shadowed";
3689
- if (ask?.inheritedUnresolved === true)
3690
- return "mandated";
3691
- if (ask?.ancestorResolved === true)
3692
- return "mandated";
3693
- return undefined;
3694
- })();
3695
- if (closedDoor !== undefined)
3696
- return { ruleOffersAbsence: closedDoor };
3697
- if (persistedRuleMandateOf({
3698
- egress: egressTools.has(toolName),
3699
- irreversibility: irreversibilityTier.get(toolName),
3700
- shellGated: shellGatedBash,
3701
- probeMandated: ask?.probeMandated === true,
3702
- }) !== undefined) {
3703
- return { ruleOffersAbsence: "mandated" };
3704
- }
3705
- const command = args?.command;
3706
- if (typeof command !== "string")
3707
- return { ruleOffersAbsence: "lane_cannot_speak" };
3708
- const offers = suggestRulesForCommand(command, ask?.segmentCoverage !== undefined ? { coverage: ask.segmentCoverage } : undefined);
3709
- return offers.length > 0 ? { ruleOffers: offers } : { ruleOffersAbsence: "lane_cannot_speak" };
3710
- };
3868
+ const permissionRuleOrgLane = makeOrgAdjudicationLane(deps.permissionRuleOrg, questionToolMounted, (message) => deps.onError?.(new Error(message), { phase: "config", sessionId }));
3869
+ const ruleOffersOf = makeRuleOffersOf({
3870
+ laneArmed: permissionRuleLane !== undefined,
3871
+ principal: spec.principal,
3872
+ localOwnerDeclared: deps.localOwnerRules === true,
3873
+ egressTools,
3874
+ irreversibilityTier,
3875
+ shellGatedBash,
3876
+ taskRoot: taskRootFinal,
3877
+ cwdRef: handsCwdRef,
3878
+ deniesDirectoryRead: readDenyMatcher !== undefined ? (d) => readDenyMatcher.matchPath(d) !== null : undefined,
3879
+ });
3711
3880
  const frozenClassifierExcluded = (d) => d.decisionReason === "hook" || d.matchedAskRule !== undefined;
3712
3881
  const recheckApprovedEdit = async (pol, onAskOf, creq, edit, csignal, ancestorDecider) => {
3713
3882
  let editArgs = edit;
@@ -4159,14 +4328,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4159
4328
  });
4160
4329
  }
4161
4330
  if (effectivePolicy || hooks?.preToolUse || egressTools.size > 0 || irreversibleTools.size > 0 || resourceSuspendEligible || platformSuspendArmed || spec.enablePlanMode === true) {
4331
+ const composedCallSignal = (callSignal) => composeCallSignal(abortController.signal, callSignal);
4162
4332
  const adjudicate = effectivePolicy
4163
- ? (req) => raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot, ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}) }, abortController.signal)), abortController.signal, () => ({
4164
- action: "deny",
4165
- message: "policy check aborted (task timed out or cancelled)",
4166
- }))
4333
+ ? (req, callSignal) => {
4334
+ const signal = composedCallSignal(callSignal);
4335
+ return raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot, ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}) }, signal)), signal, () => ({
4336
+ action: "deny",
4337
+ message: "policy check aborted (task timed out or cancelled)",
4338
+ }));
4339
+ }
4167
4340
  : undefined;
4168
4341
  const approvalPreviewOf = (toolName, args) => resolveApprovalPreview(tools, toolName, args);
4169
- const resolveAskBound = async (decision, req) => {
4342
+ const resolveAskBound = async (decision, req, callSignal) => {
4170
4343
  if (inheritedUnavailableAsks.delete(req.toolCallId)) {
4171
4344
  return {
4172
4345
  action: "deny",
@@ -4177,23 +4350,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4177
4350
  approverUnavailable: true,
4178
4351
  };
4179
4352
  }
4180
- if (decision.decisionReason === undefined || decision.decisionReason === "rule") {
4181
- const grant = inheritedAskGrants.get(req.toolCallId);
4182
- if (grant !== undefined && askApproverIdentity(grant.approver) === askApproverIdentity(onAsk) && grant.argsJson === askGrantShapeOf(req.args)) {
4183
- inheritedAskGrants.delete(req.toolCallId);
4184
- humanReviewRef.count += 1;
4185
- humanReviewRef.totalWaitMs += grant.waitMs;
4186
- const toolArg = primaryActivityArg(req.args);
4187
- humanReviewRef.gates.push({
4188
- kind: "human",
4189
- waitMs: grant.waitMs,
4190
- decision: "allow",
4191
- toolName: req.toolName,
4192
- ...(toolArg !== undefined ? { toolArg } : {}),
4193
- });
4194
- return { action: "allow", presentedInput: grant.presented };
4195
- }
4196
- }
4353
+ const grantReuse = consumeInheritedAskGrant(inheritedAskGrants, onAsk, humanReviewRef, decision, req);
4354
+ if (grantReuse !== undefined)
4355
+ return grantReuse;
4197
4356
  const t0 = now();
4198
4357
  const resolved = await resolveAsk({
4199
4358
  toolName: req.toolName,
@@ -4212,9 +4371,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4212
4371
  ...(decision.action === "ask" && decision.probeReason !== undefined ? { probeReason: decision.probeReason } : {}),
4213
4372
  ...(decision.action === "ask" && decision.probeCause !== undefined ? { probeCause: decision.probeCause } : {}),
4214
4373
  ...(decision.action === "ask" && decision.ruleEvidence !== undefined ? { ruleEvidence: decision.ruleEvidence } : {}),
4215
- }, onAsk, abortController.signal);
4374
+ }, onAsk, composedCallSignal(callSignal), lateAskSettlementObserver({ toolName: req.toolName, toolCallId: req.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
4216
4375
  const waitMs = Math.max(0, now() - t0);
4217
- if (resolved.approverUnavailable !== true) {
4376
+ if (resolved.approverUnavailable !== true && resolved.resolution !== "task_aborted") {
4218
4377
  humanReviewRef.count += 1;
4219
4378
  humanReviewRef.totalWaitMs += waitMs;
4220
4379
  const toolArg = primaryActivityArg(req.args);
@@ -4325,11 +4484,33 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4325
4484
  placementRootSessionId: stampPlacementRootSessionId(placementRootResolved),
4326
4485
  externalContentTarget: externalContentTargetActive ? true : undefined,
4327
4486
  });
4328
- const commitSuspendSaga = async (token, cp, remoteEnv, remoteHandle) => {
4487
+ const compensatePausedVM = (remoteEnv, snapshotId) => compensateUnparkedPause(remoteEnv, snapshotId, {
4488
+ boundMs: PARK_COMPENSATION_TIMEOUT_MS,
4489
+ noteFailure: (note) => remoteEnvFailures.push(note),
4490
+ disclose: (err) => {
4491
+ try {
4492
+ deps.onError?.(err, { phase: "config", sessionId });
4493
+ }
4494
+ catch {
4495
+ }
4496
+ },
4497
+ });
4498
+ const commitSuspendSaga = async (token, cp, remoteEnv, remoteHandle, cutSignal) => {
4329
4499
  if (!checkpointStore)
4330
- return { ok: false };
4500
+ return { tag: "absent" };
4331
4501
  if (memoryEngineSession)
4332
4502
  await memoryEngineSession.harvest("checkpoint");
4503
+ if (cutSignal?.aborted) {
4504
+ if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
4505
+ const back = await compensatePausedVM(remoteEnv, remoteHandle.snapshotId);
4506
+ if (!back.ok) {
4507
+ abortController.abort();
4508
+ void harness.abort();
4509
+ return { tag: "compensation_failed", reason: back.reason };
4510
+ }
4511
+ }
4512
+ return { tag: "cut" };
4513
+ }
4333
4514
  const announceCommittedScreeningPark = () => {
4334
4515
  const committedCount = cp.state.inheritedGate?.parentConstraintCount;
4335
4516
  if (cp.state.inheritedGate?.requiresParentConstraint === true &&
@@ -4367,7 +4548,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4367
4548
  try {
4368
4549
  await checkpointStore.put(token, cp);
4369
4550
  announceCommittedScreeningPark();
4370
- return { ok: true };
4551
+ return { tag: "committed" };
4371
4552
  }
4372
4553
  catch (putErr) {
4373
4554
  deps.onError?.(putErr, { phase: "config", sessionId });
@@ -4383,7 +4564,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4383
4564
  catch {
4384
4565
  }
4385
4566
  announceCommittedScreeningPark();
4386
- return { ok: true };
4567
+ return { tag: "committed" };
4387
4568
  }
4388
4569
  if (confirmed === "unknown") {
4389
4570
  const unknownReason = "the approval checkpoint's state cannot be established (the store rejected the write and then could not be read back; a committed-but-unacknowledged row may exist) — the run is stopped rather than continued past an approval whose durable record is unknown";
@@ -4398,29 +4579,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4398
4579
  }
4399
4580
  abortController.abort();
4400
4581
  void harness.abort();
4401
- return { ok: false, reason: unknownReason };
4582
+ return { tag: "unknown", reason: unknownReason };
4402
4583
  }
4403
4584
  const reason = "the approval checkpoint could not be persisted (the store rejected the write; the deployment's error face carries the store's own message)";
4404
4585
  if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
4405
- const { outcome: back, attempts: backAttempts } = await restoreWorkspaceWithRetry(remoteEnv, remoteHandle.snapshotId, {
4406
- abortSignal: abortController.signal,
4407
- });
4408
- if (back.ok) {
4409
- const init = await remoteEnv.postResumeInit();
4410
- if (init.ok)
4411
- return { ok: false, reason };
4412
- remoteEnvFailures.push(remoteEnvFailureNote("postResumeInit", init.error, 1));
4413
- deps.onError?.(init.error, { phase: "config", sessionId });
4414
- }
4415
- else {
4416
- remoteEnvFailures.push(remoteEnvFailureNote("resumeVM", back.error, backAttempts));
4417
- deps.onError?.(back.error, { phase: "config", sessionId });
4418
- }
4586
+ const back = await compensatePausedVM(remoteEnv, remoteHandle.snapshotId);
4587
+ if (back.ok)
4588
+ return { tag: "absent", reason };
4419
4589
  abortController.abort();
4420
4590
  void harness.abort();
4421
- return { ok: false, reason };
4591
+ return { tag: "compensation_failed", reason };
4422
4592
  }
4423
- return { ok: false, reason };
4593
+ return { tag: "absent", reason };
4424
4594
  }
4425
4595
  };
4426
4596
  const suspendLoopCapHit = (count, cap, detail) => {
@@ -4502,7 +4672,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4502
4672
  ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
4503
4673
  ...(spec.principal ? { principal: spec.principal } : {}),
4504
4674
  };
4505
- if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
4675
+ if ((await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).tag !== "committed")
4506
4676
  return false;
4507
4677
  publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, scope, remoteHandle, cp.checkpointId, cp.pendingAction);
4508
4678
  try {
@@ -4588,7 +4758,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4588
4758
  ...(spec.principal ? { principal: spec.principal } : {}),
4589
4759
  ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
4590
4760
  };
4591
- if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
4761
+ if ((await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).tag !== "committed")
4592
4762
  return false;
4593
4763
  publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, scope, remoteHandle, cp.checkpointId, cp.pendingAction);
4594
4764
  try {
@@ -4602,7 +4772,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4602
4772
  return true;
4603
4773
  }
4604
4774
  : undefined;
4605
- const resolveContentAsk = async (req) => {
4775
+ const resolveContentAsk = async (req, callSignal) => {
4776
+ const contentAskSignal = composedCallSignal(callSignal);
4606
4777
  if (!contentAskRoutable(req.toolCallId) || liveQuestionFace === undefined || mountedQuestionTool === undefined) {
4607
4778
  return { kind: "unavailable", parkDeclined: false };
4608
4779
  }
@@ -4633,7 +4804,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4633
4804
  if (contentAskBindings.size >= CONTENT_ASK_BINDING_CAP && !contentAskBindings.has(req.toolCallId)) {
4634
4805
  return { kind: "unavailable", parkDeclined: true };
4635
4806
  }
4636
- if (abortController.signal.aborted) {
4807
+ if (contentAskSignal.aborted) {
4637
4808
  return { kind: "delivery_failure", code: "question.aborted", presentedInput: retained };
4638
4809
  }
4639
4810
  try {
@@ -4644,8 +4815,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4644
4815
  sourceTaskId: sessionId,
4645
4816
  boundInputHash: boundInputHashOf(retained),
4646
4817
  deliveryId,
4647
- }, abortController.signal))();
4648
- const settlement = await raceAbort(facePromise.then((outcome) => ({ tag: "outcome", outcome }), (error) => ({ tag: "threw", error })), abortController.signal, () => ({ tag: "aborted" }));
4818
+ }, contentAskSignal))();
4819
+ const settlement = await raceAbort(facePromise.then((outcome) => ({ tag: "outcome", outcome }), (error) => ({ tag: "threw", error })), contentAskSignal, () => ({ tag: "aborted" }));
4649
4820
  if (settlement.tag === "aborted") {
4650
4821
  void facePromise.then((late) => {
4651
4822
  if (classifyQuestionOutcome(late).shape !== "answered")
@@ -4673,7 +4844,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4673
4844
  bindOutcome({ kind: "failed", error }, retainedQuestionsHash);
4674
4845
  return {
4675
4846
  kind: "delivery_failure",
4676
- code: settlement.tag === "aborted" || abortController.signal.aborted ? "question.aborted" : "question.human_channel_failed",
4847
+ code: settlement.tag === "aborted" || contentAskSignal.aborted ? "question.aborted" : "question.human_channel_failed",
4677
4848
  presentedInput: retained,
4678
4849
  };
4679
4850
  }
@@ -4683,7 +4854,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4683
4854
  }
4684
4855
  };
4685
4856
  const suspendAsk = parkLaneArmed && checkpointStore !== undefined
4686
- ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason, probeReason, probeCause, segmentCoverage, matchedAskRule, probeMandated) => {
4857
+ ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason, probeReason, probeCause, segmentCoverage, matchedAskRule, probeMandated, callSignal) => {
4687
4858
  const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
4688
4859
  if (syncFirstEligible &&
4689
4860
  runtimeCaps?.forceDurableGate !== true &&
@@ -4691,6 +4862,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4691
4862
  !inheritedUnavailableAsks.has(req.toolCallId)) {
4692
4863
  return undefined;
4693
4864
  }
4865
+ const cutSignal = composedCallSignal(callSignal);
4694
4866
  let token;
4695
4867
  let gate;
4696
4868
  let cp;
@@ -4726,13 +4898,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4726
4898
  if (!presented.ok) {
4727
4899
  throw new ParkRefusal(`the stored form of "${req.toolName}"'s arguments could not be presented for re-adjudication (${describeThrown(presented.cause)})`, { cause: presented.cause });
4728
4900
  }
4729
- const reprojected = refuseOutOfContractDecision(await basePolicyForResumeEdit.check({
4901
+ const reprojectedRaced = await raceSettlementAgainstSignal(Promise.resolve(basePolicyForResumeEdit.check({
4730
4902
  toolName: req.toolName,
4731
4903
  args: presented.value,
4732
4904
  toolCallId: req.toolCallId,
4733
4905
  budget: budgetSnapshot,
4734
4906
  ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}),
4735
- }, abortController.signal));
4907
+ }, cutSignal)), cutSignal);
4908
+ if (reprojectedRaced.tag === "aborted")
4909
+ return undefined;
4910
+ if (reprojectedRaced.tag === "threw")
4911
+ throw reprojectedRaced.error;
4912
+ const reprojected = refuseOutOfContractDecision(reprojectedRaced.value);
4736
4913
  const demanded = reprojected.updatedInput !== undefined ? tryCloneArgs(reprojected.updatedInput) : undefined;
4737
4914
  const rewroteTheFiledValue = reprojected.updatedInput !== undefined &&
4738
4915
  !(demanded?.ok === true &&
@@ -4763,7 +4940,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4763
4940
  }
4764
4941
  const { messages } = await session.buildContext();
4765
4942
  const { batchToolCallIds, completedCallIds } = batchContextAt(messages, req.toolCallId);
4766
- if (abortController.signal.aborted) {
4943
+ if (cutSignal.aborted) {
4767
4944
  return undefined;
4768
4945
  }
4769
4946
  if (suspendLoopCapHit(suspendChainBase(), maxSuspends, ` for tool "${req.toolName}" — likely a resume/restart loop.`))
@@ -4771,12 +4948,20 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4771
4948
  if (remoteEnv !== undefined) {
4772
4949
  if (hasBackgroundShell(remoteEnv))
4773
4950
  await sweepBackgroundShells(remoteEnv, defaultTaskRegistry);
4774
- const snap = await remoteEnv.suspendVM({ abortSignal: abortController.signal });
4951
+ const snap = await remoteEnv.suspendVM({ abortSignal: cutSignal });
4775
4952
  if (!snap.ok) {
4776
4953
  remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
4777
4954
  throw new Error(`suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error });
4778
4955
  }
4779
4956
  remoteHandle = { ...remoteEnv.workspaceHandle(), snapshotId: snap.value };
4957
+ if (cutSignal.aborted) {
4958
+ const back = await compensatePausedVM(remoteEnv, snap.value);
4959
+ if (!back.ok) {
4960
+ abortController.abort();
4961
+ void harness.abort();
4962
+ }
4963
+ return undefined;
4964
+ }
4780
4965
  }
4781
4966
  else if (parkOnlyRemoteEnv !== undefined) {
4782
4967
  if (hasBackgroundShell(parkOnlyRemoteEnv) && spec.retainBackgroundProcesses !== true)
@@ -4882,16 +5067,31 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4882
5067
  };
4883
5068
  }
4884
5069
  catch (err) {
4885
- deps.onError?.(err, { phase: "config", sessionId });
4886
- return {
4887
- parkFailed: err instanceof ParkRefusal
4888
- ? err.message
4889
- : "the durable approval park could not be prepared (the deployment's error face carries the exception)",
4890
- };
5070
+ const cutWon = cutSignal.aborted;
5071
+ const preselected = cutWon
5072
+ ? undefined
5073
+ : {
5074
+ parkFailed: err instanceof ParkRefusal
5075
+ ? err.message
5076
+ : "the durable approval park could not be prepared (the deployment's error face carries the exception)",
5077
+ };
5078
+ try {
5079
+ deps.onError?.(err, { phase: "config", sessionId });
5080
+ }
5081
+ catch {
5082
+ }
5083
+ return preselected;
4891
5084
  }
4892
- const committed = await commitSuspendSaga(token, cp, remoteEnv, remoteHandle);
4893
- if (!committed.ok) {
4894
- return committed.reason !== undefined ? { parkFailed: committed.reason } : undefined;
5085
+ const committed = await commitSuspendSaga(token, cp, remoteEnv, remoteHandle, cutSignal);
5086
+ if (committed.tag !== "committed") {
5087
+ if (committed.tag === "cut")
5088
+ return undefined;
5089
+ if (committed.tag === "absent") {
5090
+ if (cutSignal.aborted)
5091
+ return undefined;
5092
+ return committed.reason !== undefined ? { parkFailed: committed.reason } : undefined;
5093
+ }
5094
+ return { parkFailed: committed.reason };
4895
5095
  }
4896
5096
  publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, cp.scope, remoteHandle, cp.checkpointId, cp.pendingAction);
4897
5097
  try {
@@ -4956,6 +5156,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4956
5156
  result = await runToolGate({
4957
5157
  onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: hostTaskId, site: f.site, message: f.error.message, ts: Date.now() })),
4958
5158
  event: e,
5159
+ ...(e.signal !== undefined ? { callSignal: e.signal } : {}),
4959
5160
  identity: hookIdentity,
4960
5161
  reminderMark,
4961
5162
  preToolUse: ownGatePreToolUse,