@sema-agent/core 5.60.0 → 5.61.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 (36) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/dist/agents/subagent.d.ts +4 -2
  3. package/dist/agents/subagent.js +9 -9
  4. package/dist/core/checkpoint-store.d.ts +56 -6
  5. package/dist/core/governance-codes.d.ts +1 -1
  6. package/dist/core/governance-codes.js +2 -2
  7. package/dist/core/mcp.d.ts +4 -5
  8. package/dist/core/memory-engine/consolidation-driver.d.ts +19 -1
  9. package/dist/core/memory-engine/consolidation-driver.js +75 -3
  10. package/dist/core/memory-engine/consolidation.d.ts +52 -5
  11. package/dist/core/memory-engine/consolidation.js +3 -1
  12. package/dist/core/memory-engine/distiller.d.ts +89 -1
  13. package/dist/core/memory-engine/distiller.js +94 -5
  14. package/dist/core/memory-engine/engine.d.ts +13 -6
  15. package/dist/core/memory-engine/engine.js +51 -8
  16. package/dist/core/memory-engine/index.d.ts +1 -1
  17. package/dist/core/memory-engine/index.js +1 -1
  18. package/dist/core/runner/prepare-task.d.ts +6 -3
  19. package/dist/core/runner/prepare-task.js +1 -0
  20. package/dist/core/runner/runtask.js +113 -31
  21. package/dist/core/task-notification.d.ts +50 -23
  22. package/dist/core/task-notification.js +20 -4
  23. package/dist/core/tool-policy.d.ts +8 -2
  24. package/dist/core/types.d.ts +76 -19
  25. package/dist/engine/harness/agent-harness.d.ts +58 -2
  26. package/dist/engine/harness/agent-harness.js +115 -5
  27. package/dist/engine/loop/agent-loop.js +153 -15
  28. package/dist/engine/loop/types.d.ts +32 -0
  29. package/dist/index.d.ts +3 -3
  30. package/dist/index.js +3 -3
  31. package/dist/orchestration/run-workflow-tool.d.ts +7 -2
  32. package/dist/orchestration/run-workflow-tool.js +1 -1
  33. package/dist/tools/monitor.d.ts +3 -3
  34. package/dist/tools/monitor.js +1 -1
  35. package/package.json +1 -1
  36. package/test/export-surface.snapshot.json +11 -1
@@ -1,6 +1,7 @@
1
1
  import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
2
2
  import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
3
3
  import { mintSystemReminder, openSystemReminder } from "../reminder-mint.js";
4
+ import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
4
5
  import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
5
6
  import { planRejectionClears, resolveTriggerWindow } from "../context-edit.js";
6
7
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
@@ -49,7 +50,7 @@ import { RunnerSharedToolResultStore } from "../tool-result-store.js";
49
50
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
50
51
  import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
51
52
  import { defaultTaskRegistry } from "../task-registry.js";
52
- 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";
53
54
  import { ToolDetachHub } from "../tool-detach.js";
54
55
  import { createPeerInboundChainRef, createPeerSelfRef } from "../../agents/peer-admission.js";
55
56
  import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
