@sema-agent/core 5.61.0 → 5.62.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 +67 -0
- package/dist/brain/open-responses.js +8 -3
- package/dist/brain/openai.js +4 -4
- package/dist/brain/stream-engine.d.ts +13 -2
- package/dist/brain/stream-engine.js +3 -3
- package/dist/core/auto-mode-prompt-assets.js +1 -1
- package/dist/core/checkpoint-store.d.ts +36 -4
- package/dist/core/checkpoint-store.js +1 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +83 -4
- package/dist/core/hooks.js +3 -3
- package/dist/core/park-selfcheck.js +2 -0
- package/dist/core/pricing.d.ts +24 -0
- package/dist/core/pricing.js +18 -0
- package/dist/core/runner/prepare-config-doors.d.ts +34 -0
- package/dist/core/runner/prepare-config-doors.js +55 -0
- package/dist/core/runner/prepare-task.d.ts +46 -7
- package/dist/core/runner/prepare-task.js +77 -42
- package/dist/core/runner/runtask.d.ts +7 -0
- package/dist/core/runner/runtask.js +198 -13
- package/dist/core/runner/turn-attachments.d.ts +137 -5
- package/dist/core/runner/turn-attachments.js +25 -2
- package/dist/core/store-contracts/checkpoint-store-contract.js +19 -0
- package/dist/core/tool-errors.d.ts +2 -1
- package/dist/core/tool-policy.d.ts +27 -0
- package/dist/core/types.d.ts +138 -12
- package/dist/core/untrusted-text.d.ts +5 -4
- package/dist/core/untrusted-text.js +8 -0
- package/dist/core/usage-window-store.d.ts +109 -8
- package/dist/core/usage-window-store.js +79 -12
- package/dist/orchestration/run-workflow-tool.d.ts +2 -2
- package/dist/orchestration/workflow.d.ts +2 -2
- package/dist/prompt-assembly/event-registry.js +2 -0
- package/dist/server/http.d.ts +1 -1
- package/dist/stores/file/usage-window-store.d.ts +1 -1
- package/dist/stores/file/usage-window-store.js +27 -6
- package/dist/tools/loop-tick.js +1 -1
- package/dist/tools/scheduler-tools.js +9 -1
- package/package.json +1 -1
|
@@ -95,7 +95,7 @@ import { BINDING_CHECKPOINT_VERSION, mintCheckpointId, mintCheckpointToken, ORG_
|
|
|
95
95
|
import { boundInputHashOf } from "../canonical-json.js";
|
|
96
96
|
import { countElicitOptIns, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam, resolveSubagentTranscriptTier } from "../wiring-manifest.js";
|
|
97
97
|
import { durableParkGapFor } from "../park-selfcheck.js";
|
|
98
|
-
import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "../usage-window-store.js";
|
|
98
|
+
import { GLOBAL_USAGE_KEY, usageRetryAfterMs, windowsGovernCost } from "../usage-window-store.js";
|
|
99
99
|
import { deliverEngineNotice } from "../types.js";
|
|
100
100
|
let announcedMaterializeEnvBySink = new WeakMap();
|
|
101
101
|
const announcedMaterializeEnvConsole = new Set();
|
|
@@ -228,6 +228,61 @@ const ULTRA_REASONING_TIERS = new Set(["xhigh", "max"]);
|
|
|
228
228
|
const DEFAULT_RESOURCE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
229
229
|
export const ENV_LIFETIME_SUSPEND_MARGIN_MS = 60_000;
|
|
230
230
|
export const USAGE_WINDOW_REAP_MARGIN_MS = 60 * 60 * 1000;
|
|
231
|
+
function buildUsageGovernance(windows, deps, principal, sessionId) {
|
|
232
|
+
if (windows === undefined || windows.length === 0)
|
|
233
|
+
return undefined;
|
|
234
|
+
const store = deps.usageWindowStore;
|
|
235
|
+
if (store === undefined) {
|
|
236
|
+
deps.onError?.(new Error("RunnerDeps.usageWindows is set but INACTIVE: no `usageWindowStore` is wired, so cross-task usage cannot be counted and no window can be enforced. Wire InMemoryUsageWindowStore (process-local) or FileUsageWindowStore (restart-surviving)."), { phase: "config", sessionId });
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
const key = principal || GLOBAL_USAGE_KEY;
|
|
240
|
+
const governsCost = windowsGovernCost(windows);
|
|
241
|
+
let chargedTokens = 0;
|
|
242
|
+
let chargedCostMicroUsd = 0;
|
|
243
|
+
let announcedCostGap = false;
|
|
244
|
+
let recordedUnpricedGap = false;
|
|
245
|
+
return {
|
|
246
|
+
key,
|
|
247
|
+
governsCost,
|
|
248
|
+
async check(now) {
|
|
249
|
+
const readings = await store.read(key, windows, now);
|
|
250
|
+
if (!announcedCostGap && readings.some((r) => r.costUnknown === true)) {
|
|
251
|
+
announcedCostGap = true;
|
|
252
|
+
deps.onError?.(new Error(`the usage window for ledger key ${JSON.stringify(key)} holds spend that nothing could price, so its maxCostUsd ceiling is being evaluated against a LOWER BOUND until that charge ages out of the window. Price every model this deployment can reach (including degrade targets and the compaction model).`), { phase: "config", sessionId });
|
|
253
|
+
}
|
|
254
|
+
return usageRetryAfterMs(readings, windows);
|
|
255
|
+
},
|
|
256
|
+
async commit(cumulativeTokens, cumulativeCostMicroUsd, now) {
|
|
257
|
+
if (!governsCost) {
|
|
258
|
+
const delta = cumulativeTokens - chargedTokens;
|
|
259
|
+
if (delta <= 0)
|
|
260
|
+
return;
|
|
261
|
+
await store.charge(key, delta, now, windows);
|
|
262
|
+
chargedTokens = cumulativeTokens;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (cumulativeCostMicroUsd === undefined) {
|
|
266
|
+
const tokenDelta = cumulativeTokens - chargedTokens;
|
|
267
|
+
if (tokenDelta > 0 || !recordedUnpricedGap) {
|
|
268
|
+
await store.charge(key, Math.max(0, tokenDelta), now, windows, null);
|
|
269
|
+
chargedTokens = Math.max(chargedTokens, cumulativeTokens);
|
|
270
|
+
recordedUnpricedGap = true;
|
|
271
|
+
}
|
|
272
|
+
const e = new Error("a deployment usage window declares maxCostUsd, but this run's spend has NO cost figure (RB-368 unpriced: a model served without a RunnerDeps.pricing entry or a Model.cost declaration). Refused rather than charged a fabricated 0 — the money ceiling would have silently stopped applying. Price every model this run can reach, or drop maxCostUsd from the window.");
|
|
273
|
+
e.code = "config.usage_window_unpriced";
|
|
274
|
+
throw e;
|
|
275
|
+
}
|
|
276
|
+
const tokenDelta = cumulativeTokens - chargedTokens;
|
|
277
|
+
const costDelta = cumulativeCostMicroUsd - chargedCostMicroUsd;
|
|
278
|
+
if (tokenDelta <= 0 && costDelta <= 0)
|
|
279
|
+
return;
|
|
280
|
+
await store.charge(key, Math.max(0, tokenDelta), now, windows, Math.max(0, costDelta));
|
|
281
|
+
chargedTokens = cumulativeTokens;
|
|
282
|
+
chargedCostMicroUsd = cumulativeCostMicroUsd;
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
231
286
|
export function resolveEnvLifetimeExpiry(env, observedAt) {
|
|
232
287
|
const lifetimeMs = env.lifetimeMs;
|
|
233
288
|
if (lifetimeMs === undefined)
|
|
@@ -267,7 +322,7 @@ class ParkRefusal extends Error {
|
|
|
267
322
|
}
|
|
268
323
|
}
|
|
269
324
|
export { resolveCheckpointStore } from "../checkpoint-store.js";
|
|
270
|
-
export { isFableFamilyModelId, resolveModelPromptTraits, resolveTaskLimits } from "./prepare-config-doors.js";
|
|
325
|
+
export { isFableFamilyModelId, resolveAttachmentsConfig, resolveModelPromptTraits, resolveTaskLimits } from "./prepare-config-doors.js";
|
|
271
326
|
export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./prepare-workspace-restore.js";
|
|
272
327
|
function buildMicroCompactState(deps, model, sessionId, offloadStore, knob) {
|
|
273
328
|
const triggerWindow = resolveTriggerWindow(model);
|
|
@@ -339,7 +394,7 @@ function publishCommittedSuspend(refs, token, gate, scope, remoteHandle, checkpo
|
|
|
339
394
|
function hasConversationContent(branch) {
|
|
340
395
|
return branch.some((e) => e.type === "message" || e.type === "custom_message" || e.type === "compaction");
|
|
341
396
|
}
|
|
342
|
-
function effectiveDelegationFacts(internals, seedIsDelegatedChild) {
|
|
397
|
+
export function effectiveDelegationFacts(internals, seedIsDelegatedChild) {
|
|
343
398
|
const isDelegatedChild = internals?.isDelegatedChild !== undefined ? internals.isDelegatedChild === true : seedIsDelegatedChild === true;
|
|
344
399
|
return { isDelegatedChild, isNonForkChild: isDelegatedChild && internals?.insideFork !== true };
|
|
345
400
|
}
|
|
@@ -475,20 +530,21 @@ const sanitizePreview = (node, depth = 0) => {
|
|
|
475
530
|
function resolveApprovalPreview(tools, toolName, args) {
|
|
476
531
|
const t = tools.find((x) => x.name === toolName || (x.aliases?.includes(toolName) ?? false));
|
|
477
532
|
if (t?.approvalPreview === undefined)
|
|
478
|
-
return
|
|
533
|
+
return {};
|
|
479
534
|
try {
|
|
480
535
|
const raw = t.approvalPreview(args);
|
|
481
536
|
if (raw === undefined)
|
|
482
|
-
return
|
|
537
|
+
return {};
|
|
483
538
|
const bytes = JSON.stringify(raw);
|
|
484
539
|
if (bytes === undefined)
|
|
485
|
-
return
|
|
486
|
-
if (bytes.length > 16_384)
|
|
487
|
-
return { truncated: true, note: `approval preview exceeded 16KiB (${bytes.length} chars serialized)` };
|
|
488
|
-
|
|
540
|
+
return { withheld: "unavailable" };
|
|
541
|
+
if (bytes.length > 16_384) {
|
|
542
|
+
return { preview: { truncated: true, note: `approval preview exceeded 16KiB (${bytes.length} chars serialized)` }, withheld: "oversize" };
|
|
543
|
+
}
|
|
544
|
+
return { preview: sanitizePreview(raw) };
|
|
489
545
|
}
|
|
490
546
|
catch {
|
|
491
|
-
return
|
|
547
|
+
return { withheld: "unavailable" };
|
|
492
548
|
}
|
|
493
549
|
}
|
|
494
550
|
function inheritedAskRuleEvidence(deps) {
|
|
@@ -839,7 +895,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
839
895
|
if (e.type === "task_progress")
|
|
840
896
|
forwardSink(e);
|
|
841
897
|
else if (spec.forwardSubagentEvents === true &&
|
|
842
|
-
(e.type === "text_delta" || e.type === "reasoning_delta" || e.type === "tool_start" || e.type === "tool_end")) {
|
|
898
|
+
(e.type === "text_delta" || e.type === "text_end" || e.type === "reasoning_delta" || e.type === "tool_start" || e.type === "tool_end")) {
|
|
843
899
|
forwardSink(e);
|
|
844
900
|
}
|
|
845
901
|
}
|
|
@@ -3140,6 +3196,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3140
3196
|
if (ask?.requiresRealApproval === true ||
|
|
3141
3197
|
ask?.persistedRuleShadowed !== undefined ||
|
|
3142
3198
|
ask?.decisionReason === "hook" ||
|
|
3199
|
+
ask?.matchedAskRule !== undefined ||
|
|
3143
3200
|
ask?.inheritedUnresolved === true ||
|
|
3144
3201
|
ask?.ancestorResolved === true) {
|
|
3145
3202
|
return {};
|
|
@@ -3550,31 +3607,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3550
3607
|
sessionId,
|
|
3551
3608
|
});
|
|
3552
3609
|
}
|
|
3553
|
-
const usageGovernance = (
|
|
3554
|
-
if (usageWindows === undefined || usageWindows.length === 0)
|
|
3555
|
-
return undefined;
|
|
3556
|
-
const store = deps.usageWindowStore;
|
|
3557
|
-
if (store === undefined) {
|
|
3558
|
-
deps.onError?.(new Error("RunnerDeps.usageWindows is set but INACTIVE: no `usageWindowStore` is wired, so cross-task usage cannot be counted and no window can be enforced. Wire InMemoryUsageWindowStore (process-local) or FileUsageWindowStore (restart-surviving)."), { phase: "config", sessionId });
|
|
3559
|
-
return undefined;
|
|
3560
|
-
}
|
|
3561
|
-
const key = spec.principal || GLOBAL_USAGE_KEY;
|
|
3562
|
-
const windows = usageWindows;
|
|
3563
|
-
let charged = 0;
|
|
3564
|
-
return {
|
|
3565
|
-
key,
|
|
3566
|
-
async check(now) {
|
|
3567
|
-
return usageRetryAfterMs(await store.read(key, windows, now));
|
|
3568
|
-
},
|
|
3569
|
-
async commit(cumulativeTokens, now) {
|
|
3570
|
-
const delta = cumulativeTokens - charged;
|
|
3571
|
-
if (delta <= 0)
|
|
3572
|
-
return;
|
|
3573
|
-
await store.charge(key, delta, now, windows);
|
|
3574
|
-
charged = cumulativeTokens;
|
|
3575
|
-
},
|
|
3576
|
-
};
|
|
3577
|
-
})();
|
|
3610
|
+
const usageGovernance = buildUsageGovernance(usageWindows, deps, spec.principal, sessionId);
|
|
3578
3611
|
const platformSuspendArmed = durableSuspendInfraReady && (envLifetimeSuspendAt !== undefined || usageGovernance !== undefined);
|
|
3579
3612
|
const checkpointStore = resolveCheckpointStore(spec, deps);
|
|
3580
3613
|
const durableApproval = spec.durableApproval ??
|
|
@@ -3667,8 +3700,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3667
3700
|
toolCallId: req.toolCallId,
|
|
3668
3701
|
args: req.args,
|
|
3669
3702
|
...(() => {
|
|
3670
|
-
const
|
|
3671
|
-
return preview !== undefined ? { preview } : {};
|
|
3703
|
+
const p = approvalPreviewOf(req.toolName, req.args);
|
|
3704
|
+
return { ...(p.preview !== undefined ? { preview: p.preview } : {}), ...(p.withheld !== undefined ? { previewWithheld: p.withheld } : {}) };
|
|
3672
3705
|
})(),
|
|
3673
3706
|
...ruleOffersOf(req.toolName, req.args, decision.action === "ask" ? decision : undefined),
|
|
3674
3707
|
message: decision.message ?? `approval required for "${req.toolName}"`,
|
|
@@ -4148,7 +4181,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4148
4181
|
}
|
|
4149
4182
|
};
|
|
4150
4183
|
const suspendAsk = parkLaneArmed && checkpointStore !== undefined
|
|
4151
|
-
? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason, probeReason, probeCause, segmentCoverage) => {
|
|
4184
|
+
? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason, probeReason, probeCause, segmentCoverage, matchedAskRule) => {
|
|
4152
4185
|
const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
|
|
4153
4186
|
if (syncFirstEligible &&
|
|
4154
4187
|
runtimeCaps?.forceDurableGate !== true &&
|
|
@@ -4306,10 +4339,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4306
4339
|
toolName: req.toolName,
|
|
4307
4340
|
args: parkedArgs,
|
|
4308
4341
|
...(() => {
|
|
4309
|
-
const
|
|
4310
|
-
const bidi = carriesBidiControls(parkedArgs) || carriesBidiControls(preview);
|
|
4342
|
+
const p = approvalPreviewOf(req.toolName, parkedArgs);
|
|
4343
|
+
const bidi = carriesBidiControls(parkedArgs) || carriesBidiControls(p.preview);
|
|
4311
4344
|
return {
|
|
4312
|
-
...(preview !== undefined ? { preview } : {}),
|
|
4345
|
+
...(p.preview !== undefined ? { preview: p.preview } : {}),
|
|
4346
|
+
...(p.withheld !== undefined ? { previewWithheld: p.withheld } : {}),
|
|
4313
4347
|
...(bidi ? { hasBidiControls: true } : {}),
|
|
4314
4348
|
};
|
|
4315
4349
|
})(),
|
|
@@ -4317,6 +4351,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4317
4351
|
...(realApproval !== undefined ? { requiresRealApproval: true } : {}),
|
|
4318
4352
|
...(shadowedRule !== undefined ? { persistedRuleShadowed: shadowedRule } : {}),
|
|
4319
4353
|
...(askDecisionReason !== undefined ? { decisionReason: askDecisionReason } : {}),
|
|
4354
|
+
...(matchedAskRule !== undefined ? { matchedAskRule } : {}),
|
|
4320
4355
|
...(inheritedUnavailableAsks.has(req.toolCallId) ? { inheritedUnresolved: true } : {}),
|
|
4321
4356
|
...(segmentCoverage !== undefined ? { segmentCoverage } : {}),
|
|
4322
4357
|
}),
|
|
@@ -51,6 +51,13 @@ interface ResumeRun {
|
|
|
51
51
|
* wake message second), each under its own trusted framing — the old merge-into-the-slot shape
|
|
52
52
|
* silently DISPLACED the parked (undelivered) supervisor steer. Wake outcomes only. */
|
|
53
53
|
wakeMessage?: Omit<PendingSteerEntry, "seq">;
|
|
54
|
+
/** design/373 §4.3 (D2) — the userPromptSubmit screen's `additionalContext` for {@link wakeMessage},
|
|
55
|
+
* captured at the resume ENTRY (the message is screened once, pre-CAS, on the resuming process's
|
|
56
|
+
* hook) and delivered by the drain as the engine's own reminder AHEAD of the wake frame — carrying
|
|
57
|
+
* it forward is what keeps the hook single-run (re-screening at the drain would be the double-run
|
|
58
|
+
* §4.3-3 reserves for the cross-process parked leg). Present only when a wake message passed a
|
|
59
|
+
* screen that supplied context. */
|
|
60
|
+
wakeMessageHookContext?: string;
|
|
54
61
|
/** Compensation hook (design/45/49): called iff the resumed run fails with `resume.env_failed` (post-CAS
|
|
55
62
|
* workspace `resumeVM` failed) OR `resume.tool_unavailable` (P-7: the approved tool vanished) — in both
|
|
56
63
|
* the CAS already consumed the checkpoint but the pending action never ran. `resumeStream` supplies a
|
|
@@ -15,7 +15,7 @@ import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
|
15
15
|
import { DEFAULT_COMPACTION_INSTRUCTIONS, createRapidRefillState, isCompactionManualCancel, maybeCompact, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill, sanitizeCompactionSettings } from "../auto-compaction.js";
|
|
16
16
|
import { ASK_USER_QUESTION_TOOL_NAME, canonicalizeCapturedPlainData, classifyQuestionOutcome, isLiveQuestionFace, markBoundOnlyQuestionFace } from "../ask-question.js";
|
|
17
17
|
import { boundInputHashOf } from "../canonical-json.js";
|
|
18
|
-
import { computeCostMicroUsd, modelCostToPricing } from "../pricing.js";
|
|
18
|
+
import { computeCostMicroUsd, isModelPriced, malformedPricingField, modelCostToPricing } from "../pricing.js";
|
|
19
19
|
import { emitTrace } from "../trace.js";
|
|
20
20
|
import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "../permission-rule-org.js";
|
|
21
21
|
import { emitTaskOutcome } from "../task-outcome.js";
|
|
@@ -36,13 +36,13 @@ import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
|
|
|
36
36
|
import { assembleResult, errorCodeOf } from "./assemble-result.js";
|
|
37
37
|
import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, attachmentEnvelopeTags, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, renderOrphanedBackgroundTasks, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta, stampWriteAnchor } from "./turn-attachments.js";
|
|
38
38
|
import { buildWorkingFileAttachments, centerAdoptionOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
|
|
39
|
-
import { gatedCallIdOf, prepareTask, resolveCheckpointStore } from "./prepare-task.js";
|
|
39
|
+
import { effectiveDelegationFacts, gatedCallIdOf, prepareTask, resolveCheckpointStore } from "./prepare-task.js";
|
|
40
40
|
import { settleTeardownLeg } from "./teardown-bounded.js";
|
|
41
41
|
import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
|
|
42
42
|
import { hasVerifiableStructureSignal } from "./grounding-signal.js";
|
|
43
43
|
import { hasDestroy, isIsolated } from "../remote-env.js";
|
|
44
44
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
45
|
-
import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
|
|
45
|
+
import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
|
|
46
46
|
import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
|
|
47
47
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
48
48
|
import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
|
|
@@ -395,7 +395,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
395
395
|
const governance = prepared.usageGovernance;
|
|
396
396
|
try {
|
|
397
397
|
const at = Date.now();
|
|
398
|
-
await awaitChargeWithSlowDisclosure(governance.commit(stats.tokens, at), () => runnerHooks.onError?.(new Error("the usage ledger CHARGE has not settled after 10s — still waiting (a charge is never abandoned: walking away would double-charge on the end-of-task flush). A wedged ledger store wedges this boundary, now visibly."), { phase: "config", sessionId: prepared.sessionId }));
|
|
398
|
+
await awaitChargeWithSlowDisclosure(governance.commit(stats.tokens, rs.telemetry.unpricedSpend ? undefined : stats.costMicroUsd, at), () => runnerHooks.onError?.(new Error("the usage ledger CHARGE has not settled after 10s — still waiting (a charge is never abandoned: walking away would double-charge on the end-of-task flush). A wedged ledger store wedges this boundary, now visibly."), { phase: "config", sessionId: prepared.sessionId }));
|
|
399
399
|
const windowRead = governance.check(at);
|
|
400
400
|
let answer;
|
|
401
401
|
if (ledgerReadDeadline !== undefined) {
|
|
@@ -606,6 +606,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
606
606
|
const mcpRemovals = mcpInstructionsOn && mcpDelta.pendingRemovals.length > 0 ? [...mcpDelta.pendingRemovals] : undefined;
|
|
607
607
|
const mcpDropped = mcpInstructionsOn && prepared.mcp.droppedTools.length > 0 ? selectMcpDroppedBatch(prepared.mcp.droppedTools) : undefined;
|
|
608
608
|
const budgetUsdOn = rs.attach.attachmentsCfg?.budgetUsd === true && rs.budget.maxCostMicroUsd !== undefined && rs.turn.lastTurnHadToolCalls;
|
|
609
|
+
const totalTokensOn = rs.attach.attachmentsCfg?.totalTokensReminder === true && rs.turn.lastTurnHadToolCalls;
|
|
609
610
|
const toolSearchReminderOn = rs.attach.attachmentsCfg?.toolSearchReminder === true && prepared.deferredToolNames !== undefined;
|
|
610
611
|
const undiscoveredTools = toolSearchReminderOn
|
|
611
612
|
? [...prepared.deferredToolNames].filter((n) => !prepared.activeTools.has(n)).sort()
|
|
@@ -623,6 +624,10 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
623
624
|
changedFiles: changedFilesOn,
|
|
624
625
|
planModeReminder: rs.attach.attachmentsCfg?.planModeReminder === true,
|
|
625
626
|
budgetUsd: budgetUsdOn,
|
|
627
|
+
totalTokensReminder: totalTokensOn,
|
|
628
|
+
...(rs.attach.attachmentsCfg?.totalTokensReminderMode !== undefined
|
|
629
|
+
? { totalTokensReminderMode: rs.attach.attachmentsCfg.totalTokensReminderMode }
|
|
630
|
+
: {}),
|
|
626
631
|
backgroundTasks: backgroundTasksOn,
|
|
627
632
|
toolsDelta: toolsDeltaOn,
|
|
628
633
|
agentListing: rs.attach.agentListingOn,
|
|
@@ -636,6 +641,9 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
636
641
|
...(budgetUsdOn && rs.budget.maxCostMicroUsd !== undefined
|
|
637
642
|
? { budgetUsd: { used: stats.costMicroUsd / 1e6, total: rs.budget.maxCostMicroUsd / 1e6 } }
|
|
638
643
|
: {}),
|
|
644
|
+
...(totalTokensOn && rs.budget.maxTokensWindow !== undefined
|
|
645
|
+
? { totalTokens: { used: stats.tokens, total: rs.budget.maxTokensWindow } }
|
|
646
|
+
: {}),
|
|
639
647
|
...(bgTasks !== undefined ? { backgroundTasks: bgTasks } : {}),
|
|
640
648
|
...(pendingTools !== undefined ? { newTools: pendingTools } : {}),
|
|
641
649
|
...(prepared.toolMaterializeStatic ? { newToolsStaticFace: true } : {}),
|
|
@@ -1131,6 +1139,9 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1131
1139
|
rs.turn.firstTokenAt = Date.now();
|
|
1132
1140
|
pushContent({ type: "text_delta", delta: ev.delta, ...ident() });
|
|
1133
1141
|
}
|
|
1142
|
+
else if (ev.type === "text_end" && typeof ev.content === "string" && ev.content.length !== 0) {
|
|
1143
|
+
pushContent({ type: "text_end", content: ev.content, ...ident() });
|
|
1144
|
+
}
|
|
1134
1145
|
else if (ev.type === "thinking_delta" && ev.delta) {
|
|
1135
1146
|
pushContent({ type: "reasoning_delta", delta: ev.delta, ...ident() });
|
|
1136
1147
|
}
|
|
@@ -1892,6 +1903,47 @@ export class Runner {
|
|
|
1892
1903
|
return;
|
|
1893
1904
|
}
|
|
1894
1905
|
}
|
|
1906
|
+
if (resultValue !== undefined || h.loop.ended)
|
|
1907
|
+
throw steeringError("the task is no longer running");
|
|
1908
|
+
if (mintsAFrame) {
|
|
1909
|
+
const screen = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
|
|
1910
|
+
if (screen !== undefined) {
|
|
1911
|
+
let decision;
|
|
1912
|
+
try {
|
|
1913
|
+
const seat = await runHookSeat("userPromptSubmit", { timeoutMs: h.hookTimeoutMs, signal: h.abortController.signal, abortEnds: true }, (sig) => screen(text, { identity: h.hookIdentity, signal: sig, source: "steer", inputId: effectiveInputId, ...(actor !== undefined ? { actor: snapshotActorAssertion(actor) } : {}) }));
|
|
1914
|
+
if (seat.expired) {
|
|
1915
|
+
if (seat.cause === "timeout") {
|
|
1916
|
+
try {
|
|
1917
|
+
this.deps.onError?.(hookSeatExpiredError("userPromptSubmit", h.hookTimeoutMs, seat.cause, "the steering input was NOT accepted (fail-closed) and the caller was refused typed"), { phase: "hook", sessionId: h.sessionId });
|
|
1918
|
+
}
|
|
1919
|
+
catch {
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
throw steeringError(seat.cause === "timeout"
|
|
1923
|
+
? `the deployment's userPromptSubmit hook did not answer within its ${h.hookTimeoutMs}ms bound while screening this steering input; the input was NOT accepted (fail-closed)`
|
|
1924
|
+
: `the task was cancelled while the deployment's userPromptSubmit hook was still screening this steering input; the input was NOT accepted (fail-closed)`, "steering.blocked_by_hook");
|
|
1925
|
+
}
|
|
1926
|
+
decision = seat.value;
|
|
1927
|
+
}
|
|
1928
|
+
catch (hookErr) {
|
|
1929
|
+
if (hookErr instanceof Error && hookErr.code === "steering.blocked_by_hook")
|
|
1930
|
+
throw hookErr;
|
|
1931
|
+
const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
|
|
1932
|
+
try {
|
|
1933
|
+
this.deps.onError?.(err, { phase: "hook", sessionId: h.sessionId });
|
|
1934
|
+
}
|
|
1935
|
+
catch {
|
|
1936
|
+
}
|
|
1937
|
+
throw steeringError(`the deployment's userPromptSubmit hook crashed while screening this steering input (${inlineUntrusted(err.message)}); the input was NOT accepted (fail-closed)`, "steering.blocked_by_hook");
|
|
1938
|
+
}
|
|
1939
|
+
if (decision?.block !== undefined && decision.block !== "") {
|
|
1940
|
+
throw steeringError(`the deployment's userPromptSubmit hook blocked this steering input: ${inlineUntrusted(decision.block)}`, "steering.blocked_by_hook");
|
|
1941
|
+
}
|
|
1942
|
+
if (decision?.additionalContext !== undefined && decision.additionalContext !== "") {
|
|
1943
|
+
payload = `${formatHookFeedback(decision.additionalContext, h.reminderMark)}\n\n${payload}`;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1895
1947
|
const injectFramed = async () => {
|
|
1896
1948
|
const noteOptions = { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) };
|
|
1897
1949
|
if (priority === "later") {
|
|
@@ -2236,7 +2288,7 @@ export class Runner {
|
|
|
2236
2288
|
}
|
|
2237
2289
|
}
|
|
2238
2290
|
const loopLatch = { ended: false, userInterrupted: false };
|
|
2239
|
-
onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark, sessionId: prepared.sessionId });
|
|
2291
|
+
onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark, sessionId: prepared.sessionId, hookTimeoutMs: prepared.hookTimeoutMs, hookIdentity: prepared.hookIdentity });
|
|
2240
2292
|
const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
|
|
2241
2293
|
prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
|
|
2242
2294
|
if (resume &&
|
|
@@ -2258,7 +2310,7 @@ export class Runner {
|
|
|
2258
2310
|
const rs = createRunState();
|
|
2259
2311
|
rs.telemetry.cacheFamily = cacheFamilyOf(prepared.model);
|
|
2260
2312
|
rs.telemetry.pricing = this.deps.pricing?.[prepared.model.id] ?? modelCostToPricing(prepared.model.cost);
|
|
2261
|
-
rs.telemetry.pricingConfigured =
|
|
2313
|
+
rs.telemetry.pricingConfigured = isModelPriced(prepared.model, this.deps.pricing);
|
|
2262
2314
|
if (spec.limits?.degrade) {
|
|
2263
2315
|
try {
|
|
2264
2316
|
rs.degrade.degradeToModel = resolveModel(spec.limits.degrade.to, modelCatalog);
|
|
@@ -2298,6 +2350,12 @@ export class Runner {
|
|
|
2298
2350
|
catch (err) {
|
|
2299
2351
|
discloseNoteTaskRunFailure(err);
|
|
2300
2352
|
}
|
|
2353
|
+
const noteUnevaluablePriceTable = (p) => {
|
|
2354
|
+
if (prepared.usageGovernance?.governsCost !== true)
|
|
2355
|
+
return;
|
|
2356
|
+
if (malformedPricingField(p) !== undefined)
|
|
2357
|
+
rs.telemetry.unpricedSpend = true;
|
|
2358
|
+
};
|
|
2301
2359
|
rs.degrade.recordDegraded = (info, toModel) => {
|
|
2302
2360
|
if (rs.degrade.degraded !== undefined)
|
|
2303
2361
|
return;
|
|
@@ -2312,13 +2370,15 @@ export class Runner {
|
|
|
2312
2370
|
}
|
|
2313
2371
|
if (m) {
|
|
2314
2372
|
rs.telemetry.pricing = this.deps.pricing?.[m.id] ?? modelCostToPricing(m.cost);
|
|
2315
|
-
rs.telemetry.pricingConfigured = this.deps.pricing
|
|
2373
|
+
rs.telemetry.pricingConfigured = isModelPriced(m, this.deps.pricing);
|
|
2374
|
+
noteUnevaluablePriceTable(rs.telemetry.pricing);
|
|
2316
2375
|
rs.telemetry.cacheFamily = cacheFamilyOf(m);
|
|
2317
2376
|
}
|
|
2318
2377
|
else {
|
|
2319
2378
|
if (this.deps.pricing?.[info.to]) {
|
|
2320
2379
|
rs.telemetry.pricing = this.deps.pricing[info.to];
|
|
2321
2380
|
rs.telemetry.pricingConfigured = true;
|
|
2381
|
+
noteUnevaluablePriceTable(rs.telemetry.pricing);
|
|
2322
2382
|
}
|
|
2323
2383
|
else {
|
|
2324
2384
|
rs.telemetry.pricingConfigured = false;
|
|
@@ -2737,9 +2797,10 @@ export class Runner {
|
|
|
2737
2797
|
return;
|
|
2738
2798
|
const fam = cacheFamilyOf(m);
|
|
2739
2799
|
const price = this.deps.pricing?.[m.id] ?? modelCostToPricing(m.cost);
|
|
2740
|
-
const priced = this.deps.pricing
|
|
2800
|
+
const priced = isModelPriced(m, this.deps.pricing);
|
|
2741
2801
|
if (!priced)
|
|
2742
2802
|
rs.telemetry.unpricedSpend = true;
|
|
2803
|
+
noteUnevaluablePriceTable(price);
|
|
2743
2804
|
const { totalInputTokens, uncachedInputTokens, costMicroUsd } = usageCostMicroUsd(fam, u, price);
|
|
2744
2805
|
stats.tokens += u.totalTokens || 0;
|
|
2745
2806
|
stats.promptTokens += uncachedInputTokens;
|
|
@@ -3312,6 +3373,72 @@ export class Runner {
|
|
|
3312
3373
|
...(resume.wakeMessage !== undefined ? [{ entry: resume.wakeMessage, source: "wake" }] : []),
|
|
3313
3374
|
];
|
|
3314
3375
|
for (const { entry: steer, source } of resumeFrames) {
|
|
3376
|
+
if (source === "wake" && resume.wakeMessageHookContext !== undefined) {
|
|
3377
|
+
const hookStart = continuation.length;
|
|
3378
|
+
continuation += "\n\n" + formatHookFeedback(resume.wakeMessageHookContext, prepared.reminderMark);
|
|
3379
|
+
engineSegments.push({ start: hookStart, end: continuation.length });
|
|
3380
|
+
}
|
|
3381
|
+
if (source === "steer") {
|
|
3382
|
+
const parkedScreen = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
|
|
3383
|
+
if (parkedScreen !== undefined) {
|
|
3384
|
+
let outcome;
|
|
3385
|
+
try {
|
|
3386
|
+
const seat = await runHookSeat("userPromptSubmit", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => parkedScreen(steer.text, {
|
|
3387
|
+
identity: prepared.hookIdentity,
|
|
3388
|
+
signal: sig,
|
|
3389
|
+
source: "parked_redelivery",
|
|
3390
|
+
...(steer.inputId !== undefined ? { inputId: steer.inputId } : {}),
|
|
3391
|
+
...(steer.actor !== undefined ? { actor: snapshotActorAssertion(steer.actor) } : {}),
|
|
3392
|
+
}));
|
|
3393
|
+
outcome = seat.expired
|
|
3394
|
+
? { blocked: seat.cause === "timeout" ? `the screen did not answer within its ${prepared.hookTimeoutMs}ms bound (fail-closed)` : "the run was cancelled while the screen was still deciding (fail-closed)" }
|
|
3395
|
+
: seat.value?.block !== undefined && seat.value.block !== ""
|
|
3396
|
+
? { blocked: inlineUntrusted(seat.value.block) }
|
|
3397
|
+
: { ...(seat.value?.additionalContext !== undefined && seat.value.additionalContext !== "" ? { context: seat.value.additionalContext } : {}) };
|
|
3398
|
+
}
|
|
3399
|
+
catch (hookErr) {
|
|
3400
|
+
const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
|
|
3401
|
+
try {
|
|
3402
|
+
this.deps.onError?.(err, { phase: "hook", sessionId: prepared.sessionId });
|
|
3403
|
+
}
|
|
3404
|
+
catch {
|
|
3405
|
+
}
|
|
3406
|
+
outcome = { blocked: `the screen crashed (${inlineUntrusted(err.message)}; fail-closed)` };
|
|
3407
|
+
}
|
|
3408
|
+
if ("blocked" in outcome) {
|
|
3409
|
+
deliverEngineNotice(this.deps.onNotice, {
|
|
3410
|
+
code: "steering.parked_input_blocked",
|
|
3411
|
+
message: "a parked steering input was withheld by the deployment's userPromptSubmit screen when this resume redelivered it: " +
|
|
3412
|
+
`${outcome.blocked}. The parked row was consumed with the checkpoint; the instruction never reached the model — ` +
|
|
3413
|
+
"re-issue it if it still applies.",
|
|
3414
|
+
detail: {
|
|
3415
|
+
...(steer.inputId !== undefined ? { inputId: steer.inputId } : {}),
|
|
3416
|
+
sessionId: prepared.sessionId,
|
|
3417
|
+
...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
|
|
3418
|
+
},
|
|
3419
|
+
});
|
|
3420
|
+
queue.push({
|
|
3421
|
+
...buildHumanInputEvent({
|
|
3422
|
+
carrier: source,
|
|
3423
|
+
source,
|
|
3424
|
+
delivery: "blocked",
|
|
3425
|
+
sessionSeq: nextHumanInputSeq(prepared.harness),
|
|
3426
|
+
...(steer.inputId !== undefined ? { inputId: steer.inputId } : {}),
|
|
3427
|
+
...(steer.actor !== undefined ? { actor: steer.actor } : {}),
|
|
3428
|
+
...(steer.actor?.issuer !== undefined ? { issuer: steer.actor.issuer } : {}),
|
|
3429
|
+
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
3430
|
+
}),
|
|
3431
|
+
...ident(),
|
|
3432
|
+
});
|
|
3433
|
+
continue;
|
|
3434
|
+
}
|
|
3435
|
+
if ("context" in outcome && outcome.context !== undefined) {
|
|
3436
|
+
const hookStart = continuation.length;
|
|
3437
|
+
continuation += "\n\n" + formatHookFeedback(outcome.context, prepared.reminderMark);
|
|
3438
|
+
engineSegments.push({ start: hookStart, end: continuation.length });
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3315
3442
|
const start = continuation.length;
|
|
3316
3443
|
const projected = projectHumanInput({ text: steer.text, actor: steer.actor, source });
|
|
3317
3444
|
continuation +=
|
|
@@ -3362,7 +3489,7 @@ export class Runner {
|
|
|
3362
3489
|
const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
|
|
3363
3490
|
if (userPromptSubmit) {
|
|
3364
3491
|
try {
|
|
3365
|
-
const promptSeat = await runHookSeat("userPromptSubmit", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => userPromptSubmit(spec.objective, { identity: prepared.hookIdentity, signal: sig }));
|
|
3492
|
+
const promptSeat = await runHookSeat("userPromptSubmit", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => userPromptSubmit(spec.objective, { identity: prepared.hookIdentity, signal: sig, source: "objective", ...(objectiveActor !== undefined ? { actor: snapshotActorAssertion(objectiveActor) } : {}) }));
|
|
3366
3493
|
if (promptSeat.expired) {
|
|
3367
3494
|
if (promptSeat.cause === "timeout") {
|
|
3368
3495
|
try {
|
|
@@ -3586,7 +3713,7 @@ export class Runner {
|
|
|
3586
3713
|
}
|
|
3587
3714
|
if (prepared.usageGovernance !== undefined) {
|
|
3588
3715
|
try {
|
|
3589
|
-
await awaitChargeWithSlowDisclosure(prepared.usageGovernance.commit(stats.tokens, Date.now()), () => this.deps.onError?.(new Error("the usage ledger's FINAL charge has not settled after 10s — still waiting (a charge is never abandoned: walking away would double-charge on a later flush). A wedged ledger store wedges this teardown, now visibly."), { phase: "config", sessionId: prepared.sessionId }));
|
|
3716
|
+
await awaitChargeWithSlowDisclosure(prepared.usageGovernance.commit(stats.tokens, rs.telemetry.unpricedSpend ? undefined : stats.costMicroUsd, Date.now()), () => this.deps.onError?.(new Error("the usage ledger's FINAL charge has not settled after 10s — still waiting (a charge is never abandoned: walking away would double-charge on a later flush). A wedged ledger store wedges this teardown, now visibly."), { phase: "config", sessionId: prepared.sessionId }));
|
|
3590
3717
|
}
|
|
3591
3718
|
catch (flushErr) {
|
|
3592
3719
|
this.deps.onError?.(flushErr, { phase: "config", sessionId: prepared.sessionId });
|
|
@@ -4169,7 +4296,9 @@ export class Runner {
|
|
|
4169
4296
|
}
|
|
4170
4297
|
}
|
|
4171
4298
|
let wakeMessage;
|
|
4299
|
+
let wakeHookContext;
|
|
4172
4300
|
const outcomeGate = outcome.gate;
|
|
4301
|
+
const resumeSignal = taskConfig.signal;
|
|
4173
4302
|
let suppliedMessage;
|
|
4174
4303
|
if (outcomeGate === "wake") {
|
|
4175
4304
|
suppliedMessage = outcome.message;
|
|
@@ -4207,6 +4336,62 @@ export class Runner {
|
|
|
4207
4336
|
}
|
|
4208
4337
|
if (suppliedMessage !== undefined) {
|
|
4209
4338
|
wakeMessage = validatePendingSteer(suppliedMessage);
|
|
4339
|
+
{
|
|
4340
|
+
const resumeScreenHooks = taskConfig.hooks ?? this.deps.hooks;
|
|
4341
|
+
const screen = resumeScreenHooks?.userPromptSubmit;
|
|
4342
|
+
if (screen !== undefined && resumeSignal?.aborted !== true) {
|
|
4343
|
+
const screenedMessage = wakeMessage;
|
|
4344
|
+
const identity = mintHookInvocationIdentity({
|
|
4345
|
+
sessionId: cp.sessionId,
|
|
4346
|
+
taskId: taskConfig.taskId ?? cp.sessionId,
|
|
4347
|
+
legKind: "resume",
|
|
4348
|
+
isDelegatedChild: effectiveDelegationFacts(internals, cp.state.isDelegatedChild).isDelegatedChild,
|
|
4349
|
+
...(internals?.insideFork === true ? { insideFork: true } : {}),
|
|
4350
|
+
...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
|
|
4351
|
+
...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
|
|
4352
|
+
});
|
|
4353
|
+
const screenTimeoutMs = resolveHookTimeoutMs(resumeScreenHooks?.timeoutMs, (badErr) => {
|
|
4354
|
+
try {
|
|
4355
|
+
this.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
|
|
4356
|
+
}
|
|
4357
|
+
catch {
|
|
4358
|
+
}
|
|
4359
|
+
}, resumeScreenHooks);
|
|
4360
|
+
let decision;
|
|
4361
|
+
try {
|
|
4362
|
+
const seat = await runHookSeat("userPromptSubmit", { timeoutMs: screenTimeoutMs, ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}), abortEnds: true }, (sig) => screen(screenedMessage.text, {
|
|
4363
|
+
identity,
|
|
4364
|
+
signal: sig,
|
|
4365
|
+
source: "resume_message",
|
|
4366
|
+
...(screenedMessage.inputId !== undefined ? { inputId: screenedMessage.inputId } : {}),
|
|
4367
|
+
...(screenedMessage.actor !== undefined ? { actor: snapshotActorAssertion(screenedMessage.actor) } : {}),
|
|
4368
|
+
}));
|
|
4369
|
+
if (seat.expired) {
|
|
4370
|
+
throw new CheckpointError("steering.blocked_by_hook", seat.cause === "timeout"
|
|
4371
|
+
? `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`
|
|
4372
|
+
: `the resume was cancelled while the deployment's userPromptSubmit hook was still screening this wake message; the checkpoint stays pending`);
|
|
4373
|
+
}
|
|
4374
|
+
decision = seat.value;
|
|
4375
|
+
}
|
|
4376
|
+
catch (hookErr) {
|
|
4377
|
+
if (hookErr instanceof CheckpointError)
|
|
4378
|
+
throw hookErr;
|
|
4379
|
+
const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
|
|
4380
|
+
try {
|
|
4381
|
+
this.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
|
|
4382
|
+
}
|
|
4383
|
+
catch {
|
|
4384
|
+
}
|
|
4385
|
+
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`);
|
|
4386
|
+
}
|
|
4387
|
+
if (decision?.block !== undefined && decision.block !== "") {
|
|
4388
|
+
throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook blocked this wake message: ${inlineUntrusted(decision.block)}`);
|
|
4389
|
+
}
|
|
4390
|
+
if (decision?.additionalContext !== undefined && decision.additionalContext !== "") {
|
|
4391
|
+
wakeHookContext = decision.additionalContext;
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4210
4395
|
}
|
|
4211
4396
|
else if (readPendingSteerQueue(cp.state).length === 0) {
|
|
4212
4397
|
throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
|
|
@@ -4454,7 +4639,7 @@ export class Runner {
|
|
|
4454
4639
|
"resuming here would run the leg with that ceiling unenforced. Rejected pre-CAS (the checkpoint stays pending): finish it on a worker of the " +
|
|
4455
4640
|
"previous release, or start a fresh task bounded by resourceSuspend.totalTokens / totalBudgetUsd.");
|
|
4456
4641
|
}
|
|
4457
|
-
if (
|
|
4642
|
+
if (resumeSignal?.aborted === true && cp.status === "pending") {
|
|
4458
4643
|
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)");
|
|
4459
4644
|
}
|
|
4460
4645
|
if (cp.state.workspaceHandle !== undefined && this.deps.executionEnvFactory === undefined) {
|
|
@@ -4528,7 +4713,7 @@ export class Runner {
|
|
|
4528
4713
|
const preCasWindows = resolveUsageWindows(this.deps.usageWindows);
|
|
4529
4714
|
if (preCasWindows !== undefined && preCasWindows.length > 0) {
|
|
4530
4715
|
const ledgerKey = (suppliedPrincipal ?? rowPrincipal) || GLOBAL_USAGE_KEY;
|
|
4531
|
-
const wait = usageRetryAfterMs(await this.deps.usageWindowStore.read(ledgerKey, preCasWindows, Date.now()));
|
|
4716
|
+
const wait = usageRetryAfterMs(await this.deps.usageWindowStore.read(ledgerKey, preCasWindows, Date.now()), preCasWindows);
|
|
4532
4717
|
if (wait !== undefined) {
|
|
4533
4718
|
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 — ` +
|
|
4534
4719
|
`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 });
|
|
@@ -4688,7 +4873,7 @@ export class Runner {
|
|
|
4688
4873
|
? new CheckpointError("checkpoint.reopen_failed", "the resume was aborted before the approved action could run AND the store refused to reopen the checkpoint — the approval is terminally consumed and the suspended work was not executed; a retry needs a fresh approval")
|
|
4689
4874
|
: new CheckpointError("checkpoint.reopen_failed", "the resume was aborted before the approved action could run and the reopen attempt FAILED IN FLIGHT — the checkpoint's state is unprovable from here: it may already be pending again. Re-read it before deciding; do NOT issue a fresh approval on the assumption the old one is dead (the approved action did NOT run either way)");
|
|
4690
4875
|
}
|
|
4691
|
-
return this.runTaskStream(spec, { cp, outcome: outcomeForStore, onEnvRestoreFailed, ...(wakeMessage !== undefined ? { wakeMessage } : {}) }, resumeInternals);
|
|
4876
|
+
return this.runTaskStream(spec, { cp, outcome: outcomeForStore, onEnvRestoreFailed, ...(wakeMessage !== undefined ? { wakeMessage } : {}), ...(wakeHookContext !== undefined ? { wakeMessageHookContext: wakeHookContext } : {}) }, resumeInternals);
|
|
4692
4877
|
}
|
|
4693
4878
|
async applyResumeDecision(prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart) {
|
|
4694
4879
|
const { pendingAction } = resume.cp;
|