@sema-agent/core 7.1.0 → 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 (34) hide show
  1. package/CHANGELOG.md +36 -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/send-message-tool.d.ts +13 -0
  9. package/dist/agents/send-message-tool.js +36 -12
  10. package/dist/core/checkpoint-store.d.ts +189 -3
  11. package/dist/core/checkpoint-store.js +56 -16
  12. package/dist/core/hooks.d.ts +15 -8
  13. package/dist/core/hooks.js +6 -3
  14. package/dist/core/permission-rule-consent.d.ts +72 -23
  15. package/dist/core/permission-rule-consent.js +115 -26
  16. package/dist/core/permission-rule-model.d.ts +245 -51
  17. package/dist/core/permission-rule-model.js +312 -54
  18. package/dist/core/permission-rule-org.js +13 -6
  19. package/dist/core/remote-env.d.ts +8 -1
  20. package/dist/core/runner/assemble-result.js +2 -1
  21. package/dist/core/runner/prepare-task.d.ts +39 -1
  22. package/dist/core/runner/prepare-task.js +278 -113
  23. package/dist/core/runner/prepare-workspace-restore.d.ts +6 -1
  24. package/dist/core/runner/prepare-workspace-restore.js +2 -1
  25. package/dist/core/runner/runtask.js +13 -3
  26. package/dist/core/task-notification.d.ts +64 -5
  27. package/dist/core/task-notification.js +25 -4
  28. package/dist/core/tool-policy.d.ts +11 -0
  29. package/dist/core/types.d.ts +23 -0
  30. package/dist/core/untrusted-text.js +17 -1
  31. package/dist/index.d.ts +6 -3
  32. package/dist/index.js +5 -2
  33. package/package.json +1 -1
  34. package/test/export-surface.snapshot.json +125 -1
@@ -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";
@@ -713,6 +714,93 @@ function consumeInheritedAskGrant(grants, onAsk, humanReviewRef, decision, req)
713
714
  });
714
715
  return { action: "allow", presentedInput: grant.presented };
715
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
+ }
716
804
  function raceAbort(p, signal, onAbort) {
717
805
  if (signal.aborted)
718
806
  return Promise.resolve(onAbort());
@@ -869,6 +957,87 @@ function persistedRuleHitOf(admitting) {
869
957
  ? undefined
870
958
  : { rules: admitting.map((r) => ({ rule: r.rule, dots: r.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) })) };
871
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
+ }
872
1041
  function cwdConflictsRestoreError(requestedCwd) {
873
1042
  const e = new Error(`RunInternals.requestedCwd ("${requestedCwd}") cannot be combined with a checkpoint workspace restore — ` +
874
1043
  `the restored workspace's own mount path is authoritative for the task root, so a requested cwd on this leg ` +
@@ -2037,10 +2206,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2037
2206
  handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
2038
2207
  shellGatedBash = handsReadFace.shellGatedBash;
2039
2208
  shellGatedMonitor = handsReadFace.shellGatedMonitor;
2040
- if (backgroundTaskToolsActive || workflowToolsActive) {
2041
- toolEffects.set("TaskOutput", "read");
2042
- toolEffects.set("TaskStop", "write");
2043
- 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
+ }
2044
2216
  if (runnerSelf && !(spec.tools ?? []).some((t) => t.name === SEND_MESSAGE_TOOL_NAME)) {
2045
2217
  toolEffects.set(SEND_MESSAGE_TOOL_NAME, "write");
2046
2218
  axisExplicitNegatives.set(SEND_MESSAGE_TOOL_NAME, { ...axisExplicitNegatives.get(SEND_MESSAGE_TOOL_NAME), egress: false });
@@ -2070,7 +2242,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2070
2242
  owner: hostTaskId,
2071
2243
  scope: taskScope,
2072
2244
  ...(sessionId !== undefined ? { sessionId } : {}),
2073
- ...(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,
2074
2246
  ...(internals?.onSubagentSpawn !== undefined ? { sink: internals.onSubagentSpawn } : {}),
2075
2247
  ...(internals?.parentNotify !== undefined
2076
2248
  ? { uplink: internals.parentNotify, ...(internals.parentPeerRef !== undefined ? { uplinkRecipient: internals.parentPeerRef } : {}) }
@@ -2095,7 +2267,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2095
2267
  detail: { handle, ...(scope !== undefined ? { scope } : {}) },
2096
2268
  }),
2097
2269
  })));
