@sema-agent/core 5.60.1 → 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.
Files changed (64) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/agents/subagent.d.ts +4 -2
  3. package/dist/agents/subagent.js +9 -9
  4. package/dist/brain/open-responses.js +8 -3
  5. package/dist/brain/openai.js +4 -4
  6. package/dist/brain/stream-engine.d.ts +13 -2
  7. package/dist/brain/stream-engine.js +3 -3
  8. package/dist/core/auto-mode-prompt-assets.js +1 -1
  9. package/dist/core/checkpoint-store.d.ts +36 -4
  10. package/dist/core/checkpoint-store.js +1 -0
  11. package/dist/core/governance-codes.d.ts +1 -1
  12. package/dist/core/governance-codes.js +4 -2
  13. package/dist/core/hooks.d.ts +83 -4
  14. package/dist/core/hooks.js +3 -3
  15. package/dist/core/memory-engine/consolidation-driver.d.ts +19 -1
  16. package/dist/core/memory-engine/consolidation-driver.js +75 -3
  17. package/dist/core/memory-engine/consolidation.d.ts +52 -5
  18. package/dist/core/memory-engine/consolidation.js +3 -1
  19. package/dist/core/memory-engine/distiller.d.ts +89 -1
  20. package/dist/core/memory-engine/distiller.js +94 -5
  21. package/dist/core/memory-engine/engine.d.ts +8 -0
  22. package/dist/core/memory-engine/engine.js +51 -8
  23. package/dist/core/memory-engine/index.d.ts +1 -1
  24. package/dist/core/memory-engine/index.js +1 -1
  25. package/dist/core/park-selfcheck.js +2 -0
  26. package/dist/core/pricing.d.ts +24 -0
  27. package/dist/core/pricing.js +18 -0
  28. package/dist/core/runner/prepare-config-doors.d.ts +34 -0
  29. package/dist/core/runner/prepare-config-doors.js +55 -0
  30. package/dist/core/runner/prepare-task.d.ts +52 -10
  31. package/dist/core/runner/prepare-task.js +77 -42
  32. package/dist/core/runner/runtask.d.ts +7 -0
  33. package/dist/core/runner/runtask.js +254 -38
  34. package/dist/core/runner/turn-attachments.d.ts +137 -5
  35. package/dist/core/runner/turn-attachments.js +25 -2
  36. package/dist/core/store-contracts/checkpoint-store-contract.js +19 -0
  37. package/dist/core/task-notification.d.ts +50 -23
  38. package/dist/core/task-notification.js +20 -4
  39. package/dist/core/tool-errors.d.ts +2 -1
  40. package/dist/core/tool-policy.d.ts +27 -0
  41. package/dist/core/types.d.ts +214 -31
  42. package/dist/core/untrusted-text.d.ts +5 -4
  43. package/dist/core/untrusted-text.js +8 -0
  44. package/dist/core/usage-window-store.d.ts +109 -8
  45. package/dist/core/usage-window-store.js +79 -12
  46. package/dist/engine/harness/agent-harness.d.ts +58 -2
  47. package/dist/engine/harness/agent-harness.js +115 -5
  48. package/dist/engine/loop/agent-loop.js +153 -15
  49. package/dist/engine/loop/types.d.ts +32 -0
  50. package/dist/index.d.ts +2 -2
  51. package/dist/index.js +2 -2
  52. package/dist/orchestration/run-workflow-tool.d.ts +9 -4
  53. package/dist/orchestration/run-workflow-tool.js +1 -1
  54. package/dist/orchestration/workflow.d.ts +2 -2
  55. package/dist/prompt-assembly/event-registry.js +2 -0
  56. package/dist/server/http.d.ts +1 -1
  57. package/dist/stores/file/usage-window-store.d.ts +1 -1
  58. package/dist/stores/file/usage-window-store.js +27 -6
  59. package/dist/tools/loop-tick.js +1 -1
  60. package/dist/tools/monitor.d.ts +3 -3
  61. package/dist/tools/monitor.js +1 -1
  62. package/dist/tools/scheduler-tools.js +9 -1
  63. package/package.json +1 -1
  64. package/test/export-surface.snapshot.json +7 -1
@@ -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";
@@ -50,7 +50,7 @@ import { RunnerSharedToolResultStore } from "../tool-result-store.js";
50
50
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
51
51
  import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