@@ -77,6 +78,7 @@ function nextHumanInputSeq(key) {
77
78
  function sameAcceptedSteerInput(a, b) {
78
79
  return (a.payload === b.payload &&
79
80
  a.trusted === b.trusted &&
81
+ a.priority === b.priority &&
80
82
  a.actor?.id === b.actor?.id &&
81
83
  a.actor?.hostAsserted === b.actor?.hostAsserted &&
82
84
  a.actor?.issuer === b.actor?.issuer);
@@ -344,9 +346,11 @@ export async function raceUntilDeadline(p, deadline) {
344
346
  function platformLimitTerminal(reason, retryAfterMs, moment = "turn_boundary") {
345
347
  const message = moment === "entry"
346
348
  ? `a deployment usage window is exhausted (RunnerDeps.usageWindows), so the task was refused before its first model call — nothing ran and nothing was spent. There is no checkpoint to suspend into at this point, so the caller re-submits after the window frees, in ${String(retryAfterMs ?? 0)}ms.`
347
- : reason === "env_lifetime"
348
- ? "the execution environment's declared lifetime is expiring and the run could not be suspended durably (not eligible, or the suspend was refused see remoteEnvFailures / onError). It was stopped at a clean turn boundary instead of being killed mid-turn when the platform reclaims the environment."
349
- : `a deployment usage window is exhausted (RunnerDeps.usageWindows) and the run could not be suspended durably, so it was stopped at a clean turn boundary. The window frees in ${String(retryAfterMs ?? 0)}ms.`;
349
+ : moment === "resume"
350
+ ? `a deployment usage window is exhausted (RunnerDeps.usageWindows), so the resumed leg was stopped BEFORE its first model call nothing was delivered or executed on this leg. The window frees in ${String(retryAfterMs ?? 0)}ms.`
351
+ : reason === "env_lifetime"
352
+ ? "the execution environment's declared lifetime is expiring and the run could not be suspended durably (not eligible, or the suspend was refused — see remoteEnvFailures / onError). It was stopped at a clean turn boundary instead of being killed mid-turn when the platform reclaims the environment."
353
+ : `a deployment usage window is exhausted (RunnerDeps.usageWindows) and the run could not be suspended durably, so it was stopped at a clean turn boundary. The window frees in ${String(retryAfterMs ?? 0)}ms.`;
350
354
  const e = new Error(message);
351
355
  e.code = reason === "env_lifetime" ? "env.lifetime_expired" : "usage.window_exhausted";
352
356
  if (retryAfterMs !== undefined)
@@ -1826,6 +1830,11 @@ export class Runner {
1826
1830
  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");
1827
1831
  }
1828
1832
  }
1833
+ const priorityIn = options?.priority;
1834
+ if (priorityIn !== undefined && !isSystemInjectionPriority(priorityIn)) {
1835
+ throw steeringError(`priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")} when supplied`, "steering.invalid_content");
1836
+ }
1837
+ const priority = priorityIn ?? "next";
1829
1838
  const actorIn = options?.actor;
1830
1839
  const actor = actorIn === undefined ? undefined : snapshotActorAssertion(actorIn);
1831
1840
  const projected = projectHumanInput({ text, actor, source: "steer" });
@@ -1834,6 +1843,7 @@ export class Runner {
1834
1843
  text,
1835
1844
  trusted,
1836
1845
  inputId: effectiveInputId,
1846
+ ...(priorityIn !== undefined ? { priority } : {}),
1837
1847
  ...(actor !== undefined ? { actor } : {}),
1838
1848
  };
1839
1849
  let payload;
@@ -1869,7 +1879,7 @@ export class Runner {
1869
1879
  throw steeringError("the task is not running");
1870
1880
  payload = trusted ? formatHookFeedback(projected, h.reminderMark) : frameMidTurnUserInput(projected);
1871
1881
  mintsAFrame = payload.trim().length !== 0;
1872
- replay = { payload, trusted, ...(actor !== undefined ? { actor } : {}) };
1882
+ replay = { payload, trusted, priority, ...(actor !== undefined ? { actor } : {}) };
1873
1883
  if (typeof inputId === "string") {
1874
1884
  const prior = acceptedSteerInputs.get(inputId);
1875
1885
  if (prior !== undefined) {
@@ -1882,9 +1892,32 @@ export class Runner {
1882
1892
  return;
1883
1893
  }
1884
1894
  }
1885
- try {
1886
- await h.harness.steer(payload, { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) });
1895
+ const injectFramed = async () => {
1896
+ const noteOptions = { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) };
1897
+ if (priority === "later") {
1898
+ await h.harness.followUp(payload, noteOptions);
1899
+ noteAccepted(h);
1900
+ return;
1901
+ }
1902
+ const frame = await h.harness.steer(payload, { ...noteOptions, ...(priority === "now" ? { immediate: true } : {}) });
1887
1903
  noteAccepted(h);
1904
+ if (priority === "now" && frame !== undefined && h.harness.interruptTurn(frame)) {
1905
+ deliverEngineNotice(this.deps.onNotice, {
1906
+ code: "task.turn_interrupted",
1907
+ message: "a caller-provenance steer with priority \"now\" interrupted the running turn: in-flight work was cut at " +
1908
+ "a manufactured boundary (finished tool calls keep their real results; never-started ones settle as " +
1909
+ "interrupted) and the run continues with the steer at the queue head.",
1910
+ detail: {
1911
+ inputId: effectiveInputId,
1912
+ sessionId: h.sessionId,
1913
+ ...(actor?.id !== undefined ? { actorId: actor.id } : {}),
1914
+ ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
1915
+ },
1916
+ });
1917
+ }
1918
+ };
1919
+ try {
1920
+ await injectFramed();
1888
1921
  return;
1889
1922
  }
1890
1923
  catch (e) {
@@ -1894,8 +1927,7 @@ export class Runner {
1894
1927
  const birthDeadline = Date.now() + READY_TIMEOUT_MS;
1895
1928
  while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
1896
1929
  try {
1897
- await h.harness.steer(payload, { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) });
1898
- noteAccepted(h);
1930
+ await injectFramed();
1899
1931
  return;
1900
1932
  }
1901
1933
  catch (e2) {
@@ -1935,9 +1967,14 @@ export class Runner {
1935
1967
  if (input.source !== undefined && typeof input.source !== "string") {
1936
1968
  throw notifyError("input.source must be a string when present", "notify.invalid_payload");
1937
1969
  }
1938
- if (opts?.priority !== undefined && !isSystemInjectionPriority(opts.priority)) {
1970
+ const priorityIn = opts?.priority;
1971
+ if (priorityIn !== undefined && !isSystemInjectionPriority(priorityIn)) {
1939
1972
  throw notifyError(`opts.priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")} when present`, "notify.invalid_payload");
1940
1973
  }
1974
+ const priority = priorityIn;
1975
+ if (priority === "now") {
1976
+ 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");
1977
+ }
1941
1978
  const payload = {
1942
1979
  task_id: input.task_id,
1943
1980
  task_type: "external",
@@ -1951,7 +1988,7 @@ export class Runner {
1951
1988
  if (!h || notifyRef.inject === undefined) {
1952
1989
  throw notifyError("the task is not running", "notify.not_running");
1953
1990
  }
1954
- notifyRef.inject(payload, opts);
1991
+ notifyRef.inject(payload, priority !== undefined ? { priority } : undefined);
1955
1992
  },
1956
1993
  compact: async (opts) => {
1957
1994
  if (resultValue)
@@ -2047,7 +2084,7 @@ export class Runner {
2047
2084
  let descendantAnchorDisclosed = false;
2048
2085
  const parkTaskNotification = (payload, priority) => {
2049
2086
  if (notificationSessionId !== undefined)
2050
- this.pendingSessionNotifications.pend(notificationSessionId, payload);
2087
+ this.pendingSessionNotifications.pend(notificationSessionId, payload, priority);
2051
2088
  if (!isDelegatedAgentTerminal(payload))
2052
2089
  return;
2053
2090
  const uplink = internals?.parentNotify;
@@ -2062,7 +2099,7 @@ export class Runner {
2062
2099
  }
2063
2100
  const rootAnchor = internals?.rootSessionId;
2064
2101
  if (rootAnchor !== undefined && rootAnchor !== notificationSessionId) {
2065
- this.pendingSessionNotifications.pend(rootAnchor, payload);
2102
+ this.pendingSessionNotifications.pend(rootAnchor, payload, priority);
2066
2103
  return;
2067
2104
  }
2068
2105
  if (internals?.parentSessionId !== undefined && !descendantAnchorDisclosed) {
@@ -2071,10 +2108,17 @@ export class Runner {
2071
2108
  }
2072
2109
  };
2073
2110
  const unsubscribeTaskNotifications = taskNotificationQueue.subscribe((item) => {
2074
- queue.push({ type: "task_notification", notification: item.payload, ...notificationIdent() });
2111
+ queue.push({ type: "task_notification", notification: item.payload, priority: item.priority, ...notificationIdent() });
2075
2112
  if (notificationHarness && prepared.batchHaltRef.current === undefined) {
2076
2113
  const xml = renderTaskNotificationXml(item.payload);
2077
- const deliver = notificationHarness.steer(xml, { provenance: "engine-note", enginePayload: item.payload });
2114
+ const noteOptions = {
2115
+ provenance: "engine-note",
2116
+ enginePayload: item.payload,
2117
+ ...(item.payload.task_type !== "external" && isTerminalTaskNotification(item.payload) ? { capPreferred: true } : {}),
2118
+ };
2119
+ const deliver = item.priority === "later"
2120
+ ? notificationHarness.followUp(xml, noteOptions)
2121
+ : notificationHarness.steer(xml, { ...noteOptions, ...(item.priority === "now" ? { immediate: true } : {}) });
2078
2122
  void deliver.then(() => item.onDisposition?.("queued"), () => {
2079
2123
  parkTaskNotification(item.payload, item.priority);
2080
2124
  item.onDisposition?.("parked");
@@ -2087,18 +2131,7 @@ export class Runner {
2087
2131
  });
2088
2132
  const upstreamTaskNotification = internals?.onTaskNotification;
2089
2133
  const deliveredAtTurnOpen = new Set();
2090
- let priorityNowDisclosed = false;
2091
2134
  const injectTaskNotification = (notification, opts) => {
2092
- if (opts?.priority === "now" && !priorityNowDisclosed) {
2093
- priorityNowDisclosed = true;
2094
- deliverEngineNotice(this.deps.onNotice, {
2095
- code: "task.injection_priority_unimplemented",
2096
- message: `a notification was injected with priority "now", which this engine does not implement: all injection ` +
2097
- `priorities deliver at the NEXT turn boundary and none aborts the running turn. The notification is ` +
2098
- `delivered — only the interrupting semantics are absent.`,
2099
- detail: { priority: "now", ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), sessionId: prepared.sessionId },
2100
- });
2101
- }
2102
2135
  if (deliveredAtTurnOpen.has(taskNotificationDedupKey(notification)))
2103
2136
  return Promise.resolve("dropped_duplicate");
2104
2137
  if (!notificationLaneLive) {
@@ -2193,16 +2226,17 @@ export class Runner {
2193
2226
  const pendingIdle = this.pendingSessionNotifications.drain(prepared.sessionId);
2194
2227
  if (pendingIdle !== undefined) {
2195
2228
  for (const payload of discloseDroppedPending(pendingIdle)) {
2229
+ const parkedPriority = pendingIdle.priorities?.get(payload);
2196
2230
  deliveredAtTurnOpen.add(taskNotificationDedupKey(payload));
2197
- queue.push({ type: "task_notification", notification: payload, ...ident() });
2231
+ queue.push({ type: "task_notification", notification: payload, ...(parkedPriority !== undefined ? { priority: parkedPriority } : {}), ...ident() });
2198
2232
  void prepared.harness.nextTurn(renderTaskNotificationXml(payload), { provenance: "engine-note", enginePayload: payload }).catch(() => {
2199
- this.pendingSessionNotifications.pend(prepared.sessionId, payload);
2233
+ this.pendingSessionNotifications.pend(prepared.sessionId, payload, parkedPriority);
2200
2234
  });
2201
2235
  }
2202
2236
  }
2203
2237
  }
2204
2238
  const loopLatch = { ended: false, userInterrupted: false };
2205
- onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark });
2239
+ onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark, sessionId: prepared.sessionId });
2206
2240
  const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
2207
2241
  prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
2208
2242
  if (resume &&
@@ -3186,7 +3220,29 @@ export class Runner {
3186
3220
  }
3187
3221
  if (resume) {
3188
3222
  const walltimeExhaustedResume = effectiveTimeoutMs !== undefined && effectiveTimeoutMs <= 0;
3189
- if (!walltimeExhaustedResume && resume.outcome.gate !== "wake") {
3223
+ const resumeWindowRetryAfterMs = !walltimeExhaustedResume && prepared.usageGovernance !== undefined ? await prepared.usageGovernance.check(Date.now()) : undefined;
3224
+ if (resumeWindowRetryAfterMs !== undefined) {
3225
+ const owesNothing = resume.outcome.gate === "resource_limit" &&
3226
+ readPendingSteerQueue(resume.cp.state).length === 0 &&
3227
+ (resume.cp.state.runningBackgroundTasks?.length ?? 0) === 0;
3228
+ const committed = owesNothing &&
3229
+ prepared.suspendForPlatformLimit !== undefined &&
3230
+ (await prepared.suspendForPlatformLimit("usage_window", { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) }, { resumeAfterMs: resumeWindowRetryAfterMs }));
3231
+ if (committed) {
3232
+ rs.limits.turnsExceeded = false;
3233
+ rs.limits.budgetHit = undefined;
3234
+ }
3235
+ else {
3236
+ const terminal = platformLimitTerminal("usage_window", resumeWindowRetryAfterMs, "resume");
3237
+ rs.limits.platformTerminal = terminal;
3238
+ try {
3239
+ this.deps.onError?.(terminal, { phase: "config", sessionId: prepared.sessionId });
3240
+ }
3241
+ catch {
3242
+ }
3243
+ }
3244
+ }
3245
+ if (resumeWindowRetryAfterMs === undefined && !walltimeExhaustedResume && resume.outcome.gate !== "wake") {
3190
3246
  await this.applyResumeDecision(prepared, resume, (e) => pushContent({ ...e, ...ident() }), emitCommitted, (toolName, details) => {
3191
3247
  if (rs.attach.attachState === undefined)
3192
3248
  return;
@@ -3205,7 +3261,9 @@ export class Runner {
3205
3261
  resume.decisionDelivered = true;
3206
3262
  announceWorkspaceMove();
3207
3263
  }
3208
- if (walltimeExhaustedResume) {
3264
+ if (resumeWindowRetryAfterMs !== undefined) {
3265
+ }
3266
+ else if (walltimeExhaustedResume) {
3209
3267
  timeout.fired = true;
3210
3268
  timeout.clear();
3211
3269
  void prepared.harness.abort();
@@ -4455,6 +4513,29 @@ export class Runner {
4455
4513
  throw new CheckpointError("checkpoint.invalid_outcome", "TaskSpec.basePolicyForResumeEdit is administratively locked by this deployment (locked key \"toolPolicy\") — a task-supplied " +
4456
4514
  "resume-edit policy is refused pre-CAS (the checkpoint stays pending); remove the field or change the deployment's lock configuration");
4457
4515
  }
4516
+ {
4517
+ const rowPrincipal = cp.principal || undefined;
4518
+ const suppliedPrincipal = taskConfig.principal || undefined;
4519
+ if (rowPrincipal !== undefined && suppliedPrincipal !== undefined && suppliedPrincipal !== rowPrincipal) {
4520
+ throw new CheckpointError("resume.principal_mismatch", `the resume config carries principal ${JSON.stringify(suppliedPrincipal)} but this checkpoint was suspended under principal ${JSON.stringify(rowPrincipal)} — ` +
4521
+ "running the resumed leg under a different identity would move its usage-ledger bucket, scope derivation and attribution; " +
4522
+ "refused pre-CAS (the checkpoint stays pending): re-resume with the original principal, or omit the field to inherit the recorded one");
4523
+ }
4524
+ const owesDelivery = outcomeGate !== "resource_limit" ||
4525
+ readPendingSteerQueue(cp.state).length > 0 ||
4526
+ (cp.state.runningBackgroundTasks?.length ?? 0) > 0;
4527
+ if (owesDelivery && this.deps.usageWindowStore !== undefined) {
4528
+ const preCasWindows = resolveUsageWindows(this.deps.usageWindows);
4529
+ if (preCasWindows !== undefined && preCasWindows.length > 0) {
4530
+ const ledgerKey = (suppliedPrincipal ?? rowPrincipal) || GLOBAL_USAGE_KEY;
4531
+ const wait = usageRetryAfterMs(await this.deps.usageWindowStore.read(ledgerKey, preCasWindows, Date.now()));
4532
+ if (wait !== undefined) {
4533
+ 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
+ `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 });
4535
+ }
4536
+ }
4537
+ }
4538
+ }
4458
4539
  if (plainPolicyOutcome !== undefined &&
4459
4540
  plainPolicyOutcome.decision === "allow" &&
4460
4541
  plainPolicyOutcome.updatedInput !== undefined &&
@@ -4571,6 +4652,7 @@ export class Runner {
4571
4652
  : undefined;
4572
4653
  const spec = {
4573
4654
  ...taskConfig,
4655
+ ...((taskConfig.principal || undefined) === undefined && (cp.principal || undefined) !== undefined ? { principal: cp.principal } : {}),
4574
4656
  objective: "",
4575
4657
  sessionId: cp.sessionId,
4576
4658
  requireExistingSession: true,
@@ -148,28 +148,28 @@ export interface ExternalNotificationInput {
148
148
  export interface SystemInjection<TPayload = unknown> {
149
149
  kind: "task_notification";
150
150
  /**
151
- * design/116 §7 in THIS engine every priority delivers at the NEXT turn boundary via
152
- * `harness.steer()`, mid-work included, in ARRIVAL order (consecutive frames batch); `priority`
153
- * affects only the park/uplink path. A delivery that races the agent going idle parks on
154
- * PendingSessionNotifications for the session's next run; `drain()` serves that parked lane.
151
+ * design/373the injection ladder is LIVE (all three values carry delivery semantics, the CC
152
+ * 2.1.223 form; the flat "every priority delivers at the next boundary" era ended with this
153
+ * design its H-1 anchor-correction note is preserved in the design archive, and the ruled
154
+ * changes it named are exactly what landed here):
155
155
  *
156
- * **Anchor correction (backlog #389 伴生 / hallucination audit H-1).** The 2026-08-05 re-anchor
157
- * justified flattening the ladder with "CC's queued task-notification inputs are UNCONDITIONALLY
158
- * deliverable at the boundary (CC 2.1.221)". That sentence is FALSE as a statement about CC, on
159
- * 221 and 223 alike: the mid-turn fold is gated at `getCommandsByMaxPriority("next")`
160
- * (`pretty221.js:449195` / `pretty223.js:415586`), which admits `now`+`next` and EXCLUDES `later`
161
- * and `enqueuePendingNotification` defaults to `later`. CC's background-completion notices fold
162
- * mid-turn because they explicitly say `priority:"next"`; its ultraplan/artifact notices take the
163
- * default and deliberately do NOT. So all three of CC's values carry live delivery semantics
164
- * (`now` = abort the running turn, `next` = fold into it, `later` = wait for the next one).
156
+ * - `"next"` the running turn's NEXT boundary (mid-work included), arrival order, consecutive
157
+ * engine-note frames batch. The pre-373 behavior, byte-identical (the regression baseline).
158
+ * - `"later"` never folded into the work in progress: delivered at the run's natural
159
+ * would-otherwise-stop seat (its own closing turn). A run that never reaches that seat
160
+ * (abort/maxTurns) re-pends the frame per session — the session's NEXT run delivers it at
161
+ * turn-open. Structural honesty note: once parked, delivery depends on a next run HAPPENING
162
+ * (this engine does not own an idle process the way the CC client does) — a recorded
163
+ * weakening, not a bug.
164
+ * - `"now"` `"next"`'s delivery guarantee PLUS the boundary is manufactured early: on the
165
+ * notification lane the frame takes the queue's class head and the earliest natural boundary
166
+ * (NO turn interrupt — interrupt authority belongs exclusively to the caller-provenance
167
+ * steer face, `TaskStream.steer({ priority: "now" })`; the external `notify()` verb REFUSES
168
+ * `"now"` typed). An unknown value is refused at every entry (bad-value loudness).
165
169
  *
166
- * The engineering conclusion the re-anchor reached a background completion must reach a busy
167
- * model at the boundary rather than starve behind a "deliver only when it would otherwise stop"
168
- * rule stands on its own. What does not stand is the claim that CC has no ladder. Restoring the
169
- * `later` = "do not fold into the running turn" arm is a behavior-face change and `now` = "abort
170
- * the running turn" is a new capability; both are ruled changes, not silent ones. Until then the
171
- * gap is DISCLOSED at the injection funnel rather than left as a silently inert knob (`now` is
172
- * announced, an unknown value is refused) — the bad-value loudness rule.
170
+ * Park/uplink: the pend store carries the frame's priority ({@link PendingSessionNotifications})
171
+ * and the turn-open batch delivers priority-major (pend order within a class); a record with no
172
+ * carried priority ranks AS `later` and its wire field stays honestly absent.
173
173
  */
174
174
  priority: SystemInjectionPriority;
175
175
  dedupKey: string;
@@ -180,6 +180,13 @@ export interface SystemInjection<TPayload = unknown> {
180
180
  * fire-and-forget producer (no receipt to honor). */
181
181
  onDisposition?: (d: "queued" | "parked") => void;
182
182
  }
183
+ /** design/373 (adversarial r3, recorded rule): the key deliberately EXCLUDES the injection tier.
184
+ * The key names an EVENT's identity; `priority` is delivery metadata about one submission of it.
185
+ * A repeat under an in-flight key therefore FOLDS regardless of the repeat's tier — the tier of
186
+ * record is the FIRST accept's, and the folded caller's "queued" receipt is true of that standing
187
+ * entry. A producer that wants a NEW occurrence delivered under a different tier mints a fresh
188
+ * `seq` (the documented repeat discipline); silently migrating a queued frame between lanes on a
189
+ * repeat, or refusing the repeat typed, would each turn a dedup fold into a delivery mutation. */
183
190
  export declare function taskNotificationDedupKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type" | "status" | "seq">): string;
184
191
  /**
185
192
  * RB-142 — the lane-scoped identity of a task, extracted from {@link taskNotificationDedupKey} so every
@@ -247,9 +254,27 @@ export declare const MAX_PENDING_SESSIONS = 100;
247
254
  * preference applies, so "terminal" can never mean two things in this module.
248
255
  */
249
256
  export declare function isDelegatedAgentTerminal(n: Pick<TaskNotificationPayload, "task_type" | "status">): boolean;
257
+ /**
258
+ * design/373 (#445 saturation prerequisite) — is this frame a TERMINAL notification (any lane)?
259
+ * Reuses {@link TERMINAL_STATUSES} — the same terminal-vs-event rule the pending store's eviction
260
+ * preference applies, so "terminal" can never mean two things in this module. The runner marks
261
+ * INTERNAL-lane terminal frames cap-preferred at the delivery queue's mouth: a watcher's event
262
+ * storm may delay its own batches, never crowd a completion out of the run waiting on it. The
263
+ * external lane never gets the preference (its `status` is caller-supplied — an untrusted injector
264
+ * must not be able to claim the internal completions' reservation); this predicate itself stays
265
+ * lane-blind, the trust cut is the marking site's.
266
+ */
267
+ export declare function isTerminalTaskNotification(n: Pick<TaskNotificationPayload, "status">): boolean;
250
268
  export interface DrainedPendingNotifications {
251
- /** Chronological (pend order) payloads still held when the session's next run drained. */
269
+ /** Payloads still held when the session's next run drained. design/373 §3.5: batch order is
270
+ * PRIORITY-MAJOR (now < next < later; a record with no carried priority ranks AS later —
271
+ * the conservative seat), pend order (chronological) within a class — the idle-dequeue form. */
252
272
  items: TaskNotificationPayload[];
273
+ /** design/373 §3.5 — the parked priority per item, keyed by payload identity, present exactly for
274
+ * records whose pend CARRIED one. A missing key is a fact (a tier-unknown park — e.g.
275
+ * the harness's undrained sweep hands payloads back without their lane's priority): the wire
276
+ * field stays honestly absent rather than fabricating `later`. */
277
+ priorities?: Map<TaskNotificationPayload, SystemInjectionPriority>;
253
278
  /** task_id → notifications evicted by the bounds while pending (never delivered). `taskType` remembers
254
279
  * the victim's lane so a survivors-none disclosure can still render an honest synthetic payload. */
255
280
  /** RB-142: keyed by {@link taskNotificationLaneKey}, NOT the bare `task_id` — the external lane's ids are
@@ -288,8 +313,10 @@ export declare class PendingSessionNotifications {
288
313
  /** RB-143: whole-session losses whose tombstone was itself evicted — the count survives, the attribution
289
314
  * does not. Surfaced on the next session-level disclosure so it is never simply forgotten. */
290
315
  private unattributedDrops;
291
- pend(sessionId: string, n: TaskNotificationPayload): void;
292
- /** Remove and return the session's pendings (one-shot — the next run consumes them exactly once). */
316
+ pend(sessionId: string, n: TaskNotificationPayload, priority?: SystemInjectionPriority): void;
317
+ /** Remove and return the session's pendings (one-shot — the next run consumes them exactly once).
318
+ * design/373 §3.5: the batch comes out PRIORITY-MAJOR (stable sort — pend order within a class;
319
+ * a no-priority record ranks as later), the idle-dequeue min-value-first form. */
293
320
  drain(sessionId: string): DrainedPendingNotifications | undefined;
294
321
  get size(): number;
295
322
  }
@@ -92,12 +92,15 @@ const TERMINAL_STATUSES = new Set(["completed", "failed", "killed", "cancelled"]
92
92
  export function isDelegatedAgentTerminal(n) {
93
93
  return n.task_type === "background_agent" && TERMINAL_STATUSES.has(n.status);
94
94
  }
95
+ export function isTerminalTaskNotification(n) {
96
+ return TERMINAL_STATUSES.has(n.status);
97
+ }
95
98
  export class PendingSessionNotifications {
96
99
  sessions = new Map();
97
100
  droppedSessions = 0;
98
101
  evictedSessions = new Map();
99
102
  unattributedDrops = 0;
100
- pend(sessionId, n) {
103
+ pend(sessionId, n, priority) {
101
104
  let s = this.sessions.get(sessionId);
102
105
  if (s === undefined) {
103
106
  if (this.sessions.size >= MAX_PENDING_SESSIONS) {
@@ -119,7 +122,7 @@ export class PendingSessionNotifications {
119
122
  }
120
123
  }
121
124
  }
122
- s = { items: [], dropped: new Map(), keys: new Set() };
125
+ s = { items: [], dropped: new Map(), keys: new Set(), priorities: new Map() };
123
126
  this.sessions.set(sessionId, s);
124
127
  }
125
128
  const key = taskNotificationDedupKey(n);
@@ -127,6 +130,8 @@ export class PendingSessionNotifications {
127
130
  return;
128
131
  s.keys.add(key);
129
132
  s.items.push(n);
133
+ if (priority !== undefined)
134
+ s.priorities?.set(n, priority);
130
135
  const lane = taskNotificationLaneKey(n);
131
136
  let mine = 0;
132
137
  for (const i of s.items)
@@ -139,8 +144,14 @@ export class PendingSessionNotifications {
139
144
  }
140
145
  drain(sessionId) {
141
146
  const s = this.sessions.get(sessionId);
142
- if (s !== undefined)
147
+ if (s !== undefined) {
143
148
  this.sessions.delete(sessionId);
149
+ const rank = (n) => {
150
+ const p = s.priorities?.get(n);
151
+ return p === "now" ? 0 : p === "next" ? 1 : 2;
152
+ };
153
+ s.items = [...s.items].sort((a, b) => rank(a) - rank(b));
154
+ }
144
155
  const tomb = this.evictedSessions.get(sessionId);
145
156
  if (tomb === undefined && this.unattributedDrops === 0)
146
157
  return s;
@@ -163,6 +174,7 @@ function evictOldest(s, match) {
163
174
  return;
164
175
  const victim = s.items.splice(idx, 1)[0];
165
176
  s.keys.delete(taskNotificationDedupKey(victim));
177
+ s.priorities?.delete(victim);
166
178
  const laneKey = taskNotificationLaneKey(victim);
167
179
  const prior = s.dropped.get(laneKey);
168
180
  s.dropped.set(laneKey, { count: (prior?.count ?? 0) + 1, taskType: victim.task_type, taskId: victim.task_id });
@@ -190,10 +202,14 @@ export function discloseDroppedPending(drained) {
190
202
  if (dropped === undefined || disclosed.has(lane))
191
203
  return n;
192
204
  disclosed.add(lane);
193
- return {
205
+ const annotated = {
194
206
  ...n,
195
207
  summary: `[${n.task_id}] ${dropped.count} earlier pending notification(s) from this task were dropped (pending-queue overflow). ${n.summary}`,
196
208
  };
209
+ const priority = drained.priorities?.get(n);
210
+ if (priority !== undefined)
211
+ drained.priorities?.set(annotated, priority);
212
+ return annotated;
197
213
  });
198
214
  for (const [lane, dropped] of drained.dropped) {
199
215
  if (disclosed.has(lane))
@@ -413,8 +413,14 @@ export interface ToolPolicyProjection {
413
413
  *
414
414
  * Exported for the OTHER name-keyed lane in this engine — the persisted session/ancestor rules in
415
415
  * `session-rule-policy.ts`. Not part of the public API (`src/index.ts` re-exports by name and does not
416
- * list these): all four publishers of {@link ToolPolicyNameSets} must resolve a covering entry the same
417
- * way, because the prepare-time audit's exemption for these spellings speaks for all of them at once.
416
+ * list these two): every publisher of {@link ToolPolicyNameSets} must resolve a covering entry the same
417
+ * way, because the prepare-time audit's exemption for these spellings speaks for ALL published name sets
418
+ * at once — the four in-engine lanes do so by sharing this module's matcher. #450: that premise is NOT
419
+ * structural for an OUT-OF-ENGINE publisher (a host compiling its own settings into an exact-match ask
420
+ * closure is fail-open on these spellings while the audit stays silent) — which is why the underlying
421
+ * judgment predicates (`isNamespacedCoveringRuleName` / `namespacedRuleNameCovers`) ARE exported from
422
+ * the package index: an external lane either compiles through `createPermissionRulePolicy` or
423
+ * self-checks with those.
418
424
  */
419
425
  export declare function namespacedCoveringEntries(entries: readonly string[] | undefined): readonly string[];
420
426
  /** Does any covering entry reach `toolName`? See {@link namespacedCoveringEntries}. */