2098
- if (!(spec.tools ?? []).some((t) => t.name === AGENT_TRANSCRIPT_TOOL_NAME)) {
2270
+ if (delegationSurfaceActive && !(spec.tools ?? []).some((t) => t.name === AGENT_TRANSCRIPT_TOOL_NAME)) {
2099
2271
  tools.push(firstPartyOffload(createAgentTranscriptTool({
2100
2272
  runner: runnerSelf,
2101
2273
  registry: defaultTaskRegistry,
@@ -3659,10 +3831,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3659
3831
  const anonymous = spec.principal === undefined || spec.principal === "";
3660
3832
  if (anonymous && !localOwnerDeclared)
3661
3833
  return undefined;
3662
- if (req.toolName !== PERSISTED_RULE_TOOL)
3663
- return undefined;
3664
- const command = req.args?.command;
3665
- if (typeof command !== "string")
3834
+ if (req.toolName !== PERSISTED_RULE_TOOL && req.toolName !== DIRECTORY_RULE_TOOL)
3666
3835
  return undefined;
3667
3836
  let listed;
3668
3837
  try {
@@ -3682,76 +3851,32 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3682
3851
  return { unreadable: true };
3683
3852
  }
3684
3853
  const table = await spliceSessionOverlayRows(deps.sessionPermissionRules, sessionId, listed.rules, deps.tracer, hostTaskId);
3685
- 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 } : {}) });
3686
3861
  if (admitting !== undefined)
3687
3862
  return persistedRuleHitOf(admitting);
3688
- 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 } : {}) });
3689
3864
  return coverage !== undefined ? { segmentCoverage: coverage } : undefined;
3690
3865
  },
3691
3866
  };
3692
3867
  })();
3693
- const permissionRuleOrgLane = (() => {
3694
- const overlay = deps.permissionRuleOrg;
3695
- if (overlay === undefined)
3696
- return undefined;
3697
- return {
3698
- adjudicate: async (req) => {
3699
- if (req.toolName === ASK_USER_QUESTION_TOOL_NAME && questionToolMounted)
3700
- return { status: "available" };
3701
- let resolution;
3702
- try {
3703
- resolution = await overlay.resolve();
3704
- }
3705
- catch (err) {
3706
- return { status: "unavailable", disclosures: [`the org rule overlay threw: ${err instanceof Error ? err.message : String(err)}`] };
3707
- }
3708
- if (resolution.status === "unavailable")
3709
- return { status: "unavailable", disclosures: resolution.disclosures };
3710
- const revisionCell = orgRevisionEvidenceOf(resolution, (message) => deps.onError?.(new Error(message), { phase: "config", sessionId }));
3711
- const command = req.args?.command;
3712
- if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
3713
- return { status: "available", ...revisionCell };
3714
- const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
3715
- return verdict === undefined ? { status: "available", ...revisionCell } : { status: "available", verdict, ...revisionCell };
3716
- },
3717
- };
3718
- })();
3719
- const ruleOffersOf = (toolName, args, ask) => {
3720
- if (permissionRuleLane === undefined || toolName !== PERSISTED_RULE_TOOL)
3721
- return {};
3722
- if ((spec.principal === undefined || spec.principal === "") && deps.localOwnerRules !== true)
3723
- return {};
3724
- const closedDoor = (() => {
3725
- if (ask?.requiresRealApproval === true)
3726
- return "mandated";
3727
- if (ask?.persistedRuleShadowed !== undefined)
3728
- return "shadowed";
3729
- if (ask?.decisionReason === "hook")
3730
- return "mandated";
3731
- if (ask?.matchedAskRule !== undefined)
3732
- return "shadowed";
3733
- if (ask?.inheritedUnresolved === true)
3734
- return "mandated";
3735
- if (ask?.ancestorResolved === true)
3736
- return "mandated";
3737
- return undefined;
3738
- })();
3739
- if (closedDoor !== undefined)
3740
- return { ruleOffersAbsence: closedDoor };
3741
- if (persistedRuleMandateOf({
3742
- egress: egressTools.has(toolName),
3743
- irreversibility: irreversibilityTier.get(toolName),
3744
- shellGated: shellGatedBash,
3745
- probeMandated: ask?.probeMandated === true,
3746
- }) !== undefined) {
3747
- return { ruleOffersAbsence: "mandated" };
3748
- }
3749
- const command = args?.command;
3750
- if (typeof command !== "string")
3751
- return { ruleOffersAbsence: "lane_cannot_speak" };
3752
- const offers = suggestRulesForCommand(command, ask?.segmentCoverage !== undefined ? { coverage: ask.segmentCoverage } : undefined);
3753
- return offers.length > 0 ? { ruleOffers: offers } : { ruleOffersAbsence: "lane_cannot_speak" };
3754
- };
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
+ });
3755
3880
  const frozenClassifierExcluded = (d) => d.decisionReason === "hook" || d.matchedAskRule !== undefined;
