@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
@@ -1,4 +1,5 @@
1
1
  import type { McpDroppedTool } from "../mcp.js";
2
+ import type { TaskSpec } from "../types.js";
2
3
  import { type SkillListingEntry } from "./synthetic-tools.js";
3
4
  /**
4
5
  * design/133 §R3 — turn-boundary attachment seam: the PURE producer layer (CC `getContextAttachments`
@@ -28,6 +29,44 @@ export declare const TOOL_SEARCH_REMINDER_CONFIG: {
28
29
  readonly EVERY_N_TURNS: 15;
29
30
  readonly MAX_NAMES: 10;
30
31
  };
32
+ /**
33
+ * RB-318 — the `total_tokens_reminder` mode set, CC 2.1.245-VERBATIM and in CC's declaration order
34
+ * (bundle `R9o`; the settings schema declares the same five for its `totalTokensReminder` key).
35
+ * Each arm names what the rendered number IS, per CC's own schema prose:
36
+ * - `off` — the lane is silent. Kept as a member rather than folded into the opt-in flag because it
37
+ * is the DEPLOYMENT-RESOLVED kill switch (CC resolves the mode from env
38
+ * `CLAUDE_CODE_TOTAL_TOKENS_REMINDER` → settings → gate; sema keeps env resolution at the shell and
39
+ * carries only the resolved mode — the {@link AttachmentInputs.config.todoReminderMode} contract
40
+ * verbatim, where "wired on" and "silenced by policy" are also two independent bits).
41
+ * - `infinite` — the literal text `Infinite`.
42
+ * - `fixed` — the constant {@link TOTAL_TOKENS_FIXED_ARM_VALUE}. Together with `infinite` these are
43
+ * CC's two CONSTANT arms: they consume no measurement at all, which is exactly why they exist
44
+ * (they are the controls the countdown arms are measured against).
45
+ * - `countdown` — the live remaining budget, raw.
46
+ * - `padded-countdown` — the same, smoothed by a monotone floor so the number never jumps back.
47
+ */
48
+ export declare const TOTAL_TOKENS_REMINDER_MODES: readonly ["off", "infinite", "fixed", "countdown", "padded-countdown"];
49
+ /**
50
+ * RB-318 rider — the closed set is spelled TWICE (the public `TaskSpec.attachments.
51
+ * totalTokensReminderMode` literal union and this array), and only the TaskSpec→array direction had
52
+ * a compile chain (runtask's AttachmentInputs assignment). The config door widens this array to
53
+ * `readonly string[]` before `includes`, so a member added HERE and never to the public union used
54
+ * to compile clean, pass the door, and render — while the public type declared it illegal. Both
55
+ * directions now refuse at compile time (the governance-codes `satisfies` lockstep form):
56
+ * - array ⊆ TaskSpec — the `satisfies` on the declaration above;
57
+ * - TaskSpec ⊆ array — the conditional below collapses the exported type to `never` (the default-
58
+ * mode constant two lines down errors immediately) when the public union grows an unlisted member.
59
+ */
60
+ type TaskSpecTotalTokensReminderMode = NonNullable<NonNullable<TaskSpec["attachments"]>["totalTokensReminderMode"]>;
61
+ export type TotalTokensReminderMode = [TaskSpecTotalTokensReminderMode] extends [(typeof TOTAL_TOKENS_REMINDER_MODES)[number]] ? (typeof TOTAL_TOKENS_REMINDER_MODES)[number] : never;
62
+ /** RB-318 — CC 2.1.245's own default ("Defaults to padded-countdown"). The 2026-07-30 registration of
63
+ * this lane rested on the 2.1.220 premise "OFF by default even in 220"; 245 flipped it, which is the
64
+ * fact that reopened the ruling. Applies only ONCE the deployment has opted the lane in. */
65
+ export declare const TOTAL_TOKENS_REMINDER_DEFAULT_MODE: TotalTokensReminderMode;
66
+ /** RB-318 — CC's `fixed` arm constant (bundle `A9o`). A fixed anchor is the POINT of that arm (the
67
+ * number is meant not to move), so it is copied as the constant it is rather than re-pointed at a
68
+ * declared ceiling — a deployment that wants its own number picks a countdown arm instead. */
69
+ export declare const TOTAL_TOKENS_FIXED_ARM_VALUE = 5000000;
31
70
  /** F5: per-boundary stat scan upper bound — at most this many most-recently-read files are stat'ed. */