52
52
  import { defaultTaskRegistry } from "../task-registry.js";
53
- import { discloseDroppedPending, isDelegatedAgentTerminal, isSystemInjectionPriority, PendingSessionNotifications, renderTaskNotificationXml, SYSTEM_INJECTION_PRIORITIES, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
53
+ import { discloseDroppedPending, isDelegatedAgentTerminal, isSystemInjectionPriority, isTerminalTaskNotification, PendingSessionNotifications, renderTaskNotificationXml, SYSTEM_INJECTION_PRIORITIES, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
54
54
  import { ToolDetachHub } from "../tool-detach.js";
55
55
  import { createPeerInboundChainRef, createPeerSelfRef } from "../../agents/peer-admission.js";
56
56
  import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
@@ -78,6 +78,7 @@ function nextHumanInputSeq(key) {
78
78
  function sameAcceptedSteerInput(a, b) {
79
79
  return (a.payload === b.payload &&
80
80
  a.trusted === b.trusted &&
81
+ a.priority === b.priority &&
81
82
  a.actor?.id === b.actor?.id &&
82
83
  a.actor?.hostAsserted === b.actor?.hostAsserted &&
83
84
  a.actor?.issuer === b.actor?.issuer);
@@ -394,7 +395,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
394
395
  const governance = prepared.usageGovernance;
395
396
  try {
396
397
  const at = Date.now();
397
- 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 }));
398
399
  const windowRead = governance.check(at);
399
400
  let answer;
400
401
  if (ledgerReadDeadline !== undefined) {
@@ -605,6 +606,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
605
606
  const mcpRemovals = mcpInstructionsOn && mcpDelta.pendingRemovals.length > 0 ? [...mcpDelta.pendingRemovals] : undefined;
606
607
  const mcpDropped = mcpInstructionsOn && prepared.mcp.droppedTools.length > 0 ? selectMcpDroppedBatch(prepared.mcp.droppedTools) : undefined;
607
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;
608
610
  const toolSearchReminderOn = rs.attach.attachmentsCfg?.toolSearchReminder === true && prepared.deferredToolNames !== undefined;
609
611
  const undiscoveredTools = toolSearchReminderOn
610
612
  ? [...prepared.deferredToolNames].filter((n) => !prepared.activeTools.has(n)).sort()
@@ -622,6 +624,10 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
622
624
  changedFiles: changedFilesOn,
623
625
  planModeReminder: rs.attach.attachmentsCfg?.planModeReminder === true,
624
626
  budgetUsd: budgetUsdOn,
627
+ totalTokensReminder: totalTokensOn,
628
+ ...(rs.attach.attachmentsCfg?.totalTokensReminderMode !== undefined
629
+ ? { totalTokensReminderMode: rs.attach.attachmentsCfg.totalTokensReminderMode }
630
+ : {}),
625
631
  backgroundTasks: backgroundTasksOn,
626
632
  toolsDelta: toolsDeltaOn,
627
633
  agentListing: rs.attach.agentListingOn,
@@ -635,6 +641,9 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
635
641
  ...(budgetUsdOn && rs.budget.maxCostMicroUsd !== undefined
636
642
  ? { budgetUsd: { used: stats.costMicroUsd / 1e6, total: rs.budget.maxCostMicroUsd / 1e6 } }
637
643
  : {}),
644
+ ...(totalTokensOn && rs.budget.maxTokensWindow !== undefined
645
+ ? { totalTokens: { used: stats.tokens, total: rs.budget.maxTokensWindow } }
646
+ : {}),
638
647
  ...(bgTasks !== undefined ? { backgroundTasks: bgTasks } : {}),
639
648
  ...(pendingTools !== undefined ? { newTools: pendingTools } : {}),
640
649
  ...(prepared.toolMaterializeStatic ? { newToolsStaticFace: true } : {}),
@@ -1130,6 +1139,9 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1130
1139
  rs.turn.firstTokenAt = Date.now();
1131
1140
  pushContent({ type: "text_delta", delta: ev.delta, ...ident() });
1132
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
+ }
1133
1145
  else if (ev.type === "thinking_delta" && ev.delta) {
1134
1146
  pushContent({ type: "reasoning_delta", delta: ev.delta, ...ident() });
1135
1147
  }
@@ -1829,6 +1841,11 @@ export class Runner {
1829
1841
  throw steeringError(`inputId "${LEGACY_PENDING_STEER_INPUT_ID}" is reserved for a pre-queue parked steer and cannot be supplied by a caller`, "steering.invalid_content");
1830
1842
  }
1831
1843
  }
1844
+ const priorityIn = options?.priority;
1845
+ if (priorityIn !== undefined && !isSystemInjectionPriority(priorityIn)) {
1846
+ throw steeringError(`priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")} when supplied`, "steering.invalid_content");
1847
+ }
1848
+ const priority = priorityIn ?? "next";
1832
1849
  const actorIn = options?.actor;
1833
1850
  const actor = actorIn === undefined ? undefined : snapshotActorAssertion(actorIn);
1834
1851
  const projected = projectHumanInput({ text, actor, source: "steer" });
@@ -1837,6 +1854,7 @@ export class Runner {
1837
1854
  text,
1838
1855
  trusted,
1839
1856
  inputId: effectiveInputId,
1857
+ ...(priorityIn !== undefined ? { priority } : {}),
1840
1858
  ...(actor !== undefined ? { actor } : {}),
1841
1859
  };
1842
1860
  let payload;
@@ -1872,7 +1890,7 @@ export class Runner {
1872
1890
  throw steeringError("the task is not running");
1873
1891
  payload = trusted ? formatHookFeedback(projected, h.reminderMark) : frameMidTurnUserInput(projected);
1874
1892
  mintsAFrame = payload.trim().length !== 0;
1875
- replay = { payload, trusted, ...(actor !== undefined ? { actor } : {}) };
1893
+ replay = { payload, trusted, priority, ...(actor !== undefined ? { actor } : {}) };
1876
1894
  if (typeof inputId === "string") {
1877
1895
  const prior = acceptedSteerInputs.get(inputId);
1878
1896
  if (prior !== undefined) {
@@ -1885,9 +1903,73 @@ export class Runner {
1885
1903
  return;
1886
1904
  }
1887
1905
  }
1888
- try {
1889
- await h.harness.steer(payload, { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) });
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
+ }
1947
+ const injectFramed = async () => {
1948
+ const noteOptions = { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) };
1949
+ if (priority === "later") {
1950
+ await h.harness.followUp(payload, noteOptions);
1951
+ noteAccepted(h);
1952
+ return;
1953
+ }
1954
+ const frame = await h.harness.steer(payload, { ...noteOptions, ...(priority === "now" ? { immediate: true } : {}) });
1890
1955
  noteAccepted(h);
1956
+ if (priority === "now" && frame !== undefined && h.harness.interruptTurn(frame)) {
1957
+ deliverEngineNotice(this.deps.onNotice, {
1958
+ code: "task.turn_interrupted",
1959
+ message: "a caller-provenance steer with priority \"now\" interrupted the running turn: in-flight work was cut at " +
1960
+ "a manufactured boundary (finished tool calls keep their real results; never-started ones settle as " +
1961
+ "interrupted) and the run continues with the steer at the queue head.",
1962
+ detail: {
1963
+ inputId: effectiveInputId,
1964
+ sessionId: h.sessionId,
1965
+ ...(actor?.id !== undefined ? { actorId: actor.id } : {}),
1966
+ ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
1967
+ },
1968
+ });
1969
+ }
1970
+ };
1971
+ try {
1972
+ await injectFramed();
1891
1973
  return;
1892
1974
  }
1893
1975
  catch (e) {
@@ -1897,8 +1979,7 @@ export class Runner {
1897
1979
  const birthDeadline = Date.now() + READY_TIMEOUT_MS;
1898
1980
  while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
1899
1981
  try {
1900
- await h.harness.steer(payload, { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) });
1901
- noteAccepted(h);
1982
+ await injectFramed();
1902
1983
  return;
1903
1984
  }
1904
1985
  catch (e2) {
@@ -1938,9 +2019,14 @@ export class Runner {
1938
2019
  if (input.source !== undefined && typeof input.source !== "string") {
1939
2020
  throw notifyError("input.source must be a string when present", "notify.invalid_payload");
1940
2021
  }
1941
- if (opts?.priority !== undefined && !isSystemInjectionPriority(opts.priority)) {
2022
+ const priorityIn = opts?.priority;
2023
+ if (priorityIn !== undefined && !isSystemInjectionPriority(priorityIn)) {
1942
2024
  throw notifyError(`opts.priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")} when present`, "notify.invalid_payload");
1943
2025
  }
2026
+ const priority = priorityIn;
2027
+ if (priority === "now") {
2028
+ throw notifyError('priority "now" (turn interrupt) is not a notification-lane power — it belongs to the steer face; use "next" for earliest-boundary delivery', "notify.invalid_priority");
2029
+ }
1944
2030
  const payload = {
1945
2031
  task_id: input.task_id,
1946
2032
  task_type: "external",
@@ -1954,7 +2040,7 @@ export class Runner {
1954
2040
  if (!h || notifyRef.inject === undefined) {
1955
2041
  throw notifyError("the task is not running", "notify.not_running");
1956
2042
  }
1957
- notifyRef.inject(payload, opts);
2043
+ notifyRef.inject(payload, priority !== undefined ? { priority } : undefined);
1958
2044
  },
1959
2045
  compact: async (opts) => {
1960
2046
  if (resultValue)
@@ -2050,7 +2136,7 @@ export class Runner {
2050
2136
  let descendantAnchorDisclosed = false;
2051
2137
  const parkTaskNotification = (payload, priority) => {
2052
2138
  if (notificationSessionId !== undefined)
2053
- this.pendingSessionNotifications.pend(notificationSessionId, payload);
2139
+ this.pendingSessionNotifications.pend(notificationSessionId, payload, priority);
2054
2140
  if (!isDelegatedAgentTerminal(payload))
2055
2141
  return;
2056
2142
  const uplink = internals?.parentNotify;
@@ -2065,7 +2151,7 @@ export class Runner {
2065
2151
  }
2066
2152
  const rootAnchor = internals?.rootSessionId;
2067
2153
  if (rootAnchor !== undefined && rootAnchor !== notificationSessionId) {
2068
- this.pendingSessionNotifications.pend(rootAnchor, payload);
2154
+ this.pendingSessionNotifications.pend(rootAnchor, payload, priority);
2069
2155
  return;
2070
2156
  }
2071
2157
  if (internals?.parentSessionId !== undefined && !descendantAnchorDisclosed) {
@@ -2074,10 +2160,17 @@ export class Runner {
2074
2160
  }
2075
2161
  };
2076
2162
  const unsubscribeTaskNotifications = taskNotificationQueue.subscribe((item) => {
2077
- queue.push({ type: "task_notification", notification: item.payload, ...notificationIdent() });
2163
+ queue.push({ type: "task_notification", notification: item.payload, priority: item.priority, ...notificationIdent() });
2078
2164
  if (notificationHarness && prepared.batchHaltRef.current === undefined) {
2079
2165
  const xml = renderTaskNotificationXml(item.payload);
2080
- const deliver = notificationHarness.steer(xml, { provenance: "engine-note", enginePayload: item.payload });
2166
+ const noteOptions = {
2167
+ provenance: "engine-note",
2168
+ enginePayload: item.payload,
2169
+ ...(item.payload.task_type !== "external" && isTerminalTaskNotification(item.payload) ? { capPreferred: true } : {}),
2170
+ };
2171
+ const deliver = item.priority === "later"
2172
+ ? notificationHarness.followUp(xml, noteOptions)
2173
+ : notificationHarness.steer(xml, { ...noteOptions, ...(item.priority === "now" ? { immediate: true } : {}) });
2081
2174
  void deliver.then(() => item.onDisposition?.("queued"), () => {
2082
2175
  parkTaskNotification(item.payload, item.priority);
2083
2176
  item.onDisposition?.("parked");
@@ -2090,18 +2183,7 @@ export class Runner {
2090
2183
  });
2091
2184
  const upstreamTaskNotification = internals?.onTaskNotification;
2092
2185
  const deliveredAtTurnOpen = new Set();
2093
- let priorityNowDisclosed = false;
2094
2186
  const injectTaskNotification = (notification, opts) => {
2095
- if (opts?.priority === "now" && !priorityNowDisclosed) {
2096
- priorityNowDisclosed = true;
2097
- deliverEngineNotice(this.deps.onNotice, {
2098
- code: "task.injection_priority_unimplemented",
2099
- message: `a notification was injected with priority "now", which this engine does not implement: all injection ` +
2100
- `priorities deliver at the NEXT turn boundary and none aborts the running turn. The notification is ` +
2101
- `delivered — only the interrupting semantics are absent.`,
2102
- detail: { priority: "now", ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), sessionId: prepared.sessionId },
2103
- });
2104
- }
2105
2187
  if (deliveredAtTurnOpen.has(taskNotificationDedupKey(notification)))
2106
2188
  return Promise.resolve("dropped_duplicate");
2107
2189
  if (!notificationLaneLive) {
@@ -2196,16 +2278,17 @@ export class Runner {
2196
2278
  const pendingIdle = this.pendingSessionNotifications.drain(prepared.sessionId);
2197
2279
  if (pendingIdle !== undefined) {
2198
2280
  for (const payload of discloseDroppedPending(pendingIdle)) {
2281
+ const parkedPriority = pendingIdle.priorities?.get(payload);
2199
2282
  deliveredAtTurnOpen.add(taskNotificationDedupKey(payload));
2200
- queue.push({ type: "task_notification", notification: payload, ...ident() });
2283
+ queue.push({ type: "task_notification", notification: payload, ...(parkedPriority !== undefined ? { priority: parkedPriority } : {}), ...ident() });
2201
2284
  void prepared.harness.nextTurn(renderTaskNotificationXml(payload), { provenance: "engine-note", enginePayload: payload }).catch(() => {
2202
- this.pendingSessionNotifications.pend(prepared.sessionId, payload);
2285
+ this.pendingSessionNotifications.pend(prepared.sessionId, payload, parkedPriority);
2203
2286
  });
2204
2287
  }
2205
2288
  }
2206
2289
  }
2207
2290
  const loopLatch = { ended: false, userInterrupted: false };
2208
- onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark });
2291
+ onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark, sessionId: prepared.sessionId, hookTimeoutMs: prepared.hookTimeoutMs, hookIdentity: prepared.hookIdentity });
2209
2292
  const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
2210
2293
  prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
2211
2294
  if (resume &&
@@ -2227,7 +2310,7 @@ export class Runner {
2227
2310
  const rs = createRunState();
2228
2311
  rs.telemetry.cacheFamily = cacheFamilyOf(prepared.model);
2229
2312
  rs.telemetry.pricing = this.deps.pricing?.[prepared.model.id] ?? modelCostToPricing(prepared.model.cost);
2230
- rs.telemetry.pricingConfigured = this.deps.pricing?.[prepared.model.id] !== undefined || prepared.model.cost !== undefined;
2313
+ rs.telemetry.pricingConfigured = isModelPriced(prepared.model, this.deps.pricing);
2231
2314
  if (spec.limits?.degrade) {
2232
2315
  try {
2233
2316
  rs.degrade.degradeToModel = resolveModel(spec.limits.degrade.to, modelCatalog);
@@ -2267,6 +2350,12 @@ export class Runner {
2267
2350
  catch (err) {
2268
2351
  discloseNoteTaskRunFailure(err);
2269
2352
  }
2353
+ const noteUnevaluablePriceTable = (p) => {
2354
+ if (prepared.usageGovernance?.governsCost !== true)
2355
+ return;
2356
+ if (malformedPricingField(p) !== undefined)
2357
+ rs.telemetry.unpricedSpend = true;
2358
+ };
2270
2359
  rs.degrade.recordDegraded = (info, toModel) => {
2271
2360
  if (rs.degrade.degraded !== undefined)
2272
2361
  return;
@@ -2281,13 +2370,15 @@ export class Runner {
2281
2370
  }
2282
2371
  if (m) {
2283
2372
  rs.telemetry.pricing = this.deps.pricing?.[m.id] ?? modelCostToPricing(m.cost);
2284
- rs.telemetry.pricingConfigured = this.deps.pricing?.[m.id] !== undefined || m.cost !== undefined;
2373
+ rs.telemetry.pricingConfigured = isModelPriced(m, this.deps.pricing);
2374
+ noteUnevaluablePriceTable(rs.telemetry.pricing);
2285
2375
  rs.telemetry.cacheFamily = cacheFamilyOf(m);
2286
2376
  }
2287
2377
  else {
2288
2378
  if (this.deps.pricing?.[info.to]) {
2289
2379
  rs.telemetry.pricing = this.deps.pricing[info.to];
2290
2380
  rs.telemetry.pricingConfigured = true;
2381
+ noteUnevaluablePriceTable(rs.telemetry.pricing);
2291
2382
  }
2292
2383
  else {
2293
2384
  rs.telemetry.pricingConfigured = false;
@@ -2706,9 +2797,10 @@ export class Runner {
2706
2797
  return;
2707
2798
  const fam = cacheFamilyOf(m);
2708
2799
  const price = this.deps.pricing?.[m.id] ?? modelCostToPricing(m.cost);
2709
- const priced = this.deps.pricing?.[m.id] !== undefined || m.cost !== undefined;
2800
+ const priced = isModelPriced(m, this.deps.pricing);
2710
2801
  if (!priced)
2711
2802
  rs.telemetry.unpricedSpend = true;
2803
+ noteUnevaluablePriceTable(price);
2712
2804
  const { totalInputTokens, uncachedInputTokens, costMicroUsd } = usageCostMicroUsd(fam, u, price);
2713
2805
  stats.tokens += u.totalTokens || 0;
2714
2806
  stats.promptTokens += uncachedInputTokens;
@@ -3281,6 +3373,72 @@ export class Runner {
3281
3373
  ...(resume.wakeMessage !== undefined ? [{ entry: resume.wakeMessage, source: "wake" }] : []),
3282
3374
  ];
3283
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
+ }
3284
3442
  const start = continuation.length;
3285
3443
  const projected = projectHumanInput({ text: steer.text, actor: steer.actor, source });
3286
3444
  continuation +=
@@ -3331,7 +3489,7 @@ export class Runner {
3331
3489
  const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
3332
3490
  if (userPromptSubmit) {
3333
3491
  try {
3334
- 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) } : {}) }));
3335
3493
  if (promptSeat.expired) {
3336
3494
  if (promptSeat.cause === "timeout") {
3337
3495
  try {
@@ -3555,7 +3713,7 @@ export class Runner {
3555
3713
  }
3556
3714
  if (prepared.usageGovernance !== undefined) {
3557
3715
  try {
3558
- 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 }));
3559
3717
  }
3560
3718
  catch (flushErr) {
3561
3719
  this.deps.onError?.(flushErr, { phase: "config", sessionId: prepared.sessionId });
@@ -4138,7 +4296,9 @@ export class Runner {
4138
4296
  }
4139
4297
  }
4140
4298
  let wakeMessage;
4299
+ let wakeHookContext;
4141
4300
  const outcomeGate = outcome.gate;
4301
+ const resumeSignal = taskConfig.signal;
4142
4302
  let suppliedMessage;
4143
4303
  if (outcomeGate === "wake") {
4144
4304
  suppliedMessage = outcome.message;
@@ -4176,6 +4336,62 @@ export class Runner {
4176
4336
  }
4177
4337
  if (suppliedMessage !== undefined) {
4178
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
+ }
4179
4395
  }
4180
4396
  else if (readPendingSteerQueue(cp.state).length === 0) {
4181
4397
  throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
@@ -4423,7 +4639,7 @@ export class Runner {
4423
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 " +
4424
4640
  "previous release, or start a fresh task bounded by resourceSuspend.totalTokens / totalBudgetUsd.");
4425
4641
  }
4426
- if (taskConfig.signal?.aborted === true && cp.status === "pending") {
4642
+ if (resumeSignal?.aborted === true && cp.status === "pending") {
4427
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)");
4428
4644
  }
4429
4645
  if (cp.state.workspaceHandle !== undefined && this.deps.executionEnvFactory === undefined) {
@@ -4497,7 +4713,7 @@ export class Runner {
4497
4713
  const preCasWindows = resolveUsageWindows(this.deps.usageWindows);
4498
4714
  if (preCasWindows !== undefined && preCasWindows.length > 0) {
4499
4715
  const ledgerKey = (suppliedPrincipal ?? rowPrincipal) || GLOBAL_USAGE_KEY;
4500
- 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);
4501
4717
  if (wait !== undefined) {
4502
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 — ` +
4503
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 });
@@ -4657,7 +4873,7 @@ export class Runner {
4657
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")
4658
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)");
4659
4875
  }
4660
- 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);
4661
4877
  }
4662
4878
  async applyResumeDecision(prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart) {
4663
4879
  const { pendingAction } = resume.cp;