3756
3881
  const recheckApprovedEdit = async (pol, onAskOf, creq, edit, csignal, ancestorDecider) => {
3757
3882
  let editArgs = edit;
@@ -4359,11 +4484,33 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4359
4484
  placementRootSessionId: stampPlacementRootSessionId(placementRootResolved),
4360
4485
  externalContentTarget: externalContentTargetActive ? true : undefined,
4361
4486
  });
4362
- 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) => {
4363
4499
  if (!checkpointStore)
4364
- return { ok: false };
4500
+ return { tag: "absent" };
4365
4501
  if (memoryEngineSession)
4366
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
+ }
4367
4514
  const announceCommittedScreeningPark = () => {
4368
4515
  const committedCount = cp.state.inheritedGate?.parentConstraintCount;
4369
4516
  if (cp.state.inheritedGate?.requiresParentConstraint === true &&
@@ -4401,7 +4548,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4401
4548
  try {
4402
4549
  await checkpointStore.put(token, cp);
4403
4550
  announceCommittedScreeningPark();
4404
- return { ok: true };
4551
+ return { tag: "committed" };
4405
4552
  }
4406
4553
  catch (putErr) {
4407
4554
  deps.onError?.(putErr, { phase: "config", sessionId });
@@ -4417,7 +4564,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4417
4564
  catch {
4418
4565
  }
4419
4566
  announceCommittedScreeningPark();
4420
- return { ok: true };
4567
+ return { tag: "committed" };
4421
4568
  }
4422
4569
  if (confirmed === "unknown") {
4423
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";
@@ -4432,29 +4579,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4432
4579
  }
4433
4580
  abortController.abort();
4434
4581
  void harness.abort();
4435
- return { ok: false, reason: unknownReason };
4582
+ return { tag: "unknown", reason: unknownReason };
4436
4583
  }
4437
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)";
4438
4585
  if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
4439
- const { outcome: back, attempts: backAttempts } = await restoreWorkspaceWithRetry(remoteEnv, remoteHandle.snapshotId, {
4440
- abortSignal: abortController.signal,
4441
- });
4442
- if (back.ok) {
4443
- const init = await remoteEnv.postResumeInit();
4444
- if (init.ok)
4445
- return { ok: false, reason };
4446
- remoteEnvFailures.push(remoteEnvFailureNote("postResumeInit", init.error, 1));
4447
- deps.onError?.(init.error, { phase: "config", sessionId });
4448
- }
4449
- else {
4450
- remoteEnvFailures.push(remoteEnvFailureNote("resumeVM", back.error, backAttempts));
4451
- deps.onError?.(back.error, { phase: "config", sessionId });
4452
- }
4586
+ const back = await compensatePausedVM(remoteEnv, remoteHandle.snapshotId);
4587
+ if (back.ok)
4588
+ return { tag: "absent", reason };
4453
4589
  abortController.abort();
4454
4590
  void harness.abort();
4455
- return { ok: false, reason };
4591
+ return { tag: "compensation_failed", reason };
4456
4592
  }
4457
- return { ok: false, reason };
4593
+ return { tag: "absent", reason };
4458
4594
  }