32
71
  export declare const CHANGED_FILES_MAX = 20;
33
72
  /**
@@ -39,7 +78,7 @@ export declare const CHANGED_FILES_MAX = 20;
39
78
  export declare const CHANGED_FILES_MTIME_EPS_MS = 2000;
40
79
  /** Per-boundary payload byte cap over the whole bundle (weak-model context economics). */
41
80
  export declare const ATTACHMENT_BYTE_CAP: number;
42
- export type AttachmentSource = "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "workflow_size_guideline_change" | "budget_usd" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
81
+ export type AttachmentSource = "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "workflow_size_guideline_change" | "budget_usd" | "total_tokens_reminder" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
43
82
  /** G1 续批 — one agent type on the delegation tool's roster (ToolSpec.agentListing, filled by
44
83
  * createSubagentTool: defined agents + the built-in fork when offered). */
45
84
  export interface AgentListingEntry {
@@ -164,6 +203,33 @@ export interface AttachmentState {
164
203
  * agent frame permanently starving the skills frame (each frame individually fits the cap by
165
204
  * construction — agent roster ≤ cap by deployment size, skills block ≤ SKILLS_BLOCK_MAX_BYTES). */
166
205
  skillsListingStarved?: boolean;
206
+ /**
207
+ * RB-318 — the `padded-countdown` arm's SMOOTHING FLOOR: the highest usage figure this run has
208
+ * already published against. CC's `cumulativeUsed` (bundle: `Math.max(storedFloor, rolledOver +
209
+ * used − anchor)`, stored back) exists so the countdown never RUNS BACKWARDS when the live usage
210
+ * coordinate drops under it — a model that watched its budget shrink and then saw it grow again
211
+ * learns the number is noise.
212
+ *
213
+ * Two of CC's three maps are structurally degenerate here and that is a mapping, not a loss:
214
+ * - the per-agent KEY — CC's tracker is session-scoped and keyed by `agentId ?? "main"` because one
215
+ * CC session hosts the main agent and its subagents; a sema {@link AttachmentState} is already
216
+ * per-RUN, and a delegated child is its own run with its own state, so this scalar IS that slot.
217
+ * - the ANCHOR (`reanchorTaskBudget`, re-armed on every regular user prompt so a new user task
218
+ * counts down from the full budget again) — a sema LEG is one task epoch, so the anchor is fixed
219
+ * at 0 for the leg's whole life and the epoch reset is the next `runTask` invocation — a
220
+ * durable-resume leg INCLUDED (it re-enters through `runTask`, rebuilds this state, re-derives
221
+ * its slice window and restarts the spend coordinate, so its countdown re-anchors to the new
222
+ * leg's real remaining — the CC re-anchor arm, not a floor violation; the floor deliberately
223
+ * does NOT ride the checkpoint, since a carried floor against a fresh slice window would freeze
224
+ * the readout at "0 tokens left" over an allowance the run genuinely holds). Same lifecycle
225
+ * reasoning as the plan_mode lane's `countPlanModeAttachmentsSinceLastExit` note.
226
+ * - the ROLLOVER (`rollOverContext`, which adds each compacted-away context back so the count keeps
227
+ * advancing) is likewise not owed: sema feeds a CUMULATIVE spend coordinate (`stats.tokens`),
228
+ * which a compaction never rewinds, where CC feeds the live context size.
229
+ * `undefined` = the padded arm has not rendered yet (an OFF lane, or a raw-countdown one, never
230
+ * allocates it).
231
+ */
232
+ totalTokensFloor?: number;
167
233
  }
168
234
  /**
169
235
  * A1-R — the date_change lane's OWN state, deliberately NOT an
@@ -314,6 +380,25 @@ export interface AttachmentInputs {
314
380
  used: number;
315
381
  total: number;
316
382
  };
383
+ /** RB-318 total_tokens_reminder lane (裁 B, 2026-08-26) — the TOKEN twin of {@link budgetUsd}, fed
384
+ * from the same place for the same reason: `used` is the run's cumulative token spend
385
+ * (`stats.tokens`) and `total` is the ceiling the budget gate enforces against it
386
+ * (`rs.budget.maxTokensWindow` = min(`limits.maxTokens`, the cross-slice allocation's remainder)),
387
+ * so a model-visible countdown can never disagree with an eventual `limits.max_tokens_exceeded`.
388
+ * Core neither estimates nor discovers this pair: no ceiling declared ⇒ absent ⇒ the countdown arms
389
+ * are silent (CC's own `maxBudgetUsd === void 0 → []` posture on the sibling lane).
390
+ *
391
+ * COORDINATE NOTE (candidate divergence registration): CC's two countdown arms read two DIFFERENT
392
+ * sources — `countdown` the live remaining context window, `padded-countdown` a synthetic task
393
+ * budget (its default 15_000_000 is far larger than any real window). sema carries ONE pair, the
394
+ * enforced spend coordinate, and lets the mode pick the arithmetic over it: publishing a context
395
+ * window would mean publishing a figure core cannot make true (BYOM — the window, the tokenizer and
396
+ * the accounting belong to the deployment's model), while the enforced ceiling is the one token
397
+ * number core owns end to end. */
398
+ totalTokens?: {
399
+ used: number;
400
+ total: number;
401
+ };
317
402
  config: {
318
403
  todoReminder: boolean;
319
404
  /** 口径③ — CC `r2o()` kill-switch parity (pretty.js:479167-479171; consumed by both producers at
@@ -349,6 +434,36 @@ export interface AttachmentInputs {
349
434
  * through a workflow governance baseline (`WorkflowGovernanceBaseline.base`, which explicitly
350
435
  * snapshots `maxCostUsd` and every other governance field) reaches the lane. */
351
436
  budgetUsd?: boolean;
437
+ /**
438
+ * RB-318 (裁 B, 2026-08-26) — `total_tokens_reminder` (CC 2.1.245 producer `MPs`, renderer `Oje`,
439
+ * registered in the SHARED producer group beside `budget_usd`, so a subagent carrying its own
440
+ * ceiling sees it too — the RB-311 attribution verbatim). Opt-in, default OFF like every other 133
441
+ * member: an un-opted deployment renders no reminder frame — the ATTACHMENT lane itself is
442
+ * byte-identical to the pre-lane engine. (Scoped deliberately to this lane: registering
443
+ * `<total_tokens>` in ENGINE_ENVELOPES changed the SANITIZE/DISCLOSURE faces for every
444
+ * deployment, opted or not — untrusted text carrying that spelling is now defused on the fenced
445
+ * lanes and envelope-shaped external data trips the design/319 disclosure tail. That is the
446
+ * defensive direction and unconditional by design: the envelope family must be un-forgeable
447
+ * whether or not anyone renders the real frame.)
448
+ *
449
+ * WHY IT EXISTS NOW. The 2026-07-30 registration refused it as a product-face question and rested
450
+ * on "OFF by default even in 220". 245 flips exactly that premise — its settings schema reads
451
+ * "Defaults to padded-countdown", i.e. upstream now publishes a token countdown to the model by
452
+ * default. The BYOM honesty objection is answered by SHAPE rather than by dropping it: core
453
+ * estimates nothing. The numbers are the ones the engine already enforces against
454
+ * ({@link AttachmentInputs.totalTokens}), the ceiling is the deployment's own declared
455
+ * `limits.maxTokens`, and with no ceiling declared the countdown arms simply never fire.
456
+ *
457
+ * The arm is chosen by {@link totalTokensReminderMode} (default {@link
458
+ * TOTAL_TOKENS_REMINDER_DEFAULT_MODE}); `off` there silences the lane without un-wiring it.
459
+ */
460
+ totalTokensReminder?: boolean;
461
+ /** RB-318 — the RESOLVED mode (see {@link TOTAL_TOKENS_REMINDER_MODES}). CC resolves it from env /
462
+ * settings / a server gate; sema keeps env resolution at the deployment shell and carries only the
463
+ * resolved value, exactly like {@link todoReminderMode}. Absent ⇒ {@link
464
+ * TOTAL_TOKENS_REMINDER_DEFAULT_MODE}. A value outside the closed set is refused at the config
465
+ * door (`config.attachment_invalid`), never folded to the default. */
466
+ totalTokensReminderMode?: TotalTokensReminderMode;
352
467
  /** G1: post-compact background-task announce (opt-in, default OFF like the 133 members). */
353
468
  backgroundTasks?: boolean;
354
469
  /** G1: deferred-tool materialization announce (design/36 setTools delta → boundary notice). */
@@ -441,10 +556,11 @@ export interface AttachmentInputs {
441
556
  * done HERE because the caller only invokes this once per boundary after its injection gate passed —
442
557
  * splitting "decide" from "mark sent" would just invite a drift between the two.
443
558
  *
444
- * Returns attachments in EVIDENCE-STRENGTH order (todo/task → plan_mode budget_usd →
445
- * background_taskstools_deltaagent_listingskills_listingmcp_instructionschanged_files), which is also the §R3 MED-6
446
- * truncation order read backwards: under the 8KB cap changed_files is sacrificed first, the todo/task
447
- * list last. The G1/续批 members are registry/engine ground truth (strong), but the
559
+ * Returns attachments in EVIDENCE-STRENGTH order (todo/task → tool_search_usage_reminder
560
+ * plan_modebudget_usdtotal_tokens_reminderbackground_taskstools_deltaagent_listing
561
+ * skills_listing mcp_instructions mcp_dropped_tools changed_files), which is also the §R3
562
+ * MED-6 truncation order read backwards: under the 8KB cap changed_files is sacrificed first, the
563
+ * todo/task list last. The G1/续批 members are registry/engine ground truth (strong), but the
448
564
  * todo/plan lane keeps priority: it carries the run's own work-tracking state, which the model can
449
565
  * least afford to lose.
450
566
  */
@@ -528,6 +644,21 @@ export declare function commitInstructionsChange(state: InstructionsChangeState,
528
644
  contentHash: string | null;
529
645
  }>): void;
530
646
  export declare function renderBudgetUsd(used: number, total: number): string;
647
+ /**
648
+ * RB-318 — CC 2.1.245 `Oje` VERBATIM: the whole body is the one-line envelope
649
+ * `<total_tokens>N tokens left</total_tokens>`, where N is the arm's own substitution — the literal
650
+ * `Infinite`, the {@link TOTAL_TOKENS_FIXED_ARM_VALUE} constant, or the remaining count clamped at
651
+ * zero (CC's `Math.max(0, …)`: an over-spent run reads `0 tokens left`, never a negative number).
652
+ *
653
+ * `remaining` is ignored by the two constant arms — the caller passes whatever it has (the producer
654
+ * passes 0), matching CC, whose own producer computes `0` for them.
655
+ *
656
+ * The `<total_tokens>` tag is an ENGINE-MINTED authority envelope: it is registered in the
657
+ * `ENGINE_ENVELOPES` census (untrusted-text.ts) and is in the fenced family, so the same spelling
658
+ * arriving inside untrusted content is defused rather than read as this readout. Its own body needs no
659
+ * escaping in return — every byte here is engine copy or a finite number.
660
+ */
661
+ export declare function renderTotalTokensReminder(mode: TotalTokensReminderMode, remaining: number): string;
531
662
  /**
532
663
  * SR-7 — CC 2.1.198 VERBATIM inner text (bundle pretty.js:698391-698398, `F6c`): the aggregated
533
664
  * orphaned-background-task notice a restarted/resumed leg injects ONCE (CC's own `<system-reminder>`
@@ -829,3 +960,4 @@ export declare function selectMcpDroppedBatch<T extends McpDroppedTool>(entries:
829
960
  * is unchanged: this is a pure function of (string, number).
830
961
  */
831
962
  export declare function clipToBytes(s: string, maxBytes: number): string;
963
+ export {};
@@ -15,15 +15,21 @@ export const TOOL_SEARCH_REMINDER_CONFIG = {
15
15
  EVERY_N_TURNS: 15,
16
16
  MAX_NAMES: 10,
17
17
  };
18
+ export const TOTAL_TOKENS_REMINDER_MODES = ["off", "infinite", "fixed", "countdown", "padded-countdown"];
19
+ export const TOTAL_TOKENS_REMINDER_DEFAULT_MODE = "padded-countdown";
20
+ export const TOTAL_TOKENS_FIXED_ARM_VALUE = 5_000_000;
18
21
  export const CHANGED_FILES_MAX = 20;
19
22
  export const CHANGED_FILES_MTIME_EPS_MS = 2000;
20
23
  export const ATTACHMENT_BYTE_CAP = 8 * 1024;
21
24
  const PROJECTION_ITEMS_MAX = 50;
22
25
  const PROJECTION_CONTENT_MAX = 80;
23
26
  const ATTACHMENT_TAGS_DEFAULT = [...SHELLED_BODY_ENVELOPE_TAGS];
24
- const ATTACHMENT_TAGS_SKILLS_OWNER = SHELLED_BODY_ENVELOPE_TAGS.filter((t) => t !== "skills");
27
+ const ATTACHMENT_TAGS_OWNED = {
28
+ skills_listing: SHELLED_BODY_ENVELOPE_TAGS.filter((t) => t !== "skills"),
29
+ total_tokens_reminder: SHELLED_BODY_ENVELOPE_TAGS.filter((t) => t !== "total_tokens"),
30
+ };
25
31
  export function attachmentEnvelopeTags(source) {
26
- return source === "skills_listing" ? ATTACHMENT_TAGS_SKILLS_OWNER : ATTACHMENT_TAGS_DEFAULT;
32
+ return ATTACHMENT_TAGS_OWNED[source] ?? ATTACHMENT_TAGS_DEFAULT;
27
33
  }
28
34
  export const INSTRUCTIONS_CHANGE_BYTE_CAP = 512;
29
35
  export function createAttachmentState() {
@@ -166,6 +172,19 @@ export function collectDueAttachments(state, inp) {
166
172
  if (inp.config.budgetUsd && inp.budgetUsd !== undefined) {
167
173
  (out ??= []).push({ source: "budget_usd", body: renderBudgetUsd(inp.budgetUsd.used, inp.budgetUsd.total) });
168
174
  }
175
+ if (inp.config.totalTokensReminder === true) {
176
+ const mode = inp.config.totalTokensReminderMode ?? TOTAL_TOKENS_REMINDER_DEFAULT_MODE;
177
+ if (mode === "infinite" || mode === "fixed") {
178
+ (out ??= []).push({ source: "total_tokens_reminder", body: renderTotalTokensReminder(mode, 0) });
179
+ }
180
+ else if ((mode === "countdown" || mode === "padded-countdown") && inp.totalTokens !== undefined) {
181
+ const { used, total } = inp.totalTokens;
182
+ if (Number.isFinite(used) && Number.isFinite(total) && used >= 0 && total >= 0) {
183
+ const effectiveUsed = mode === "padded-countdown" ? (state.totalTokensFloor = Math.max(state.totalTokensFloor ?? 0, used)) : used;
184
+ (out ??= []).push({ source: "total_tokens_reminder", body: renderTotalTokensReminder(mode, total - effectiveUsed) });
185
+ }
186
+ }
187
+ }
169
188
  if (inp.config.backgroundTasks && state.postCompactPending) {
170
189
  state.postCompactPending = false;
171
190
  if (inp.backgroundTasks !== undefined && inp.backgroundTasks.length > 0) {
@@ -362,6 +381,10 @@ export function commitInstructionsChange(state, announced) {
362
381
  export function renderBudgetUsd(used, total) {
363
382
  return `USD budget: $${used}/$${total}; $${total - used} remaining`;
364
383
  }
384
+ export function renderTotalTokensReminder(mode, remaining) {
385
+ const shown = mode === "infinite" ? "Infinite" : mode === "fixed" ? TOTAL_TOKENS_FIXED_ARM_VALUE : Math.max(0, remaining);
386
+ return `<total_tokens>${shown} tokens left</total_tokens>`;
387
+ }
365
388
  export function renderOrphanedBackgroundTasks(tasks) {
366
389
  const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
367
390
  return (`The container was restarted. The following background tasks were running and are now stopped:\n` +
@@ -212,6 +212,25 @@ export async function checkpointStoreContract(make, runAssertion) {
212
212
  const stored = (await store.get(bitless.token)).pendingAction;
213
213
  assert.equal("hasBidiControls" in stored, false, "projection backfill must not write back to the row");
214
214
  });
215
+ run("#457 previewWithheld three-form matrix: a row's spelling survives + projects; absent stays absent; an out-of-contract spelling reads as absent", async () => {
216
+ const store = make();
217
+ const oversize = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "pw-oversize" });
218
+ oversize.pendingAction.previewWithheld = "oversize";
219
+ const unavailable = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "pw-unavailable" });
220
+ unavailable.pendingAction.previewWithheld = "unavailable";
221
+ const absent = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "pw-absent" });
222
+ const bogus = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "pw-bogus" });
223
+ bogus.pendingAction.previewWithheld = "shrugged";
224
+ for (const cp of [oversize, unavailable, absent, bogus])
225
+ await store.put(cp.token, cp);
226
+ const back = (await store.get(oversize.token)).pendingAction;
227
+ assert.equal(back.previewWithheld, "oversize", "the row's previewWithheld must survive the round-trip verbatim");
228
+ const byId = new Map((await store.listByScope("tenant-a")).map((s) => [s.sessionId, s]));
229
+ assert.equal(byId.get("pw-oversize")?.previewWithheld, "oversize");
230
+ assert.equal(byId.get("pw-unavailable")?.previewWithheld, "unavailable");
231
+ assert.equal("previewWithheld" in byId.get("pw-absent"), false, "absent must project the key OMITTED, not null/empty");
232
+ assert.equal("previewWithheld" in byId.get("pw-bogus"), false, "an out-of-contract spelling must read as absent, never project verbatim");
233
+ });
215
234
  await settle();
