@sema-agent/core 7.8.0 → 7.9.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.
- package/CHANGELOG.md +52 -1
- package/dist/agents/subagent.d.ts +1 -1
- package/dist/core/ask-origin.d.ts +12 -1
- package/dist/core/ask-origin.js +5 -1
- package/dist/core/checkpoint-store.d.ts +34 -28
- package/dist/core/gate-lanes.js +49 -23
- package/dist/core/gate-outcome.d.ts +8 -4
- package/dist/core/gate-outcome.js +2 -1
- package/dist/core/hooks.d.ts +56 -31
- package/dist/core/permission-rule-consent.d.ts +29 -36
- package/dist/core/permission-rule-consent.js +110 -60
- package/dist/core/permission-rule-model.d.ts +254 -55
- package/dist/core/permission-rule-model.js +323 -43
- package/dist/core/permission-rule-org.d.ts +9 -8
- package/dist/core/permission-rule-org.js +6 -15
- package/dist/core/permission-rule-provider.d.ts +10 -6
- package/dist/core/permission-rule-provider.js +12 -8
- package/dist/core/permission-rule-session.d.ts +7 -6
- package/dist/core/permission-rule-session.js +29 -10
- package/dist/core/permission-rule-store.d.ts +40 -18
- package/dist/core/permission-rule-store.js +68 -42
- package/dist/core/permission-rule-sync.d.ts +9 -3
- package/dist/core/permission-rule-sync.js +29 -25
- package/dist/core/permission-rule-syntax.d.ts +30 -0
- package/dist/core/permission-rule-syntax.js +44 -0
- package/dist/core/permission-rules.d.ts +55 -33
- package/dist/core/permission-rules.js +65 -55
- package/dist/core/persisted-rule-arms.d.ts +56 -0
- package/dist/core/persisted-rule-arms.js +48 -0
- package/dist/core/runner/contracts.d.ts +21 -2
- package/dist/core/runner/permission-rule-lanes.d.ts +33 -26
- package/dist/core/runner/permission-rule-lanes.js +27 -21
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-safety-scan.js +8 -2
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/resume-admission.d.ts +53 -0
- package/dist/core/runner/resume-admission.js +83 -0
- package/dist/core/runner/resume-apply.d.ts +50 -0
- package/dist/core/runner/resume-apply.js +184 -0
- package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
- package/dist/core/runner/resume-checkpoint-screen.js +108 -0
- package/dist/core/runner/resume-claim.d.ts +32 -0
- package/dist/core/runner/resume-claim.js +27 -0
- package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
- package/dist/core/runner/resume-internals-and-config.js +50 -0
- package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
- package/dist/core/runner/resume-policy-outcome.js +127 -0
- package/dist/core/runner/resume-preflight.d.ts +40 -0
- package/dist/core/runner/resume-preflight.js +122 -0
- package/dist/core/runner/resume-review-outcome.d.ts +30 -0
- package/dist/core/runner/resume-review-outcome.js +88 -0
- package/dist/core/runner/run-harness-handlers.js +1 -1
- package/dist/core/runner/runtask.d.ts +20 -0
- package/dist/core/runner/runtask.js +85 -734
- package/dist/core/runner/tool-end-body.d.ts +12 -5
- package/dist/core/runner/tool-end-body.js +5 -5
- package/dist/core/runner/tool-output-projection.d.ts +9 -6
- package/dist/core/runner/tool-output-projection.js +1 -18
- package/dist/core/runner/turn-attachments.d.ts +2 -2
- package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
- package/dist/core/tool-roster.d.ts +9 -0
- package/dist/core/tool-roster.js +15 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/stores/file/checkpoint-store.js +1 -1
- package/dist/stores/file/permission-rule-store.d.ts +28 -23
- package/dist/stores/file/permission-rule-store.js +64 -16
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/notebook.d.ts +1 -1
- package/dist/tools/fs/safety.d.ts +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +43 -11
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
|
|
2
1
|
import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
|
|
3
2
|
import { mintSystemReminder, openSystemReminder } from "../reminder-mint.js";
|
|
4
|
-
import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
|
|
5
3
|
import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
|
|
6
4
|
import { planRejectionClears, resolveTriggerWindow } from "../context-edit.js";
|
|
7
5
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, isSyntheticApiErrorMessage, uuidv7 } from "../../internal/harness.js";
|
|
8
6
|
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
9
|
-
import { CheckpointError,
|
|
7
|
+
import { CheckpointError, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, resolveCheckpointStore, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, realApprovalOrgFact } from "../checkpoint-store.js";
|
|
10
8
|
import { GIT_STATUS_ECHO_PREVIEW, stripGitStatusUnits } from "./git-status-frame.js";
|
|
11
9
|
import { settleExecutionRecord } from "./execution-record.js";
|
|
12
10
|
import { engineVersion } from "../version.js";
|
|
13
11
|
import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
|
|
14
12
|
import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
15
13
|
import { DEFAULT_COMPACTION_INSTRUCTIONS, createRapidRefillState, maybeCompact, sanitizeCompactionSettings } from "../auto-compaction.js";
|
|
16
|
-
import { ASK_USER_QUESTION_TOOL_NAME
|
|
14
|
+
import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
|
|
17
15
|
import { boundInputHashOf } from "../canonical-json.js";
|
|
18
16
|
import { computeCostMicroUsd, isModelPriced, malformedPricingField, modelCostToPricing } from "../pricing.js";
|
|
19
17
|
import { emitTrace } from "../trace.js";
|
|
@@ -38,20 +36,20 @@ import { AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTI
|
|
|
38
36
|
import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
|
|
39
37
|
import { effectiveDelegationFacts, prepareTask } from "./prepare-task.js";
|
|
40
38
|
import { drainForwardedFramesBeforeDone, forwardsSubagentEvents } from "./prepare-run-refs.js";
|
|
41
|
-
import { callFaceSeat, judgeParkedToolIdentity,
|
|
42
|
-
import {
|
|
39
|
+
import { callFaceSeat, judgeParkedToolIdentity, rosterEntryNamed, toolCallFaceOf } from "../tool-roster.js";
|
|
40
|
+
import { normalizePersistedRuleHit } from "../gate-lanes.js";
|
|
41
|
+
import { applyPersistedTightening, disclosedRuleSet } from "../persisted-rule-arms.js";
|
|
43
42
|
import { SessionReadFileStates } from "./prepare-hands-readface.js";
|
|
44
43
|
import { settleTeardownLeg } from "./teardown-bounded.js";
|
|
45
44
|
import { hasDestroy, isIsolated } from "../remote-env.js";
|
|
46
45
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
47
|
-
import {
|
|
46
|
+
import { formatHookFeedback, hookSeatExpiredError, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
|
|
48
47
|
import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
|
|
49
48
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
|
|
50
49
|
import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
|
|
51
50
|
import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOfSuppliedLayer, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
|
|
51
|
+
import { PAUSE_REGISTRY } from "../pause-registry.js";
|
|
52
|
+
import { refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
|
|
55
53
|
import { mintGateOutcome } from "./gate-exit.js";
|
|
56
54
|
import { defaultTaskRegistry } from "../task-registry.js";
|
|
57
55
|
import { discloseDroppedPending, isDelegatedAgentTerminal, isSystemInjectionPriority, isTerminalTaskNotification, PendingSessionNotifications, renderTaskNotificationXml, SYSTEM_INJECTION_PRIORITIES, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
@@ -60,23 +58,25 @@ import { createPeerInboundChainRef, createPeerSelfRef } from "../../agents/peer-
|
|
|
60
58
|
import { createRunState } from "./initial-run-state.js";
|
|
61
59
|
import { nextHumanInputSeq, sameAcceptedSteerInput } from "./steer-admission.js";
|
|
62
60
|
import { reconciledToolEndBody, toolEndBodyFrom, toolResultMsg, writeFamilyOfCanonical } from "./tool-end-body.js";
|
|
63
|
-
import { answerFaceForRedeemedCall,
|
|
61
|
+
import { answerFaceForRedeemedCall, deepJsonEqual, DEFERRED_REISSUE, pendingContentAskCallId, resumeContinuation, resumeDecisionWasNegative } from "./decide-continuation.js";
|
|
64
62
|
import { awaitChargeWithSlowDisclosure, DEFAULT_PRECALL_OUTPUT_TOKENS, discloseUnevaluableWindow, platformLimitTerminal, resolveMaxTurns, startTimeout, TIMER_LATENESS_REPORT_MS } from "./clock-and-limits.js";
|
|
65
63
|
import { gitRestateOption, resolveGitLegDelivery, wrapGitFrame } from "./git-leg-delivery.js";
|
|
66
64
|
import { COMPACTION_FREED_EPSILON, COMPACTION_REGROWTH_FACTOR, MAX_CONSECUTIVE_COMPACTION_FAILURES, createTurnBoundary } from "./run-turn-boundary.js";
|
|
67
65
|
import { createHarnessHandlers } from "./run-harness-handlers.js";
|
|
68
|
-
|
|
66
|
+
import { resumeAdmission } from "./resume-admission.js";
|
|
67
|
+
import { resumeReviewOutcome } from "./resume-review-outcome.js";
|
|
68
|
+
import { resumePolicyOutcome } from "./resume-policy-outcome.js";
|
|
69
|
+
import { resumeCheckpointScreen } from "./resume-checkpoint-screen.js";
|
|
70
|
+
import { resumeInternalsAndConfig } from "./resume-internals-and-config.js";
|
|
71
|
+
import { resumeApply } from "./resume-apply.js";
|
|
72
|
+
import { resumePreflight } from "./resume-preflight.js";
|
|
73
|
+
import { resumeClaim } from "./resume-claim.js";
|
|
69
74
|
const STOP_HOOK_BLOCK_CAP = 8;
|
|
70
75
|
const ORG_DISCLOSURE_MAX_CHARS = 600;
|
|
71
76
|
const SUGGESTIONS_DEFAULT_COUNT = 3;
|
|
72
77
|
const SUGGESTIONS_MAX_COUNT = 8;
|
|
73
78
|
const SUGGESTIONS_TIMEOUT_MS = 30_000;
|
|
74
79
|
const SUGGESTIONS_TRANSCRIPT_MESSAGES = 12;
|
|
75
|
-
const REFUSED_DECIDE_KEYS = [
|
|
76
|
-
["gateOutcome", "the settlement record is minted by core from the host's decision facts and the row's origin, never supplied"],
|
|
77
|
-
["settledBy", 'a retired key — the host supplies decision FACTS as hostDecision{decidedBy: "person" | "sla_timeout", approver?} and core mints the settlement word from them'],
|
|
78
|
-
["approver", "a retired key — the attribution rides the facts it belongs to, as hostDecision.approver"],
|
|
79
|
-
];
|
|
80
80
|
export class Runner {
|
|
81
81
|
deps;
|
|
82
82
|
sessions;
|
|
@@ -87,7 +87,10 @@ export class Runner {
|
|
|
87
87
|
suspendedEnvReaps = new Map();
|
|
88
88
|
locallyClaimedTokens = new Set();
|
|
89
89
|
static PARENT_CONSTRAINT_REGISTRY_CAP = 1024;
|
|
90
|
+
depsSeat;
|
|
90
91
|
constructor(deps) {
|
|
92
|
+
const runner = this;
|
|
93
|
+
this.depsSeat = { get deps() { return runner.deps; } };
|
|
91
94
|
this.deps = deps;
|
|
92
95
|
if (this.deps.tiers) {
|
|
93
96
|
this.deps = { ...this.deps, models: expandTiers(this.deps.models, this.deps.tiers) };
|
|
@@ -979,6 +982,7 @@ export class Runner {
|
|
|
979
982
|
const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
|
|
980
983
|
notificationIdent = ident;
|
|
981
984
|
queue.push({ type: "wiring_manifest", manifest: prepared.wiringManifest, ...ident() });
|
|
985
|
+
prepared.toolRosterDeltas.subscribe((delta) => queue.push({ type: "tool_roster_delta", delta, ...ident() }));
|
|
982
986
|
const delegationLifecycleNotifier = createSafeNotifier({
|
|
983
987
|
onError: (f) => console.warn(`[sema-core] ${f.site}: delegation-lifecycle observer threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
|
|
984
988
|
});
|
|
@@ -1540,7 +1544,7 @@ export class Runner {
|
|
|
1540
1544
|
const withBrainSinks = (fn) => runWithStatusSink(statusEmit, () => runWithBrainTelemetry(telemetryEmit, () => runWithReasoningWireFacts(observeReasoningWireFacts, fn)));
|
|
1541
1545
|
const toolLabels = new Map(prepared.tools.flatMap((t) => (t.label !== undefined && t.label !== t.name ? [[t.name, t.label]] : [])));
|
|
1542
1546
|
for (const orphan of prepared.wakeRecovered) {
|
|
1543
|
-
queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan), ...ident() });
|
|
1547
|
+
queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan, prepared.structuredProjector), ...ident() });
|
|
1544
1548
|
emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId);
|
|
1545
1549
|
}
|
|
1546
1550
|
const postToolBatchHook = (spec.hooks ?? this.deps.hooks)?.postToolBatch;
|
|
@@ -2512,6 +2516,7 @@ export class Runner {
|
|
|
2512
2516
|
}
|
|
2513
2517
|
notificationLaneLive = false;
|
|
2514
2518
|
unsubscribeTaskNotifications();
|
|
2519
|
+
prepared.toolRosterDeltas.unsubscribe();
|
|
2515
2520
|
unsubGitRetry();
|
|
2516
2521
|
unsubBoundary();
|
|
2517
2522
|
unsub();
|
|
@@ -2544,7 +2549,7 @@ export class Runner {
|
|
|
2544
2549
|
const report = await reconcileInterruptedSession(prepared.session, prepared.toolEffects, undefined, startedToolCallIds);
|
|
2545
2550
|
orphansClosed = report.recovered.length;
|
|
2546
2551
|
for (const orphan of report.recovered) {
|
|
2547
|
-
queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan), ...ident() });
|
|
2552
|
+
queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan, prepared.structuredProjector), ...ident() });
|
|
2548
2553
|
emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId);
|
|
2549
2554
|
}
|
|
2550
2555
|
reconcileComplete = true;
|
|
@@ -3017,719 +3022,41 @@ export class Runner {
|
|
|
3017
3022
|
return stream.result();
|
|
3018
3023
|
}
|
|
3019
3024
|
async resumeStream(token, outcome, taskConfig, internals) {
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
}
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
}
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
suppliedMessage = outcome.message;
|
|
3056
|
-
const gk = cp.gate.kind;
|
|
3057
|
-
const decideEntry = gk === "human" || gk === "irreversible_ask"
|
|
3058
|
-
? 'a `policy_ask` outcome (decision allow/deny bound to the pending tool call)'
|
|
3059
|
-
: gk === "needs_review"
|
|
3060
|
-
? 'a `dry_run_review` outcome (decision approve/reject)'
|
|
3061
|
-
: gk === "plan_review"
|
|
3062
|
-
? 'a `plan_review` outcome (decision approve/edit/reject)'
|
|
3063
|
-
: gk === "resource_limit"
|
|
3064
|
-
? 'a `resource_limit` outcome (decision "continue")'
|
|
3065
|
-
: undefined;
|
|
3066
|
-
if (gk !== "task_done") {
|
|
3067
|
-
throw new CheckpointError("wake.gate_pending", `cannot wake: this checkpoint awaits a pending gate decision (gate "${gk}") — wake is not a decision ` +
|
|
3068
|
-
`channel; resume it with ${decideEntry ?? "its own gate outcome"} instead`);
|
|
3069
|
-
}
|
|
3070
|
-
const pa = cp.pendingAction;
|
|
3071
|
-
switch (pa.kind) {
|
|
3072
|
-
case "task_done":
|
|
3073
|
-
break;
|
|
3074
|
-
case "tool_approval":
|
|
3075
|
-
case "resource_limit":
|
|
3076
|
-
case "review":
|
|
3077
|
-
case "plan_review": {
|
|
3078
|
-
throw new CheckpointError("wake.gate_pending", `cannot wake: this checkpoint still carries a pending gate-typed action (pendingAction "${pa.kind}" ` +
|
|
3079
|
-
`on a "${gk}" gate — inconsistent row) — wake is not a decision channel; resolve it through its ` +
|
|
3080
|
-
`own decide entry instead`);
|
|
3081
|
-
}
|
|
3082
|
-
default: {
|
|
3083
|
-
const _exhaustive = pa;
|
|
3084
|
-
void _exhaustive;
|
|
3085
|
-
throw new CheckpointError("wake.gate_pending", `cannot wake: this checkpoint carries an unrecognized pendingAction kind "${pa.kind}" ` +
|
|
3086
|
-
`(newer-version row?) — fail-closed: wake only serves a pure park (pendingAction "task_done")`);
|
|
3087
|
-
}
|
|
3088
|
-
}
|
|
3089
|
-
if (suppliedMessage !== undefined) {
|
|
3090
|
-
wakeMessage = validatePendingSteer(suppliedMessage);
|
|
3091
|
-
}
|
|
3092
|
-
else if (readPendingSteerQueue(cp.state).length === 0) {
|
|
3093
|
-
throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
|
|
3094
|
-
"wake would burn the checkpoint on a blank continuation; supply `message` or park a steer first");
|
|
3095
|
-
}
|
|
3096
|
-
}
|
|
3097
|
-
const gateMatch = isGateKind(cp.gate.kind) && resumeGateMatches(cp.gate.kind, outcomeGate);
|
|
3098
|
-
if (!gateMatch) {
|
|
3099
|
-
throw new CheckpointError("checkpoint.gate_mismatch", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") does not match checkpoint gate "${cp.gate.kind}" — resume serves ${Object.entries(PAUSE_REGISTRY)
|
|
3100
|
-
.filter(([, row]) => !row.terminal)
|
|
3101
|
-
.map(([k, row]) => `${k}/${row.resume}`)
|
|
3102
|
-
.join(", ")}`);
|
|
3103
|
-
}
|
|
3104
|
-
let plainReviewOutcome;
|
|
3105
|
-
if (cp.gate.kind === "plan_review") {
|
|
3106
|
-
const decide = outcome;
|
|
3107
|
-
const decision = decide.decision;
|
|
3108
|
-
const editedPlan = decide.editedPlan;
|
|
3109
|
-
const reason = decide.reason;
|
|
3110
|
-
if (decision !== "approve" && decision !== "edit" && decision !== "reject") {
|
|
3111
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the plan_review domain — a plan review is exactly "approve", "edit" or "reject"; refusing pre-CAS, the checkpoint stays pending`);
|
|
3112
|
-
}
|
|
3113
|
-
assertOutcomeText(editedPlan, "editedPlan");
|
|
3114
|
-
assertOutcomeText(reason, "reason");
|
|
3115
|
-
plainReviewOutcome = {
|
|
3116
|
-
gate: "plan_review",
|
|
3117
|
-
decision,
|
|
3118
|
-
...(editedPlan !== undefined ? { editedPlan } : {}),
|
|
3119
|
-
...(reason !== undefined ? { reason } : {}),
|
|
3120
|
-
};
|
|
3121
|
-
}
|
|
3122
|
-
else if (cp.gate.kind === "needs_review") {
|
|
3123
|
-
const decide = outcome;
|
|
3124
|
-
const decision = decide.decision;
|
|
3125
|
-
const reason = decide.reason;
|
|
3126
|
-
if (decision !== "approve" && decision !== "reject") {
|
|
3127
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the dry_run_review domain — a dry-run review is exactly "approve" or "reject"; refusing pre-CAS, the checkpoint stays pending`);
|
|
3128
|
-
}
|
|
3129
|
-
assertOutcomeText(reason, "reason");
|
|
3130
|
-
plainReviewOutcome = {
|
|
3131
|
-
gate: "dry_run_review",
|
|
3132
|
-
decision,
|
|
3133
|
-
...(reason !== undefined ? { reason } : {}),
|
|
3134
|
-
};
|
|
3135
|
-
}
|
|
3136
|
-
if (plainReviewOutcome !== undefined) {
|
|
3137
|
-
const reason = plainReviewOutcome.reason;
|
|
3138
|
-
if (plainReviewOutcome.decision === "reject" && reason && sanitizeUntrustedText(reason) !== reason) {
|
|
3139
|
-
throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
|
|
3140
|
-
}
|
|
3141
|
-
const capturedPlan = plainReviewOutcome.gate === "plan_review" ? plainReviewOutcome.editedPlan : undefined;
|
|
3142
|
-
if (plainReviewOutcome.decision === "edit" &&
|
|
3143
|
-
capturedPlan &&
|
|
3144
|
-
sanitizeUntrustedText(capturedPlan) !== capturedPlan) {
|
|
3145
|
-
throw new CheckpointError("checkpoint.invalid_outcome", "resume editedPlan must not contain a </system-reminder> tag");
|
|
3146
|
-
}
|
|
3147
|
-
if (cp.reopenReason === "env_failed") {
|
|
3148
|
-
const winner = cp.resolvedOutcome;
|
|
3149
|
-
if (winner === undefined) {
|
|
3150
|
-
throw new CheckpointError("checkpoint.reopen_revote", "review checkpoint was reopened after an env-restore failure (env_failed) but carries no persisted winner to replay — refusing to resume (inconsistent row, fail-closed)");
|
|
3151
|
-
}
|
|
3152
|
-
if (winner.decision !== plainReviewOutcome.decision) {
|
|
3153
|
-
throw new CheckpointError("checkpoint.reopen_revote", `an env_failed reopen replays the ALREADY-RECORDED review decision ("${winner.decision}") — refusing a re-vote with a different decision ("${plainReviewOutcome.decision}")`);
|
|
3154
|
-
}
|
|
3155
|
-
if (winner.updatedInput !== capturedPlan) {
|
|
3156
|
-
throw new CheckpointError("checkpoint.reopen_revote", "an env_failed reopen replays the ALREADY-RECORDED review decision — refusing a re-vote whose edited plan differs from the recorded one");
|
|
3157
|
-
}
|
|
3158
|
-
if (winner.reason !== undefined && winner.reason !== plainReviewOutcome.reason) {
|
|
3159
|
-
throw new CheckpointError("checkpoint.reopen_revote", "an env_failed reopen replays the ALREADY-RECORDED review decision — refusing a re-vote whose reviewer note differs from the recorded one");
|
|
3160
|
-
}
|
|
3161
|
-
}
|
|
3162
|
-
}
|
|
3163
|
-
let plainParkOutcome;
|
|
3164
|
-
if (outcomeGate === "wake") {
|
|
3165
|
-
plainParkOutcome = {
|
|
3166
|
-
gate: "wake",
|
|
3167
|
-
...(wakeMessage !== undefined ? { message: wakeMessage } : {}),
|
|
3168
|
-
};
|
|
3169
|
-
}
|
|
3170
|
-
else if (outcomeGate === "resource_limit") {
|
|
3171
|
-
const decision = outcome.decision;
|
|
3172
|
-
if (decision !== "continue") {
|
|
3173
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the resource_limit domain — a slice resume is exactly "continue"; refusing pre-CAS, the checkpoint stays pending`);
|
|
3174
|
-
}
|
|
3175
|
-
plainParkOutcome = { gate: "resource_limit", decision };
|
|
3176
|
-
}
|
|
3177
|
-
let suppliedAnswer;
|
|
3178
|
-
let redeemedAnswer;
|
|
3179
|
-
let plainPolicyOutcome;
|
|
3180
|
-
if (cp.gate.kind === "human" || cp.gate.kind === "irreversible_ask") {
|
|
3181
|
-
const decide = outcome;
|
|
3182
|
-
suppliedAnswer = decide.answer;
|
|
3183
|
-
if (suppliedAnswer !== undefined) {
|
|
3184
|
-
const reading = classifyQuestionOutcome(suppliedAnswer);
|
|
3185
|
-
if (reading.shape !== "answered") {
|
|
3186
|
-
throw new CheckpointError("checkpoint.invalid_outcome", "resume carried a content-ask `answer` that does not read as an answer (malformed, sparse, or not capturable as plain data) — " +
|
|
3187
|
-
"refusing pre-CAS rather than fabricating an empty human response; the checkpoint stays pending", { field: "answer" });
|
|
3188
|
-
}
|
|
3189
|
-
redeemedAnswer = reading.answer;
|
|
3190
|
-
}
|
|
3191
|
-
const updatedInputRaw = decide.updatedInput;
|
|
3192
|
-
let updatedInput;
|
|
3193
|
-
try {
|
|
3194
|
-
updatedInput = updatedInputRaw === undefined ? undefined : structuredClone(updatedInputRaw);
|
|
3195
|
-
}
|
|
3196
|
-
catch {
|
|
3197
|
-
throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` that could not be captured as plain data — refusing pre-CAS; the checkpoint stays pending");
|
|
3198
|
-
}
|
|
3199
|
-
if (Object.is(updatedInput, -0))
|
|
3200
|
-
updatedInput = 0;
|
|
3201
|
-
if (updatedInput !== undefined && !canonicalizeCapturedPlainData(updatedInput)) {
|
|
3202
|
-
throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` outside the plain-JSON value domain (a Map/Set/Date/buffer, a sparse or expando-carrying array, a non-finite number, or a cycle) — " +
|
|
3203
|
-
"such a value reads differently on the two sides of the durable store; refusing pre-CAS, the checkpoint stays pending");
|
|
3204
|
-
}
|
|
3205
|
-
const decision = decide.decision;
|
|
3206
|
-
if (decision !== "allow" && decision !== "deny") {
|
|
3207
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the policy_ask domain — a decide is exactly "allow" or "deny"; refusing pre-CAS, the checkpoint stays pending`);
|
|
3208
|
-
}
|
|
3209
|
-
for (const [key, why] of REFUSED_DECIDE_KEYS) {
|
|
3210
|
-
if (Reflect.get(decide, key) !== undefined) {
|
|
3211
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries \`${key}\` — ${why}; refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
|
|
3212
|
-
}
|
|
3213
|
-
}
|
|
3214
|
-
const hostDecisionRaw = decide.hostDecision;
|
|
3215
|
-
if (typeof hostDecisionRaw !== "object" || hostDecisionRaw === null) {
|
|
3216
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries no hostDecision — a policy_ask decide names who decided it ({ decidedBy: "person" | "sla_timeout", approver? }); refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
|
|
3217
|
-
}
|
|
3218
|
-
const decidedBy = hostDecisionRaw.decidedBy;
|
|
3219
|
-
if (decidedBy !== "person" && decidedBy !== "sla_timeout") {
|
|
3220
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume hostDecision.decidedBy "${describeSuppliedValue(decidedBy)}" is outside the closed set — it is exactly "person" or "sla_timeout"; refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
|
|
3221
|
-
}
|
|
3222
|
-
if (decidedBy === "sla_timeout" && decision === "allow") {
|
|
3223
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries decision "allow" decided by "sla_timeout" — an elapsed SLA window cannot be what approved an action that EXECUTES; refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
|
|
3224
|
-
}
|
|
3225
|
-
const attribution = screenApproverAttribution(hostDecisionRaw.approver);
|
|
3226
|
-
if (attribution.defect !== undefined) {
|
|
3227
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries an approver attribution this engine refuses: ${attribution.defect}; refusing pre-CAS, the checkpoint stays pending`, { field: "approver" });
|
|
3228
|
-
}
|
|
3229
|
-
const reason = decide.reason;
|
|
3230
|
-
assertOutcomeText(reason, "reason");
|
|
3231
|
-
const approverCell = attribution.approver !== undefined ? { approver: attribution.approver } : {};
|
|
3232
|
-
plainPolicyOutcome = {
|
|
3233
|
-
gate: "policy_ask",
|
|
3234
|
-
boundCallId: decide.boundCallId,
|
|
3235
|
-
boundInputHash: decide.boundInputHash,
|
|
3236
|
-
decision,
|
|
3237
|
-
...(updatedInput !== undefined ? { updatedInput } : {}),
|
|
3238
|
-
...(reason !== undefined ? { reason } : {}),
|
|
3239
|
-
...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
|
|
3240
|
-
hostDecision: { decidedBy, ...approverCell },
|
|
3241
|
-
};
|
|
3242
|
-
if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
|
|
3243
|
-
throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
|
|
3244
|
-
}
|
|
3245
|
-
}
|
|
3246
|
-
if (plainPolicyOutcome !== undefined) {
|
|
3247
|
-
const boundTo = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.toolCallId : undefined;
|
|
3248
|
-
if (plainPolicyOutcome.boundCallId !== boundTo) {
|
|
3249
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${plainPolicyOutcome.boundCallId}" does not match the checkpoint's pending tool call "${boundTo ?? "(none)"}" — the decision-action binding (design/80 D-1) failed; refusing to apply a decision bound to a different action`, { field: "boundCallId" });
|
|
3250
|
-
}
|
|
3251
|
-
const boundHash = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.boundInputHash : undefined;
|
|
3252
|
-
if (boundHash !== undefined) {
|
|
3253
|
-
if (plainPolicyOutcome.boundInputHash !== boundHash) {
|
|
3254
|
-
throw new CheckpointError("checkpoint.invalid_outcome", "resume boundInputHash does not match the checkpoint's pending tool call input — the decision-action input binding (design/80 D-1 §2) failed; refusing to apply an approval bound to a different input (TOCTOU re-mint guard)", { field: "boundInputHash" });
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
|
-
else if (checkpointVersionOf(cp) >= BINDING_CHECKPOINT_VERSION) {
|
|
3258
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume of a binding-version (v${checkpointVersionOf(cp)}) checkpoint whose pending tool call has no persisted boundInputHash — refusing to resume an unbound binding-version checkpoint (corruption / downgrade guard)`);
|
|
3259
|
-
}
|
|
3260
|
-
if (cp.reopenReason === "env_failed") {
|
|
3261
|
-
if (cp.resolvedOutcome === undefined) {
|
|
3262
|
-
throw new CheckpointError("checkpoint.reopen_revote", "checkpoint was reopened after an env-restore failure (env_failed) but carries no persisted winner to replay — refusing to resume (inconsistent row, fail-closed)");
|
|
3263
|
-
}
|
|
3264
|
-
const incoming = winnerFromOutcome(plainPolicyOutcome);
|
|
3265
|
-
const persistedWinner = (() => {
|
|
3266
|
-
const w = cp.resolvedOutcome;
|
|
3267
|
-
if (w?.answer === undefined)
|
|
3268
|
-
return w;
|
|
3269
|
-
const reading = classifyQuestionOutcome(w.answer);
|
|
3270
|
-
return reading.shape === "answered" ? { ...w, answer: reading.answer } : w;
|
|
3271
|
-
})();
|
|
3272
|
-
if (incoming === undefined || !sameWinner(incoming, persistedWinner)) {
|
|
3273
|
-
throw new CheckpointError("checkpoint.reopen_revote", `this checkpoint was reopened after an env-restore failure (env_failed) — a re-resume is a system retry of the already-approved action, not a re-vote; the supplied decision must replay the persisted winner (boundCallId "${cp.resolvedOutcome.boundCallId}", decision "${cp.resolvedOutcome.decision}"), refusing a different decision`);
|
|
3274
|
-
}
|
|
3275
|
-
}
|
|
3276
|
-
const contentAskCallId = pendingContentAskCallId(cp);
|
|
3277
|
-
if (suppliedAnswer !== undefined && (contentAskCallId === undefined || plainPolicyOutcome.decision !== "allow")) {
|
|
3278
|
-
throw new CheckpointError("checkpoint.invalid_outcome", contentAskCallId === undefined
|
|
3279
|
-
? "resume carried a content-ask `answer` but the checkpoint's pending call is not the reserved question tool — an answer has no consumer on a side-effecting tool's approval; refusing rather than dropping it silently"
|
|
3280
|
-
: "resume carried a content-ask `answer` on a `deny` — a denial injects a refusal, never an answer; refusing rather than dropping it silently", { field: "answer" });
|
|
3281
|
-
}
|
|
3282
|
-
if (contentAskCallId !== undefined && plainPolicyOutcome.decision === "allow" && suppliedAnswer === undefined) {
|
|
3283
|
-
if (!isLiveQuestionFace(config.onQuestion ?? this.deps.onQuestion)) {
|
|
3284
|
-
throw new CheckpointError("checkpoint.invalid_outcome", "resume approved a content-ask (the reserved question tool) without an `answer`, and this resume has no live answering face — " +
|
|
3285
|
-
'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
|
|
3286
|
-
"re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
|
|
3287
|
-
}
|
|
3288
|
-
}
|
|
3289
|
-
}
|
|
3290
|
-
if (checkpointVersionOf(cp) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
|
|
3291
|
-
throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`, { reason: "version_newer" });
|
|
3292
|
-
}
|
|
3293
|
-
const preCasGateBit = cp.gate.kind === "irreversible_ask" ? cp.gate.realApproval : undefined;
|
|
3294
|
-
const preCasBitWellFormed = preCasGateBit !== undefined && typeof preCasGateBit === "object" && isAskOrigin(preCasGateBit.origin);
|
|
3295
|
-
const faceSaysGoverned = (cp.state.readFace?.realApproval) === true;
|
|
3296
|
-
const preCasBitViolation = checkpointVersionOf(cp) === REAL_APPROVAL_CHECKPOINT_VERSION
|
|
3297
|
-
? !preCasBitWellFormed
|
|
3298
|
-
: checkpointVersionOf(cp) < REAL_APPROVAL_CHECKPOINT_VERSION
|
|
3299
|
-
? preCasGateBit !== undefined
|
|
3300
|
-
: faceSaysGoverned
|
|
3301
|
-
? !preCasBitWellFormed
|
|
3302
|
-
: preCasGateBit !== undefined && !preCasBitWellFormed;
|
|
3303
|
-
if (preCasBitViolation) {
|
|
3304
|
-
throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION
|
|
3305
|
-
? `a v${checkpointVersionOf(cp)} checkpoint's realApproval gate bit must be well-formed (origin a member of the closed ask-origin set)${checkpointVersionOf(cp) === REAL_APPROVAL_CHECKPOINT_VERSION ? " and present on an irreversible_ask gate" : ""} — this row's is not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
|
|
3306
|
-
: `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? "real_approval_damaged" : "real_approval_forged" });
|
|
3307
|
-
}
|
|
3308
|
-
{
|
|
3309
|
-
const faceSectionRaw = cp.state.readFace;
|
|
3310
|
-
const faceCandidate = faceSectionRaw !== undefined && typeof faceSectionRaw === "object" && faceSectionRaw !== null
|
|
3311
|
-
? faceSectionRaw
|
|
3312
|
-
: undefined;
|
|
3313
|
-
const faceWellFormed = faceCandidate !== undefined &&
|
|
3314
|
-
(faceCandidate.face === "open" || faceCandidate.face === "roots") &&
|
|
3315
|
-
(faceCandidate.realApproval === undefined || faceCandidate.realApproval === true) &&
|
|
3316
|
-
(faceCandidate.denyEntries === undefined ||
|
|
3317
|
-
(Array.isArray(faceCandidate.denyEntries) &&
|
|
3318
|
-
faceCandidate.denyEntries.every((e) => persistedReadDenyEntryProblem(e) === null)));
|
|
3319
|
-
if (checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION ? !faceWellFormed : faceSectionRaw !== undefined) {
|
|
3320
|
-
throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION
|
|
3321
|
-
? `a v${checkpointVersionOf(cp)} checkpoint must carry a well-formed read-face section (state.readFace) — this row's is missing or malformed; refusing to resume a damaged read-face row (corruption / downgrade guard), the checkpoint stays pending`
|
|
3322
|
-
: `a v${checkpointVersionOf(cp)} checkpoint carries a read-face section no release of that version ever minted — refusing to honor a fabricated read-face state (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION ? "read_face_damaged" : "read_face_forged" });
|
|
3323
|
-
}
|
|
3324
|
-
}
|
|
3325
|
-
if (realApprovalOrgFact(preCasGateBit) !== undefined && this.deps.permissionRuleStore?.partitions.org !== true) {
|
|
3326
|
-
throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (org ${String(realApprovalOrgFact(preCasGateBit))}, routed as ${String(preCasGateBit?.origin)}) and this worker has no org adjudication wiring (its permission-rule store carries no org partition) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`, { reason: "governed_unwired" });
|
|
3327
|
-
}
|
|
3328
|
-
const retiredWalltimeTotal = cp.resourceLedger?.totalWalltimeSec;
|
|
3329
|
-
if (retiredWalltimeTotal !== undefined) {
|
|
3330
|
-
throw new CheckpointError("checkpoint.walltime_axis_retired", "this checkpoint's ledger carries a cross-slice WALL-CLOCK allocation (resourceLedger.totalWalltimeSec), an axis this engine version retired — " +
|
|
3331
|
-
"resuming here would run the leg with that ceiling unenforced. Rejected pre-CAS (the checkpoint stays pending): finish it on a worker of the " +
|
|
3332
|
-
"previous release, or start a fresh task bounded by resourceSuspend.totalTokens / totalBudgetUsd.");
|
|
3333
|
-
}
|
|
3334
|
-
if (resumeSignal?.aborted === true && cp.status === "pending") {
|
|
3335
|
-
throw new CheckpointError("checkpoint.resume_aborted", "the resume was handed an ALREADY-ABORTED signal — refusing to consume the approval on a leg that cannot run it (the checkpoint stays pending and is resumable with a live signal)");
|
|
3336
|
-
}
|
|
3337
|
-
if (cp.state.workspaceHandle !== undefined && this.deps.executionEnvFactory === undefined) {
|
|
3338
|
-
throw new CheckpointError("checkpoint.unsupported_version", "checkpoint has a remote workspaceHandle but no RunnerDeps.executionEnvFactory is wired to rebuild the env", { reason: "env_factory_missing" });
|
|
3339
|
-
}
|
|
3340
|
-
if (cp.state.workspaceHandle !== undefined && internals?.requestedCwd !== undefined) {
|
|
3341
|
-
throw new CheckpointError("checkpoint.cwd_conflicts_restore", `internals.requestedCwd ("${internals.requestedCwd}") cannot be combined with a checkpoint workspace restore — ` +
|
|
3342
|
-
"the restored workspace's own mount path is authoritative for the task root. Drop requestedCwd on resume legs " +
|
|
3343
|
-
"(the checkpoint stays pending and is resumable without it).");
|
|
3344
|
-
}
|
|
3345
|
-
if (cp.state.inheritedGate?.requiresParentConstraint === true) {
|
|
3346
|
-
const supplied = internals?.inheritedGate?.parentConstraints?.length ?? 0;
|
|
3347
|
-
if (supplied === 0) {
|
|
3348
|
-
throw new CheckpointError("resume.parent_constraint_missing", "this checkpoint's task ran under inherited parent-policy constraints (state.inheritedGate.requiresParentConstraint) " +
|
|
3349
|
-
"— the live policy/onAsk closures cannot be persisted, so the resume must re-supply them via " +
|
|
3350
|
-
"resumeStream(token, outcome, taskConfig, internals) with internals.inheritedGate.parentConstraints; " +
|
|
3351
|
-
"rejected pre-CAS (the checkpoint stays pending) rather than silently dropping the ancestors' gate");
|
|
3352
|
-
}
|
|
3353
|
-
const expected = cp.state.inheritedGate.parentConstraintCount;
|
|
3354
|
-
if (expected !== undefined && supplied !== expected) {
|
|
3355
|
-
throw new CheckpointError("resume.parent_constraint_mismatch", `re-supplied ${supplied} parent constraint(s) but the checkpoint recorded ${expected} — a partial/mismatched ` +
|
|
3356
|
-
"re-supply would run the resumed leg under a different ancestor chain than it suspended with; " +
|
|
3357
|
-
"rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
|
|
3358
|
-
}
|
|
3359
|
-
if (checkpointVersionOf(cp) >= F012_CHECKPOINT_VERSION &&
|
|
3360
|
-
(cp.state.inheritedGate.constraintChain === undefined || cp.state.inheritedGate.constraintDigest === undefined)) {
|
|
3361
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `a v${checkpointVersionOf(cp)} checkpoint that requires parent constraints must carry BOTH the frozen constraintChain and its constraintDigest (they are minted in one write with the version stamp) — this row carries neither or only one; refusing to fall back to the count-only contract on a damaged row (corruption / downgrade guard), the checkpoint stays pending`, { reason: "constraint_chain_missing" });
|
|
3362
|
-
}
|
|
3363
|
-
const expectedDigest = cp.state.inheritedGate.constraintDigest;
|
|
3364
|
-
if (expectedDigest !== undefined) {
|
|
3365
|
-
const persistedChain = cp.state.inheritedGate.constraintChain;
|
|
3366
|
-
if (persistedChain === undefined || constraintChainDigest(persistedChain) !== expectedDigest) {
|
|
3367
|
-
throw new CheckpointError("resume.parent_constraint_mismatch", "the checkpoint's OWN recorded constraint chain does not match its recorded digest " +
|
|
3368
|
-
"(corrupt / inconsistently-minted row) — the frozen deny data the resume would execute is not the data the " +
|
|
3369
|
-
"digest authenticates; rejected pre-CAS (the checkpoint stays pending)");
|
|
3370
|
-
}
|
|
3371
|
-
const suppliedDigest = constraintChainDigest((internals?.inheritedGate?.parentConstraints ?? []).map((pc, i) => constraintChainEntryOfSuppliedLayer(pc, persistedChain[i])));
|
|
3372
|
-
if (suppliedDigest !== expectedDigest) {
|
|
3373
|
-
throw new CheckpointError("resume.parent_constraint_mismatch", "the re-supplied parent-constraint chain's projection content does not match what the checkpoint recorded " +
|
|
3374
|
-
`(recorded ${expectedDigest}, re-supplied ${suppliedDigest}) — a same-shape chain with different frozen ` +
|
|
3375
|
-
"deny data would run the resumed leg under a different ancestor gate than it suspended with; " +
|
|
3376
|
-
"rejected pre-CAS (the checkpoint stays pending) — re-resume with the original chain's policies " +
|
|
3377
|
-
"(including, when the row recorded one, the ancestor's auto-mode arming recipe verbatim)");
|
|
3378
|
-
}
|
|
3379
|
-
}
|
|
3380
|
-
}
|
|
3381
|
-
const resumeInternals = internals?.inheritedGate?.parentConstraints !== undefined
|
|
3382
|
-
? {
|
|
3383
|
-
...internals,
|
|
3384
|
-
inheritedGate: {
|
|
3385
|
-
...internals.inheritedGate,
|
|
3386
|
-
parentConstraints: Object.freeze(internals.inheritedGate.parentConstraints.map((e) => ({ ...e }))),
|
|
3387
|
-
},
|
|
3388
|
-
}
|
|
3389
|
-
: internals;
|
|
3390
|
-
if (this.deps.lockedConfig?.keys?.includes("toolPolicy") === true && config.basePolicyForResumeEdit !== undefined) {
|
|
3391
|
-
throw new CheckpointError("checkpoint.invalid_outcome", "TaskSpec.basePolicyForResumeEdit is administratively locked by this deployment (locked key \"toolPolicy\") — a task-supplied " +
|
|
3392
|
-
"resume-edit policy is refused pre-CAS (the checkpoint stays pending); remove the field or change the deployment's lock configuration");
|
|
3393
|
-
}
|
|
3394
|
-
let recheckGovernanceWindow;
|
|
3395
|
-
{
|
|
3396
|
-
const rowPrincipal = cp.principal || undefined;
|
|
3397
|
-
const suppliedPrincipal = config.principal || undefined;
|
|
3398
|
-
if (rowPrincipal !== undefined && suppliedPrincipal !== undefined && suppliedPrincipal !== rowPrincipal) {
|
|
3399
|
-
throw new CheckpointError("resume.principal_mismatch", `the resume config carries principal ${JSON.stringify(suppliedPrincipal)} but this checkpoint was suspended under principal ${JSON.stringify(rowPrincipal)} — ` +
|
|
3400
|
-
"running the resumed leg under a different identity would move its usage-ledger bucket, scope derivation and attribution; " +
|
|
3401
|
-
"refused pre-CAS (the checkpoint stays pending): re-resume with the original principal, or omit the field to inherit the recorded one");
|
|
3402
|
-
}
|
|
3403
|
-
{
|
|
3404
|
-
const recordedPlacementRoot = placementValueOrAbsent(cp.state.placementRootSessionId);
|
|
3405
|
-
const suppliedPlacementRoot = placementValueOrAbsent(internals?.placementRoot);
|
|
3406
|
-
if (recordedPlacementRoot !== undefined && suppliedPlacementRoot !== undefined && suppliedPlacementRoot !== recordedPlacementRoot) {
|
|
3407
|
-
throw new CheckpointError("resume.placement_mismatch", `the resume supplied explicit placement root ${JSON.stringify(suppliedPlacementRoot)} but this checkpoint recorded placement root ${JSON.stringify(recordedPlacementRoot)} at suspend — ` +
|
|
3408
|
-
"resuming under a different placement fixed point would re-place the leg (and its descendants) on another target; " +
|
|
3409
|
-
"refused pre-CAS (the checkpoint stays pending): re-resume with the recorded root, or omit internals.placementRoot to inherit it");
|
|
3410
|
-
}
|
|
3411
|
-
}
|
|
3412
|
-
const owesDelivery = outcomeGate !== "resource_limit" ||
|
|
3413
|
-
readPendingSteerQueue(cp.state).length > 0 ||
|
|
3414
|
-
(cp.state.runningBackgroundTasks?.length ?? 0) > 0;
|
|
3415
|
-
const usageWindowStore = this.deps.usageWindowStore;
|
|
3416
|
-
if (owesDelivery && usageWindowStore !== undefined) {
|
|
3417
|
-
const preCasWindows = resolveUsageWindows(this.deps.usageWindows);
|
|
3418
|
-
if (preCasWindows !== undefined && preCasWindows.length > 0) {
|
|
3419
|
-
const ledgerKey = (suppliedPrincipal ?? rowPrincipal) || GLOBAL_USAGE_KEY;
|
|
3420
|
-
const assertWindowOpen = async () => {
|
|
3421
|
-
const wait = usageRetryAfterMs(await usageWindowStore.read(ledgerKey, preCasWindows, Date.now()), preCasWindows);
|
|
3422
|
-
if (wait !== undefined) {
|
|
3423
|
-
throw new CheckpointError("resume.usage_window_exhausted", `a deployment usage window for ledger key ${JSON.stringify(ledgerKey)} is exhausted, and this checkpoint still owes a delivery a re-mint cannot carry — ` +
|
|
3424
|
-
`refused pre-CAS (nothing consumed, nothing unpinned): the same token and the same decision are redeemable once the window frees, in ${String(wait)}ms`, { retryAfterMs: wait });
|
|
3425
|
-
}
|
|
3426
|
-
};
|
|
3427
|
-
recheckGovernanceWindow = assertWindowOpen;
|
|
3428
|
-
await assertWindowOpen();
|
|
3429
|
-
}
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
if (outcomeGate === "wake" && wakeMessage !== undefined) {
|
|
3433
|
-
const resumeScreenHooks = config.hooks ?? this.deps.hooks;
|
|
3434
|
-
const screen = resumeScreenHooks?.userPromptSubmit;
|
|
3435
|
-
if (screen !== undefined && resumeSignal?.aborted !== true) {
|
|
3436
|
-
const screenedMessage = wakeMessage;
|
|
3437
|
-
const identity = mintHookInvocationIdentity({
|
|
3438
|
-
sessionId: cp.sessionId,
|
|
3439
|
-
taskId: config.taskId ?? cp.sessionId,
|
|
3440
|
-
legKind: "resume",
|
|
3441
|
-
isDelegatedChild: effectiveDelegationFacts(internals, cp.state.isDelegatedChild).isDelegatedChild,
|
|
3442
|
-
...(internals?.insideFork === true ? { insideFork: true } : {}),
|
|
3443
|
-
...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
|
|
3444
|
-
...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
|
|
3445
|
-
});
|
|
3446
|
-
const screenTimeoutMs = resolveHookTimeoutMs(resumeScreenHooks?.timeoutMs, (badErr) => {
|
|
3447
|
-
try {
|
|
3448
|
-
this.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
|
|
3449
|
-
}
|
|
3450
|
-
catch {
|
|
3451
|
-
}
|
|
3452
|
-
}, resumeScreenHooks);
|
|
3453
|
-
let decision;
|
|
3454
|
-
try {
|
|
3455
|
-
const seat = await runHookSeat("userPromptSubmit", { timeoutMs: screenTimeoutMs, ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}), abortEnds: true }, (sig) => screen(screenedMessage.text, {
|
|
3456
|
-
identity,
|
|
3457
|
-
signal: sig,
|
|
3458
|
-
source: "resume_message",
|
|
3459
|
-
...(screenedMessage.inputId !== undefined ? { inputId: screenedMessage.inputId } : {}),
|
|
3460
|
-
...(screenedMessage.actor !== undefined ? { actor: snapshotActorAssertion(screenedMessage.actor) } : {}),
|
|
3461
|
-
}));
|
|
3462
|
-
if (seat.expired) {
|
|
3463
|
-
throw new CheckpointError("steering.blocked_by_hook", seat.cause === "timeout"
|
|
3464
|
-
? `the deployment's userPromptSubmit hook did not answer within its ${screenTimeoutMs}ms bound while screening this wake message; the resume was refused (fail-closed) and the checkpoint stays pending`
|
|
3465
|
-
: `the resume was cancelled while the deployment's userPromptSubmit hook was still screening this wake message; the checkpoint stays pending`);
|
|
3466
|
-
}
|
|
3467
|
-
decision = seat.value;
|
|
3468
|
-
}
|
|
3469
|
-
catch (hookErr) {
|
|
3470
|
-
if (hookErr instanceof CheckpointError)
|
|
3471
|
-
throw hookErr;
|
|
3472
|
-
const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
|
|
3473
|
-
try {
|
|
3474
|
-
this.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
|
|
3475
|
-
}
|
|
3476
|
-
catch {
|
|
3477
|
-
}
|
|
3478
|
-
throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook crashed while screening this wake message (${inlineUntrusted(err.message)}); the resume was refused (fail-closed) and the checkpoint stays pending`);
|
|
3479
|
-
}
|
|
3480
|
-
if (decision?.block !== undefined && decision.block !== "") {
|
|
3481
|
-
throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook blocked this wake message: ${inlineUntrusted(decision.block)}`);
|
|
3482
|
-
}
|
|
3483
|
-
if (decision?.additionalContext !== undefined && decision.additionalContext !== "") {
|
|
3484
|
-
wakeHookContext = decision.additionalContext;
|
|
3485
|
-
}
|
|
3486
|
-
if (recheckGovernanceWindow !== undefined)
|
|
3487
|
-
await recheckGovernanceWindow();
|
|
3488
|
-
}
|
|
3489
|
-
}
|
|
3490
|
-
if (plainPolicyOutcome !== undefined &&
|
|
3491
|
-
plainPolicyOutcome.decision === "allow" &&
|
|
3492
|
-
plainPolicyOutcome.updatedInput !== undefined &&
|
|
3493
|
-
cp.pendingAction.kind === "tool_approval" &&
|
|
3494
|
-
cp.pendingAction.toolName !== ASK_USER_QUESTION_TOOL_NAME) {
|
|
3495
|
-
const editedArgs = plainPolicyOutcome.updatedInput;
|
|
3496
|
-
const editBudget = {
|
|
3497
|
-
resourceRemainingMicroUsd: remainingBudgetMicroUsd(cp.resourceLedger),
|
|
3498
|
-
resourceSpentMicroUsd: cp.resourceLedger?.spentMicroUsd ?? 0,
|
|
3499
|
-
suspendCount: cp.suspendCount ?? 0,
|
|
3500
|
-
};
|
|
3501
|
-
const editedReq = {
|
|
3502
|
-
toolName: cp.pendingAction.toolName,
|
|
3503
|
-
args: editedArgs,
|
|
3504
|
-
toolCallId: cp.pendingAction.toolCallId,
|
|
3505
|
-
budget: editBudget,
|
|
3506
|
-
...callFaceSeat(parkedCallFaceOf(cp.pendingAction)),
|
|
3507
|
-
...(cp.state.handsCwd !== undefined ? { cwd: cp.state.handsCwd } : {}),
|
|
3508
|
-
};
|
|
3509
|
-
const editSignal = config.signal;
|
|
3510
|
-
const raceEditAbort = async (p) => {
|
|
3511
|
-
if (editSignal === undefined)
|
|
3512
|
-
return p;
|
|
3513
|
-
if (editSignal.aborted) {
|
|
3514
|
-
throw new CheckpointError("checkpoint.resume_aborted", "the resume was aborted while re-adjudicating the approver's edit — the checkpoint stays pending and is resumable with a live signal");
|
|
3515
|
-
}
|
|
3516
|
-
return new Promise((resolve, reject) => {
|
|
3517
|
-
const onAbort = () => reject(new CheckpointError("checkpoint.resume_aborted", "the resume was aborted while re-adjudicating the approver's edit — the checkpoint stays pending and is resumable with a live signal"));
|
|
3518
|
-
editSignal.addEventListener("abort", onAbort, { once: true });
|
|
3519
|
-
p.then((v) => { editSignal.removeEventListener("abort", onAbort); resolve(v); }, (e) => { editSignal.removeEventListener("abort", onAbort); reject(e); });
|
|
3520
|
-
});
|
|
3521
|
-
};
|
|
3522
|
-
const ig = cp.state.inheritedGate;
|
|
3523
|
-
if (ig?.requiresParentConstraint === true) {
|
|
3524
|
-
const chain = ig.constraintChain;
|
|
3525
|
-
const unprojectable = chain === undefined
|
|
3526
|
-
? "the checkpoint predates the frozen projection chain (no constraintChain recorded)"
|
|
3527
|
-
: chain.some((e) => "opaque" in e)
|
|
3528
|
-
? "the inherited chain carries an OPAQUE layer (no projection exported)"
|
|
3529
|
-
: chain.some((e) => !("opaque" in e) && e.requiresLiveRemainder)
|
|
3530
|
-
? "an inherited chain layer declares a live remainder beyond its projection"
|
|
3531
|
-
: undefined;
|
|
3532
|
-
if (unprojectable !== undefined) {
|
|
3533
|
-
throw new CheckpointError("resume.constraint_unprojectable", `the approver EDITED this call's input, but the inherited ancestor chain cannot re-adjudicate the edit statically: ${unprojectable}. ` +
|
|
3534
|
-
"Refusing the edited resume pre-CAS (the checkpoint stays pending and remains decidable) — approve the shown input as-is, deny it, " +
|
|
3535
|
-
"or re-issue the edited action as a fresh call so the full live chain adjudicates it");
|
|
3536
|
-
}
|
|
3537
|
-
for (const entry of chain) {
|
|
3538
|
-
const denied = checkToolPolicyProjection(entry, editedReq);
|
|
3539
|
-
if (denied !== undefined) {
|
|
3540
|
-
throw new CheckpointError("resume.constraint_rejected", `the approver EDITED this call's input and a FROZEN inherited ancestor constraint denies the edited args${denied.message ? `: ${denied.message}` : ""}. ` +
|
|
3541
|
-
"Refused pre-CAS (fresh-redecision semantics: the checkpoint stays pending and the same token remains decidable — approve as shown, deny, or edit differently)");
|
|
3542
|
-
}
|
|
3543
|
-
}
|
|
3544
|
-
}
|
|
3545
|
-
const toolPolicyLocked = this.deps.lockedConfig?.keys?.includes("toolPolicy") === true;
|
|
3546
|
-
const editBasePolicy = toolPolicyLocked
|
|
3547
|
-
? (this.deps.basePolicyForResumeEdit ?? this.deps.toolPolicy)
|
|
3548
|
-
: (config.basePolicyForResumeEdit ?? this.deps.basePolicyForResumeEdit ?? config.toolPolicy ?? this.deps.toolPolicy);
|
|
3549
|
-
if (editBasePolicy !== undefined) {
|
|
3550
|
-
let rechecked;
|
|
3551
|
-
try {
|
|
3552
|
-
rechecked = refuseOutOfContractDecision(await raceEditAbort(Promise.resolve(editBasePolicy.check(editedReq, editSignal))));
|
|
3553
|
-
}
|
|
3554
|
-
catch (err) {
|
|
3555
|
-
if (err instanceof CheckpointError)
|
|
3556
|
-
throw err;
|
|
3557
|
-
rechecked = { action: "deny", message: `the resume-edit policy re-check errored: ${err instanceof Error ? err.message : String(err)}` };
|
|
3558
|
-
}
|
|
3559
|
-
if (rechecked.action === "deny") {
|
|
3560
|
-
throw new CheckpointError("resume.constraint_rejected", `the approver EDITED this call's input and the deployment's resume-edit policy denies the edited args${rechecked.message ? `: ${rechecked.message}` : ""}. ` +
|
|
3561
|
-
"Refused pre-CAS (the checkpoint stays pending and remains decidable)");
|
|
3562
|
-
}
|
|
3563
|
-
if (rechecked.updatedInput !== undefined && !deepJsonEqual(rechecked.updatedInput, editedArgs)) {
|
|
3564
|
-
throw new CheckpointError("resume.constraint_rejected", `the deployment's resume-edit policy REWROTE the approver's edited args to a different value than was approved — the durable resume cannot execute either the un-adjudicated edit or an unapproved rewrite; ` +
|
|
3565
|
-
`refused pre-CAS (the checkpoint stays pending); re-submit the edited action so the rewrite is adjudicated and approved synchronously`);
|
|
3566
|
-
}
|
|
3567
|
-
}
|
|
3568
|
-
const resumeHooks = config.hooks ?? this.deps.hooks;
|
|
3569
|
-
if (resumeHooks?.preToolUse !== undefined && resumeHooks.preToolUseObservational === true) {
|
|
3570
|
-
try {
|
|
3571
|
-
await raceEditAbort(Promise.resolve(resumeHooks.preToolUse(cp.pendingAction.toolName, cloneObserverInput(editedArgs), {
|
|
3572
|
-
toolCallId: cp.pendingAction.toolCallId,
|
|
3573
|
-
toolName: cp.pendingAction.toolName,
|
|
3574
|
-
})));
|
|
3575
|
-
}
|
|
3576
|
-
catch (err) {
|
|
3577
|
-
if (err instanceof CheckpointError)
|
|
3578
|
-
throw err;
|
|
3579
|
-
throw new CheckpointError("resume.constraint_rejected", `the declared-observational PreToolUse face threw while observing the approver's edited args (${err instanceof Error ? err.message : String(err)}) — ` +
|
|
3580
|
-
"refused fail-closed pre-CAS (the checkpoint stays pending and remains decidable)");
|
|
3581
|
-
}
|
|
3582
|
-
}
|
|
3583
|
-
}
|
|
3584
|
-
if (plainPolicyOutcome !== undefined) {
|
|
3585
|
-
const parkedOrigin = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.origin : undefined;
|
|
3586
|
-
if (!isAskOrigin(parkedOrigin)) {
|
|
3587
|
-
throw new CheckpointError("checkpoint.unsupported_version", "this checkpoint's pending approval carries no origin word (a row minted before the ask's origin was persisted on the park) — its settlement cannot be recorded; the checkpoint stays pending", { reason: "origin_missing" });
|
|
3588
|
-
}
|
|
3589
|
-
const facts = plainPolicyOutcome.hostDecision;
|
|
3590
|
-
const approverCell = facts.approver !== undefined ? { approver: facts.approver } : {};
|
|
3591
|
-
const settlement = facts.decidedBy === "sla_timeout"
|
|
3592
|
-
? { kind: "park_sla_expired", who: { party: "host", ...approverCell }, when: Date.now() }
|
|
3593
|
-
: { kind: plainPolicyOutcome.decision === "allow" ? "human_allowed" : "human_refused", who: { party: "person", ...approverCell }, when: Date.now(), ...(plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason !== undefined ? { note: plainPolicyOutcome.reason } : {}) };
|
|
3594
|
-
plainPolicyOutcome = { ...plainPolicyOutcome, gateOutcome: mintGateOutcome({ ...(plainPolicyOutcome.decision === "deny" ? { deniedBy: "ask_resolution" } : {}), settled: { settlement, origin: parkedOrigin } }) };
|
|
3595
|
-
}
|
|
3596
|
-
const outcomeForStore = plainPolicyOutcome ?? plainReviewOutcome ?? plainParkOutcome;
|
|
3597
|
-
if (outcomeForStore === undefined) {
|
|
3598
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") matched the checkpoint gate but no lane captured it — refusing pre-CAS rather than passing the caller's live object to the store and the resumed run`);
|
|
3599
|
-
}
|
|
3600
|
-
if (this.deps.resumePreflight !== undefined) {
|
|
3601
|
-
const preflight = this.deps.resumePreflight;
|
|
3602
|
-
const rawDeadline = this.deps.resumePreflightTimeoutMs;
|
|
3603
|
-
let preflightTimeoutMs = RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS;
|
|
3604
|
-
if (rawDeadline !== undefined) {
|
|
3605
|
-
if (typeof rawDeadline === "number" && Number.isFinite(rawDeadline) && rawDeadline > 0 && rawDeadline <= MAX_HOOK_TIMEOUT_MS) {
|
|
3606
|
-
preflightTimeoutMs = rawDeadline;
|
|
3607
|
-
}
|
|
3608
|
-
else {
|
|
3609
|
-
try {
|
|
3610
|
-
this.deps.onError?.(new Error(`RunnerDeps.resumePreflightTimeoutMs is not a positive finite number within the seat ceiling of ${MAX_HOOK_TIMEOUT_MS}ms (got ${describeSuppliedValue(rawDeadline)}) — the resume preflight falls back to its own ${RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS}ms default`), { phase: "config", sessionId: cp.sessionId });
|
|
3611
|
-
}
|
|
3612
|
-
catch {
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
const info = {
|
|
3617
|
-
token,
|
|
3618
|
-
sessionId: cp.sessionId,
|
|
3619
|
-
...(cp.state.placementRootSessionId !== undefined ? { placementRootSessionId: cp.state.placementRootSessionId } : {}),
|
|
3620
|
-
...(cp.principal ? { principal: cp.principal } : {}),
|
|
3621
|
-
...(cp.state.workspaceHandle !== undefined ? { workspaceHandle: structuredClone(cp.state.workspaceHandle) } : {}),
|
|
3622
|
-
gateKind: cp.gate.kind,
|
|
3623
|
-
};
|
|
3624
|
-
const refusalTail = "; the checkpoint stays pending and the same token is redeemable once the obstacle clears";
|
|
3625
|
-
let verdict;
|
|
3626
|
-
try {
|
|
3627
|
-
const seat = await runHookSeat("resumePreflight", {
|
|
3628
|
-
timeoutMs: preflightTimeoutMs,
|
|
3629
|
-
...(resumeSignal !== undefined ? { signal: resumeSignal } : {}),
|
|
3630
|
-
abortEnds: true,
|
|
3631
|
-
onBadTimeout: (badErr) => {
|
|
3632
|
-
try {
|
|
3633
|
-
this.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
|
|
3634
|
-
}
|
|
3635
|
-
catch {
|
|
3636
|
-
}
|
|
3637
|
-
},
|
|
3638
|
-
owner: this.deps,
|
|
3639
|
-
}, (sig) => preflight(info, sig));
|
|
3640
|
-
if (seat.expired) {
|
|
3641
|
-
throw new CheckpointError("resume.preflight_rejected", seat.cause === "timeout"
|
|
3642
|
-
? `the deployment's resumePreflight did not answer within its ${preflightTimeoutMs}ms deadline while screening this resume — refused fail-closed (retry-later arm)${refusalTail}`
|
|
3643
|
-
: `the resume was cancelled while the deployment's resumePreflight was still screening it — refused (retry-later arm)${refusalTail}`);
|
|
3644
|
-
}
|
|
3645
|
-
verdict = seat.value;
|
|
3646
|
-
}
|
|
3647
|
-
catch (preflightErr) {
|
|
3648
|
-
if (preflightErr instanceof CheckpointError)
|
|
3649
|
-
throw preflightErr;
|
|
3650
|
-
const err = preflightErr instanceof Error ? preflightErr : new Error(String(preflightErr));
|
|
3651
|
-
try {
|
|
3652
|
-
this.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
|
|
3653
|
-
}
|
|
3654
|
-
catch {
|
|
3655
|
-
}
|
|
3656
|
-
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight crashed while screening this resume (${inlineUntrusted(err.message)}) — refused fail-closed (retry-later arm)${refusalTail}`);
|
|
3657
|
-
}
|
|
3658
|
-
const verdictOk = verdict !== null && typeof verdict === "object" ? verdict.ok : undefined;
|
|
3659
|
-
if (verdictOk !== true) {
|
|
3660
|
-
const bag = verdict !== null && typeof verdict === "object" ? verdict : undefined;
|
|
3661
|
-
const disposition = bag?.disposition;
|
|
3662
|
-
const suppliedMessage = bag?.message;
|
|
3663
|
-
const retryAfterMs = bag?.retryAfterMs;
|
|
3664
|
-
const readableMessage = typeof suppliedMessage === "string" && suppliedMessage !== "" ? suppliedMessage : undefined;
|
|
3665
|
-
const said = readableMessage !== undefined ? `: ${inlineUntrusted(readableMessage)}` : " (no readable verdict — fail-closed)";
|
|
3666
|
-
if (disposition === "terminal" && readableMessage !== undefined) {
|
|
3667
|
-
this.locallyClaimedTokens.add(token);
|
|
3668
|
-
let settled;
|
|
3669
|
-
try {
|
|
3670
|
-
settled = await store.expire(token, cp.scope);
|
|
3671
|
-
}
|
|
3672
|
-
catch (expireErr) {
|
|
3673
|
-
this.locallyClaimedTokens.delete(token);
|
|
3674
|
-
throw expireErr;
|
|
3675
|
-
}
|
|
3676
|
-
if (!settled) {
|
|
3677
|
-
this.locallyClaimedTokens.delete(token);
|
|
3678
|
-
const live = await store.get(token);
|
|
3679
|
-
if (live?.status === "pending") {
|
|
3680
|
-
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said}, but the row moved under this refusal (a concurrent resolve/reopen cycle) — nothing was settled here; the row is pending again, re-resume against the current state (the preflight screens every attempt)`);
|
|
3681
|
-
}
|
|
3682
|
-
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said} — a concurrent actor had already settled the row (${describeSuppliedValue(live?.status ?? "consumed")}); nothing was settled by this refusal`);
|
|
3683
|
-
}
|
|
3684
|
-
this.parentConstraintRegistry.delete(token);
|
|
3685
|
-
const terminalReap = this.suspendedEnvReaps.get(token);
|
|
3686
|
-
this.suspendedEnvReaps.delete(token);
|
|
3687
|
-
this.locallyClaimedTokens.delete(token);
|
|
3688
|
-
if (terminalReap?.env !== undefined && hasDestroy(terminalReap.env)) {
|
|
3689
|
-
const terminalEnv = terminalReap.env;
|
|
3690
|
-
await settleTeardownLeg(() => terminalEnv.destroy(), "terminal-preflight env destroy", (envErr) => {
|
|
3691
|
-
try {
|
|
3692
|
-
this.deps.onError?.(envErr, { phase: "config", sessionId: terminalReap.sessionId });
|
|
3693
|
-
}
|
|
3694
|
-
catch {
|
|
3695
|
-
}
|
|
3696
|
-
});
|
|
3697
|
-
}
|
|
3698
|
-
try {
|
|
3699
|
-
this.deps.onError?.(new Error(`resumePreflight ruled this row PERMANENTLY blocked${said} — the row was settled terminally (expired) by this refusal's single-shot CAS (token no longer redeemable)`), { phase: "hook", sessionId: cp.sessionId });
|
|
3700
|
-
}
|
|
3701
|
-
catch {
|
|
3702
|
-
}
|
|
3703
|
-
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said} — the row was settled terminally (expired by this refusal's single-shot CAS); the token is not redeemable`);
|
|
3704
|
-
}
|
|
3705
|
-
const waitHint = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : undefined;
|
|
3706
|
-
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight refused this resume${said}${refusalTail}${waitHint !== undefined ? ` (deployment wait hint: ${String(waitHint)}ms)` : ""}`, waitHint !== undefined ? { retryAfterMs: waitHint } : undefined);
|
|
3707
|
-
}
|
|
3708
|
-
if (recheckGovernanceWindow !== undefined)
|
|
3709
|
-
await recheckGovernanceWindow();
|
|
3710
|
-
}
|
|
3711
|
-
this.locallyClaimedTokens.add(token);
|
|
3712
|
-
let won;
|
|
3713
|
-
let claimLossStatus;
|
|
3714
|
-
if (store.claimTerminal !== undefined) {
|
|
3715
|
-
const claim = await store.claimTerminal(token, cp.scope, { kind: "resolve", outcome: outcomeForStore, expect: { rev: cp.rev ?? 0 } });
|
|
3716
|
-
won = claim.claimed;
|
|
3717
|
-
if (!claim.claimed)
|
|
3718
|
-
claimLossStatus = claim.current.status;
|
|
3719
|
-
}
|
|
3720
|
-
else {
|
|
3721
|
-
won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
|
|
3722
|
-
}
|
|
3723
|
-
if (!won)
|
|
3724
|
-
this.locallyClaimedTokens.delete(token);
|
|
3725
|
-
if (!won) {
|
|
3726
|
-
const stillPending = claimLossStatus !== undefined ? claimLossStatus === "pending" : (await store.get(token))?.status === "pending";
|
|
3727
|
-
if (stillPending) {
|
|
3728
|
-
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");
|
|
3729
|
-
}
|
|
3730
|
-
this.parentConstraintRegistry.delete(token);
|
|
3731
|
-
throw new CheckpointError("checkpoint.already_resolved", "checkpoint already resolved or expired — not re-executed (idempotent)");
|
|
3732
|
-
}
|
|
3025
|
+
return await resumeAdmission({
|
|
3026
|
+
token,
|
|
3027
|
+
outcome,
|
|
3028
|
+
taskConfig,
|
|
3029
|
+
internals,
|
|
3030
|
+
runner: this.depsSeat,
|
|
3031
|
+
parentConstraintRegistry: this.parentConstraintRegistry,
|
|
3032
|
+
next: (admitted) => this.resumeLadder(token, outcome, admitted),
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
async resumeLadder(token, outcome, admitted) {
|
|
3036
|
+
const { config, internals, store, cp, outcomeGate, resumeSignal, wakeMessage } = admitted;
|
|
3037
|
+
const { plainReviewOutcome, plainParkOutcome } = resumeReviewOutcome({ cp, outcome, outcomeGate, wakeMessage });
|
|
3038
|
+
const { plainPolicyOutcome: capturedPolicyOutcome, redeemedAnswer } = resumePolicyOutcome({ cp, outcome, config, runner: this.depsSeat });
|
|
3039
|
+
const { resumeInternals } = resumeCheckpointScreen({ cp, resumeSignal, internals, runner: this.depsSeat });
|
|
3040
|
+
const { recheckGovernanceWindow, windowEntrance } = resumeInternalsAndConfig({ cp, config, internals, outcomeGate, runner: this.depsSeat });
|
|
3041
|
+
if (windowEntrance !== undefined)
|
|
3042
|
+
await windowEntrance;
|
|
3043
|
+
return await resumeApply({
|
|
3044
|
+
cp, config, internals, outcomeGate, wakeMessage, resumeSignal, recheckGovernanceWindow,
|
|
3045
|
+
plainPolicyOutcome: capturedPolicyOutcome, plainReviewOutcome, plainParkOutcome,
|
|
3046
|
+
runner: this.depsSeat, effectiveDelegationFacts,
|
|
3047
|
+
next: ({ wakeHookContext, plainPolicyOutcome, outcomeForStore }) => resumePreflight({
|
|
3048
|
+
token, cp, resumeSignal, recheckGovernanceWindow, store, runner: this.depsSeat,
|
|
3049
|
+
locallyClaimedTokens: this.locallyClaimedTokens, parentConstraintRegistry: this.parentConstraintRegistry, suspendedEnvReaps: this.suspendedEnvReaps,
|
|
3050
|
+
next: () => resumeClaim({
|
|
3051
|
+
token, cp, store, outcomeForStore,
|
|
3052
|
+
locallyClaimedTokens: this.locallyClaimedTokens, parentConstraintRegistry: this.parentConstraintRegistry,
|
|
3053
|
+
next: () => this.resumeHandoff({ token, cp, store, config, internals, resumeInternals, plainPolicyOutcome, redeemedAnswer, outcomeForStore, wakeMessage, wakeHookContext, resumeSignal }),
|
|
3054
|
+
}),
|
|
3055
|
+
}),
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
async resumeHandoff(claimed) {
|
|
3059
|
+
const { token, cp, store, config, internals, resumeInternals, plainPolicyOutcome, redeemedAnswer, outcomeForStore, wakeMessage, wakeHookContext, resumeSignal } = claimed;
|
|
3733
3060
|
let consumeFlipDone = false;
|
|
3734
3061
|
if (internals?.afterCheckpointResolve !== undefined) {
|
|
3735
3062
|
await internals.afterCheckpointResolve();
|
|
@@ -3828,7 +3155,7 @@ export class Runner {
|
|
|
3828
3155
|
return l !== undefined && l !== name ? { label: l } : {};
|
|
3829
3156
|
})();
|
|
3830
3157
|
emit({ type: "tool_start", toolCallId: id, toolName: name, ...displayLabel, args: deferredArgs.get(id) ?? {} });
|
|
3831
|
-
emit({ type: "tool_end", toolCallId: id, toolName: name, ...displayLabel, isError: true, ...toolEndBodyFrom({ content: formatHookFeedback(DEFERRED_REISSUE, prepared.reminderMark) }, true) });
|
|
3158
|
+
emit({ type: "tool_end", toolCallId: id, toolName: name, ...displayLabel, isError: true, ...toolEndBodyFrom({ content: formatHookFeedback(DEFERRED_REISSUE, prepared.reminderMark) }, true, prepared.structuredProjector) });
|
|
3832
3159
|
const eid = await prepared.session.appendMessage(toolResultMsg(id, name, formatHookFeedback(DEFERRED_REISSUE, prepared.reminderMark), true));
|
|
3833
3160
|
emitCommitted(eid, "toolResult", id);
|
|
3834
3161
|
}
|
|
@@ -3847,7 +3174,7 @@ export class Runner {
|
|
|
3847
3174
|
const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
|
|
3848
3175
|
emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
|
|
3849
3176
|
const settleEnd = async (isError, result, gate = decidedGate) => {
|
|
3850
|
-
emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, gate) });
|
|
3177
|
+
emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, prepared.structuredProjector, gate) });
|
|
3851
3178
|
const word = await resume.recordExecutionOutcome(gate);
|
|
3852
3179
|
settleExecutionRecord(this.deps.onNotice, word, { toolName: pendingAction.toolName, gate, sessionId: prepared.sessionId, runId: prepared.runId, scope: resume.cp.scope, ...(resume.cp.checkpointId !== undefined ? { checkpointId: resume.cp.checkpointId } : {}) });
|
|
3853
3180
|
};
|
|
@@ -3950,6 +3277,30 @@ export class Runner {
|
|
|
3950
3277
|
return;
|
|
3951
3278
|
}
|
|
3952
3279
|
}
|
|
3280
|
+
if (prepared.persistedRuleLane !== undefined) {
|
|
3281
|
+
const unreadable = { unreadable: true };
|
|
3282
|
+
const read = await settleOrgVerdictWithin(Promise.resolve()
|
|
3283
|
+
.then(() => prepared.persistedRuleLane.adjudicate({
|
|
3284
|
+
toolName: pendingAction.toolName,
|
|
3285
|
+
args: resolvedArgs,
|
|
3286
|
+
toolCallId: pendingAction.toolCallId,
|
|
3287
|
+
...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
|
|
3288
|
+
budget: beltBudget,
|
|
3289
|
+
...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
|
|
3290
|
+
}))
|
|
3291
|
+
.then((answer) => normalizePersistedRuleHit(answer)), unreadable, { signal: prepared.abortController.signal, timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
3292
|
+
const tightened = applyPersistedTightening({ action: "allow" }, read);
|
|
3293
|
+
if (tightened.tightened === "deny" || tightened.tightened === "unreadable") {
|
|
3294
|
+
const why = tightened.tightened === "deny"
|
|
3295
|
+
? `a standing deny rule of yours (${disclosedRuleSet(read.hit?.rules ?? [])}) now refuses it`
|
|
3296
|
+
: "your persisted permission rules could not be read on this leg";
|
|
3297
|
+
const ruleDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${why}. This approval is spent — the call has to be re-issued and approved again.`, prepared.reminderMark);
|
|
3298
|
+
await settleEnd(true, { content: ruleDenial }, vetoed("persisted_rule"));
|
|
3299
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, ruleDenial, true));
|
|
3300
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3953
3304
|
const tool = prepared.tools.find((t) => t.name === pendingAction.toolName);
|
|
3954
3305
|
if (!tool) {
|
|
3955
3306
|
const e = new Error(`the approved tool "${pendingAction.toolName}" is no longer available on resume`);
|