4459
4595
  };
4460
4596
  const suspendLoopCapHit = (count, cap, detail) => {
@@ -4536,7 +4672,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4536
4672
  ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
4537
4673
  ...(spec.principal ? { principal: spec.principal } : {}),
4538
4674
  };
4539
- if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
4675
+ if ((await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).tag !== "committed")
4540
4676
  return false;
4541
4677
  publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, scope, remoteHandle, cp.checkpointId, cp.pendingAction);
4542
4678
  try {
@@ -4622,7 +4758,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4622
4758
  ...(spec.principal ? { principal: spec.principal } : {}),
4623
4759
  ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
4624
4760
  };
4625
- if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
4761
+ if ((await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).tag !== "committed")
4626
4762
  return false;
4627
4763
  publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, scope, remoteHandle, cp.checkpointId, cp.pendingAction);
4628
4764
  try {
@@ -4718,7 +4854,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4718
4854
  }
4719
4855
  };
4720
4856
  const suspendAsk = parkLaneArmed && checkpointStore !== undefined
4721
- ? 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) => {
4722
4858
  const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
4723
4859
  if (syncFirstEligible &&
4724
4860
  runtimeCaps?.forceDurableGate !== true &&
@@ -4726,6 +4862,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4726
4862
  !inheritedUnavailableAsks.has(req.toolCallId)) {
4727
4863
  return undefined;
4728
4864
  }
4865
+ const cutSignal = composedCallSignal(callSignal);
4729
4866
  let token;
4730
4867
  let gate;
4731
4868
  let cp;
@@ -4761,13 +4898,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4761
4898
  if (!presented.ok) {
4762
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 });
4763
4900
  }