216
235
  }
217
236
  const sortByToken = (a, b) => a.token.localeCompare(b.token);
@@ -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))
@@ -118,7 +118,8 @@ export type WorkerErrorClass = "budget" | "limit" | "output" | "suspend" | "revi
118
118
  * The `limits.` namespace splits into the `budget` and `limit` classes by exact code — see
119
119
  * {@link EXACT_CODE_CLASS}. Operation-level dotted codes that are surfaced to a *caller* and never become a
120
120
  * task outcome are deliberately OUT of scope: `steering.*` (`steering.not_running`/`steering.invalid_content`/
121
- * `steering.duplicate_input_id`, rejected to the `steer()` caller) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
121
+ * `steering.duplicate_input_id`/`steering.blocked_by_hook`, rejected to the `steer()` caller the fourth also
122
+ * rides the resume face as a `CheckpointError` code) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
122
123
  * reaches `TaskResult.errorCode`, so a caller will never pass them here. A genuinely unmapped code → `"unknown"` (which therefore means
123
124
  * "known-but-foreign or no code", e.g. a leaked fs/Node code, NOT "an OUR terminal class we forgot to add").
124
125
  */
@@ -853,6 +853,33 @@ export interface AskRequest {
853
853
  * with contextual escaping and show {@link args} alongside — the preview can misrepresent the
854
854
  * executable action and never replaces args. Never adjudication input. */
855
855
  readonly preview?: unknown;
856
+ /**
857
+ * #457 ② — PRESENT ⇔ this tool DECLARES an approval preview and the gate could not present a usable
858
+ * one, naming which way it failed: `"oversize"` (produced, but past the 16KiB display bound — the
859
+ * truncation stub still rides {@link preview}) or `"unavailable"` (the projection threw, or its value
860
+ * has no JSON serialization, so nothing was produced at all).
861
+ *
862
+ * ABSENT is the ordinary world and covers three cases that are NOT withholdings: the tool declares no
863
+ * preview, the projection declined to speak for these args, and the projection succeeded. A surface
864
+ * must therefore read presence, never absence: absence is not a claim that the preview is faithful.
865
+ *
866
+ * A DISCLOSURE, not a verdict and not a transform — the same posture as {@link hasBidiControls}. Core
867
+ * refuses nothing and narrows nothing on account of it: {@link args} and {@link boundInputHash} are
868
+ * always carried, so unlike a surface whose only content IS the projection, a core approval card can
869
+ * still show the person exactly what will run. What the bit buys is the honest sentence a card owes
870
+ * when it is rendering less than the tool meant it to — and the ground for a surface that chooses to
871
+ * withhold its own PERSISTENT "allow and stop asking me this" affordance while the action cannot be
872
+ * reviewed as intended (upstream's form: a withheld preview vetoes the standing row, leaving one-time
873
+ * approval only). Core deliberately does NOT apply that veto to {@link ruleOffers} itself — core's
874
+ * offers speak for shell COMMAND text, which rides `args` and is shown whatever the preview did.
875
+ *
876
+ * Filled at the ask MINT sites that resolve a preview (the synchronous gate leg), not at `resolveAsk`
877
+ * — the chokepoint receives an already-built request and has no tool table to project from. The
878
+ * durable park leg's twin is `PendingAction.tool_approval.previewWithheld` on the row, judged over
879
+ * that row's own fidelity-projected args snapshot; the two faces can honestly differ exactly as the
880
+ * bidi bit's two faces can.
881
+ */
882
+ readonly previewWithheld?: "oversize" | "unavailable";
856
883
  /** Canonical digest of {@link args} as presented in THIS ask, computed by the engine's one
857
884
  * `boundInputHashOf` — the same digest a durable park binds its checkpoint to, so an aggregating
858
885
  * approver can reconcile a synchronous ask row against a parked checkpoint row for the same call