4764
- const reprojected = refuseOutOfContractDecision(await basePolicyForResumeEdit.check({
4901
+ const reprojectedRaced = await raceSettlementAgainstSignal(Promise.resolve(basePolicyForResumeEdit.check({
4765
4902
  toolName: req.toolName,
4766
4903
  args: presented.value,
4767
4904
  toolCallId: req.toolCallId,
4768
4905
  budget: budgetSnapshot,
4769
4906
  ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}),
4770
- }, 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);
4771
4913
  const demanded = reprojected.updatedInput !== undefined ? tryCloneArgs(reprojected.updatedInput) : undefined;
4772
4914
  const rewroteTheFiledValue = reprojected.updatedInput !== undefined &&
4773
4915
  !(demanded?.ok === true &&
@@ -4798,7 +4940,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4798
4940
  }
4799
4941
  const { messages } = await session.buildContext();
4800
4942
  const { batchToolCallIds, completedCallIds } = batchContextAt(messages, req.toolCallId);
4801
- if (abortController.signal.aborted) {
4943
+ if (cutSignal.aborted) {
4802
4944
  return undefined;
4803
4945
  }
4804
4946
  if (suspendLoopCapHit(suspendChainBase(), maxSuspends, ` for tool "${req.toolName}" — likely a resume/restart loop.`))
@@ -4806,12 +4948,20 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4806
4948
  if (remoteEnv !== undefined) {
4807
4949
  if (hasBackgroundShell(remoteEnv))
4808
4950
  await sweepBackgroundShells(remoteEnv, defaultTaskRegistry);
4809
- const snap = await remoteEnv.suspendVM({ abortSignal: abortController.signal });
4951
+ const snap = await remoteEnv.suspendVM({ abortSignal: cutSignal });
4810
4952
  if (!snap.ok) {
4811
4953
  remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
4812
4954
  throw new Error(`suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error });
4813
4955
  }
4814
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
+ }
4815
4965
  }
4816
4966
  else if (parkOnlyRemoteEnv !== undefined) {
4817
4967
  if (hasBackgroundShell(parkOnlyRemoteEnv) && spec.retainBackgroundProcesses !== true)
@@ -4917,16 +5067,31 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4917
5067
  };
4918
5068
  }
4919
5069
  catch (err) {
4920
- deps.onError?.(err, { phase: "config", sessionId });
4921
- return {
4922
- parkFailed: err instanceof ParkRefusal
4923
- ? err.message
4924
- : "the durable approval park could not be prepared (the deployment's error face carries the exception)",
4925
- };
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;
4926
5084
  }
4927
- const committed = await commitSuspendSaga(token, cp, remoteEnv, remoteHandle);
4928
- if (!committed.ok) {
4929
- 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 };
4930
5095
  }
4931
5096
  publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, cp.scope, remoteHandle, cp.checkpointId, cp.pendingAction);
4932
5097
  try {
@@ -32,7 +32,12 @@ export declare function remoteEnvFailureNote(op: RemoteEnvFailureNote["op"], err
32
32
  * decides. A code outside the retryable family (`unsupported`, `auth_failed`) returns on the first
33
33
  * attempt — `withRetry` will not spend a second call on a permanent refusal.
34
34
  */
35
- export declare function restoreWorkspaceWithRetry(env: RemoteExecutionEnv, snapshotId: SnapshotId, options: VmLifecycleOptions): Promise<{
35
+ export declare function restoreWorkspaceWithRetry(env: RemoteExecutionEnv, snapshotId: SnapshotId, options: VmLifecycleOptions,
36
+ /** design/384 slice 2 (additive): observe each attempt AS IT STARTS. The returned `attempts` is
37
+ * only readable after settlement, and a caller whose wait on this promise is BOUNDED (the park
38
+ * compensation) must report the live attempt count when its bound fires mid-call — without this
39
+ * seat a deaf second attempt was reported as `attempts: 1`. */
40
+ onAttempt?: (attempt: number) => void): Promise<{
36
41
  outcome: Awaited<ReturnType<RemoteExecutionEnv["resumeVM"]>>;
37
42
  attempts: number;
38
43
  }>;
@@ -19,10 +19,11 @@ export function remoteEnvFailureNote(op, error, attempts) {
19
19
  }
20
20
  const REMOTE_RESTORE_MAX_ATTEMPTS = 2;
21
21
  const REMOTE_RESTORE_BACKOFF_MS = 200;
22
- export async function restoreWorkspaceWithRetry(env, snapshotId, options) {
22
+ export async function restoreWorkspaceWithRetry(env, snapshotId, options, onAttempt) {
23
23
  let attempts = 0;
24
24
  const outcome = await withRetry(async (attempt) => {
25
25
  attempts = attempt;
26
+ onAttempt?.(attempt);
26
27
  return env.resumeVM(snapshotId, options);
27
28
  }, { retryableCodes: RETRYABLE_REMOTE_ERROR_CODES, maxAttempts: REMOTE_RESTORE_MAX_ATTEMPTS, backoffMs: () => REMOTE_RESTORE_BACKOFF_MS }, { ...(options.abortSignal !== undefined ? { signal: options.abortSignal } : {}) });
28
29
  return { outcome, attempts };
@@ -5080,12 +5080,22 @@ export class Runner {
5080
5080
  await recheckGovernanceWindow();
5081
5081
  }
5082
5082
  this.locallyClaimedTokens.add(token);
5083
- const won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
5083
+ let won;
5084
+ let claimLossStatus;
5085
+ if (store.claimTerminal !== undefined) {
5086
+ const claim = await store.claimTerminal(token, cp.scope, { kind: "resolve", outcome: outcomeForStore, expect: { rev: cp.rev ?? 0 } });
5087
+ won = claim.claimed;
5088
+ if (!claim.claimed)
5089
+ claimLossStatus = claim.current.status;
5090
+ }
5091
+ else {
5092
+ won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
5093
+ }
5084
5094
  if (!won)
5085
5095
  this.locallyClaimedTokens.delete(token);
5086
5096
  if (!won) {
5087
- const live = await store.get(token);
5088
- if (live?.status === "pending") {
5097
+ const stillPending = claimLossStatus !== undefined ? claimLossStatus === "pending" : (await store.get(token))?.status === "pending";
5098
+ if (stillPending) {
5089
5099
  throw new CheckpointError("checkpoint.reopened_concurrently", "checkpoint changed concurrently (its revision advanced via a resolve/reopen cycle since this resume validated) — not executed; re-resume against the current state");
5090
5100
  }
5091
5101
  this.parentConstraintRegistry.delete(token);