@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
@@ -2596,7 +2596,7 @@ export class MemoryEngine {
2596
2596
  if (fresh.epoch !== epoch) {
2597
2597
  throw new ConsolidationRefusedError("memory.consolidation_stale_snapshot", `memory consolidation refused: the ${JSON.stringify(scope)} watermark advanced during the snapshot read — retake the snapshot.`);
2598
2598
  }
2599
- fresh.snapshot = { token: cycleToken, at, epoch, candidates: candidateRevs, eligible: eligibleRevs, marked };
2599
+ fresh.snapshot = { token: cycleToken, at, epoch, candidates: candidateRevs, eligible: eligibleRevs, marked, markedIds: candidates.filter((c) => c.marked).map((c) => c.entry.id) };
2600
2600
  let announce = false;
2601
2601
  if (opts.force !== undefined && fresh.lastForcedRequestId !== opts.force.requestId) {
2602
2602
  fresh.lastForcedRequestId = opts.force.requestId;
@@ -2656,6 +2656,9 @@ export class MemoryEngine {
2656
2656
  else if (p.inputs.length > screened.maxInputsPerProduct)
2657
2657
  reasons.push(`product #${i}: ${p.inputs.length} inputs > maxInputsPerProduct ${screened.maxInputsPerProduct}`);
2658
2658
  }
2659
+ if (proposal.mintExposure !== undefined && proposal.mintExposure !== "partitioned") {
2660
+ reasons.push(`mintExposure ${JSON.stringify(proposal.mintExposure)} is not a member of the closed attestation set {"partitioned"} — omit it (run-level fold) or spell it exactly`);
2661
+ }
2659
2662
  if (proposal.products.length === 0 && intents.length === 0) {
2660
2663
  reasons.push("empty plan: zero products and zero directed intents — a plan with nothing to apply must not settle as a completed run (the vacuous completion would stamp the eligible set into the fingerprint and blind the incremental face)");
2661
2664
  }
@@ -2741,19 +2744,43 @@ export class MemoryEngine {
2741
2744
  }
2742
2745
  const activeSetSize = headers.filter((h) => !superseded.has(h.id) && !exclusions.has(h.id)).length;
2743
2746
  const ceiling = supersessionFuseCeiling(activeSetSize, screened);
2747
+ const attested = proposal.mintExposure === "partitioned";
2748
+ if (attested) {
2749
+ const staleAxes = [];
2750
+ const baseline = snapshot.markedIds !== undefined ? new Set(snapshot.markedIds) : undefined;
2751
+ for (const [id, rev] of Object.entries(snapshot.candidates)) {
2752
+ const h = headerById.get(id);
2753
+ if (h === undefined)
2754
+ staleAxes.push(`served candidate ${id} left the committed listing`);
2755
+ else if (h.rev !== rev)
2756
+ staleAxes.push(`served candidate ${id} moved rev since the snapshot`);
2757
+ else if (exclusions.has(id))
2758
+ staleAxes.push(`served candidate ${id} entered the challenge/latch exclusion set`);
2759
+ else if (h.exposure !== undefined && (baseline === undefined || !baseline.has(id))) {
2760
+ staleAxes.push(baseline === undefined
2761
+ ? `served candidate ${id} is marked and the snapshot row predates the markedIds baseline — no proof the mark predates the mint (conservative arm)`
2762
+ : `served candidate ${id} turned marked since the snapshot`);
2763
+ }
2764
+ }
2765
+ if (staleAxes.length > 0) {
2766
+ throw new ConsolidationRefusedError("memory.consolidation_stale_snapshot", `memory consolidation refused: the attested (exposure-partitioned) mint's clean-arm premise is stale — the ${JSON.stringify(scope)} world moved between snapshot and freeze; retake the snapshot and re-mint`, staleAxes);
2767
+ }
2768
+ }
2744
2769
  let visibleMarked = snapshot.marked;
2745
- if (!visibleMarked) {
2770
+ if (!attested && !visibleMarked) {
2746
2771
  const inputIds = [...new Set(proposal.products.flatMap((p) => p.inputs.map((i2) => i2.id)))];
2747
2772
  const committedInputs = await face.getByIds(inputIds);
2748
2773
  visibleMarked = committedInputs.some((e) => committedOriginOf(e.frontmatter) !== undefined);
2749
2774
  }
2750
- const foldedOrigin = visibleMarked ? { taint: "external", cause: "derived", at } : undefined;
2775
+ const foldedOrigin = !attested && visibleMarked ? { taint: "external", cause: "derived", at } : undefined;
2776
+ const productMarked = (p) => p.inputs.some((i2) => headerById.get(i2.id)?.exposure !== undefined);
2751
2777
  for (let i = 0; i < proposal.products.length; i++) {
2752
2778
  const p = proposal.products[i];
2753
2779
  if (!isInstructionEntry({ ...(p.type !== undefined ? { type: p.type } : {}) }))
2754
2780
  continue;
2755
- if (visibleMarked) {
2756
- wholeReasons.push(`product #${i}: instruction-form product (type ${JSON.stringify(p.type)}) refused — the run's visible set contains marked content (the laundering hard gate, G7/G26; no option opens this arm)`);
2781
+ const hardArm = attested ? productMarked(p) : visibleMarked;
2782
+ if (hardArm) {
2783
+ wholeReasons.push(`product #${i}: instruction-form product (type ${JSON.stringify(p.type)}) refused — ${attested ? "the product's declared inputs contain marked content" : "the run's visible set contains marked content"} (the laundering hard gate, G7/G26; no option opens this arm)`);
2757
2784
  }
2758
2785
  else if (!screened.allowInstructionProducts) {
2759
2786
  wholeReasons.push(`product #${i}: instruction-form product (type ${JSON.stringify(p.type)}) refused — instruction-form consolidation products are refused unconditionally by default (a consolidation must not mint privileged entries out of ordinary notes); the host escape hatch is consolidation.allowInstructionProducts: true`);
@@ -2784,6 +2811,7 @@ export class MemoryEngine {
2784
2811
  }
2785
2812
  const id = uuidv7();
2786
2813
  const slug = deriveProductSlug(p.name, `consolidated-${id.slice(0, 8)}`);
2814
+ const productOrigin = attested ? (productMarked(p) ? { taint: "external", cause: "derived", at } : undefined) : foldedOrigin;
2787
2815
  const entry = {
2788
2816
  id,
2789
2817
  slug,
@@ -2792,7 +2820,7 @@ export class MemoryEngine {
2792
2820
  name: p.name ?? slug,
2793
2821
  ...(p.description !== undefined ? { description: p.description } : {}),
2794
2822
  ...(p.type !== undefined ? { type: p.type } : {}),
2795
- ...(foldedOrigin !== undefined ? { origin: { ...foldedOrigin } } : {}),
2823
+ ...(productOrigin !== undefined ? { origin: { ...productOrigin } } : {}),
2796
2824
  distilled: {
2797
2825
  planId,
2798
2826
  at,
@@ -2917,6 +2945,7 @@ export class MemoryEngine {
2917
2945
  epoch: snapshot.epoch,
2918
2946
  visibleMarked,
2919
2947
  ...(foldedOrigin !== undefined ? { foldedOrigin } : {}),
2948
+ ...(attested ? { mintExposure: "partitioned" } : {}),
2920
2949
  products: assembled,
2921
2950
  productStates: Object.fromEntries(assembled.map((e) => [e.id, "pending"])),
2922
2951
  ...(freezeRefusedInputIds.length > 0 ? { freezeRefusedInputIds } : {}),
@@ -2924,7 +2953,21 @@ export class MemoryEngine {
2924
2953
  intents: intents.map((it) => ({ requestId: it.requestId, state: "pending" })),
2925
2954
  state: "open",
2926
2955
  audit: [
2927
- { at, event: "frozen", requestId: input.requestId, ...(refusedProducts.length > 0 ? { detail: `${refusedProducts.length} product(s) refused at freeze: ${refusedProducts.map((r) => `#${r.index}: ${r.reason}`).join(" | ")}` } : {}) },
2956
+ {
2957
+ at,
2958
+ event: "frozen",
2959
+ requestId: input.requestId,
2960
+ ...((() => {
2961
+ const parts = [];
2962
+ if (attested) {
2963
+ const markedProducts = assembled.filter((e) => e.frontmatter.origin !== undefined).length;
2964
+ parts.push(`attested exposure-partitioned mint: ${markedProducts} marked / ${assembled.length - markedProducts} clean product(s)`);
2965
+ }
2966
+ if (refusedProducts.length > 0)
2967
+ parts.push(`${refusedProducts.length} product(s) refused at freeze: ${refusedProducts.map((r) => `#${r.index}: ${r.reason}`).join(" | ")}`);
2968
+ return parts.length > 0 ? { detail: parts.join("; ") } : {};
2969
+ })()),
2970
+ },
2928
2971
  ],
2929
2972
  };
2930
2973
  writeConsolidationPlan(this.controlDir, plan);
@@ -3492,7 +3535,7 @@ export class MemoryEngine {
3492
3535
  if (read.state === "absent")
3493
3536
  continue;
3494
3537
  const p = read.plan;
3495
- out.push({ planId, scope: p.scope, state: p.state, createdAt: p.createdAt, products: p.products.length, directed: p.directed.length, intents: p.intents.length });
3538
+ out.push({ planId, scope: p.scope, state: p.state, createdAt: p.createdAt, products: p.products.length, markedProducts: p.products.filter((e) => e.frontmatter?.origin !== undefined).length, directed: p.directed.length, intents: p.intents.length });
3496
3539
  }
3497
3540
  return out;
3498
3541
  }
@@ -9,7 +9,7 @@ export { readV2HeaderHints, isInstructionEntry, type V2HeaderHints } from "./hea
9
9
  export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, ambiguousOriginRepresentation, type ParsedEntryFile } from "./frontmatter.js";
10
10
  export { committedDistilledOf, distilledEquals } from "./frontmatter.js";
11
11
  export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, CONSOLIDATION_RUN_STOP_REASONS, type ConsolidationRunStopReason, } from "./consolidation.js";
12
- export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintLlmConsolidationPlan, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, } from "./distiller.js";
12
+ export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanArm, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, } from "./distiller.js";
13
13
  export { CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, type ConsolidationDriverEngine, type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type RunMemoryConsolidationOptions, } from "./consolidation-driver.js";
14
14
  export type { OriginClearanceRow, OriginClearanceEvent } from "./origin-clearance.js";
15
15
  export { MEMORY_ORIGIN_CAUSES } from "./types.js";
@@ -9,7 +9,7 @@ export { readV2HeaderHints, isInstructionEntry } from "./header-hints.js";
9
9
  export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, ambiguousOriginRepresentation } from "./frontmatter.js";
10
10
  export { committedDistilledOf, distilledEquals } from "./frontmatter.js";
11
11
  export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, CONSOLIDATION_RUN_STOP_REASONS, } from "./consolidation.js";
12
- export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintLlmConsolidationPlan, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, } from "./distiller.js";
12
+ export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, } from "./distiller.js";
13
13
  export { CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, } from "./consolidation-driver.js";
14
14
  export { MEMORY_ORIGIN_CAUSES } from "./types.js";
15
15
  export { memoryBackendContract, assertMemoryBackendSearchEquivalence, } from "./memory-backend-contract.js";
@@ -71,6 +71,8 @@ function syntheticCheckpoint(scope) {
71
71
  args: { probe: true, nested: { depth: 2, list: [1, 2, 3] } },
72
72
  boundInputHash: boundInputHashOf({ probe: true, nested: { depth: 2, list: [1, 2, 3] } }),
73
73
  hasBidiControls: true,
74
+ preview: { probe: true, note: "park wiring self-check display stub" },
75
+ previewWithheld: "oversize",
74
76
  batchToolCallIds: [`${scope}:call`],
75
77
  completedCallIds: [],
76
78
  },
@@ -37,6 +37,30 @@ export interface TokenCounts {
37
37
  * pricing the cache subset separately instead of trusting the brain's possibly-overstated cost total.
38
38
  */
39
39
  export declare function computeCostMicroUsd(counts: TokenCounts, pricing: ModelPricing): number;
40
+ /**
41
+ * RB-368's predicate, single-sourced: does a price table EXIST for this model — an id-keyed
42
+ * `RunnerDeps.pricing` entry, or the model's own `cost` declaration? When neither does,
43
+ * `modelCostToPricing(undefined)` yields an all-zero table and every computed cost is a fabricated 0,
44
+ * indistinguishable from "declared free". Every consumer that must keep those apart (the disclosure faces
45
+ * that go absent rather than lie; the deployment $ governance window that refuses rather than charge a
46
+ * fabricated 0) asks HERE, so the two can never drift into two different answers.
47
+ */
48
+ export declare function isModelPriced(model: {
49
+ id: string;
50
+ cost?: unknown;
51
+ }, pricing: Record<string, ModelPricing> | undefined): boolean;
52
+ /**
53
+ * The first UNEVALUABLE member of a price table (`"inputPer1M"`, …), or `undefined` when every declared
54
+ * price is a finite, non-negative number.
55
+ *
56
+ * {@link isModelPriced} answers "does a table EXIST", which is the RB-368 question. It is deliberately not
57
+ * the same question as "can this table price anything": a `NaN` rate silently computes a cost of ZERO (the
58
+ * `price && tokens` guard treats NaN as absent) and a negative rate computes a negative one, so a table of
59
+ * garbage looks priced and prices everything at nothing. Wherever a number from this table becomes a
60
+ * CEILING rather than a report, the caller must ask this too — a bad value has to be loud, never folded
61
+ * into a silent default.
62
+ */
63
+ export declare function malformedPricingField(pricing: ModelPricing): string | undefined;
40
64
  /** Map a vendor `Model.cost` (already per-1M absolute) to `ModelPricing`. We don't emit 1h cache
41
65
  * writes, so `cacheWriteLongPer1M` defaults to the 5-min write price (irrelevant while that count is 0). */
42
66
  export declare function modelCostToPricing(cost: {
@@ -11,6 +11,24 @@ export function computeCostMicroUsd(counts, pricing) {
11
11
  per1M(counts.outputTokens, pricing.outputPer1M);
12
12
  return Math.round(usd * 1e6);
13
13
  }
14
+ export function isModelPriced(model, pricing) {
15
+ return pricing?.[model.id] !== undefined || model.cost !== undefined;
16
+ }
17
+ export function malformedPricingField(pricing) {
18
+ for (const f of ["inputPer1M", "outputPer1M"]) {
19
+ const v = pricing[f];
20
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0)
21
+ return f;
22
+ }
23
+ for (const f of ["cacheReadPer1M", "cacheWritePer1M", "cacheWriteLongPer1M"]) {
24
+ const v = pricing[f];
25
+ if (v === undefined)
26
+ continue;
27
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0)
28
+ return f;
29
+ }
30
+ return undefined;
31
+ }
14
32
  export function modelCostToPricing(cost) {
15
33
  return {
16
34
  inputPer1M: cost?.input ?? 0,
@@ -46,6 +46,40 @@ export declare function limitConfigError(code: string, message: string): Error &
46
46
  * sentinel, and on the budget axes it is an exhausted window (absurd but honest).
47
47
  */
48
48
  export declare function resolveTaskLimits(limits: TaskLimits | undefined): TaskLimits | undefined;
49
+ /**
50
+ * RB-318 — validate the MODE-VALUED members of `TaskSpec.attachments` at the same door, and return the
51
+ * config unchanged. Today that is exactly one member, `totalTokensReminderMode`.
52
+ *
53
+ * WHY A DOOR AT ALL (坏值响亮度律): a knob whose bad value silently folds to the default is a knob the
54
+ * deployment believes is armed. `totalTokensReminderMode: "padded_countdown"` (underscore) or
55
+ * `"Countdown"` are the near-misses a hand-written config produces, and both would otherwise read as
56
+ * "absent" and take the default arm — a DIFFERENT readout than the one that was asked for, with nothing
57
+ * anywhere saying so.
58
+ *
59
+ * DELIBERATELY NARROW, twice over:
60
+ * - no unknown-KEY refusal (the `resolveTaskLimits` half that has no counterpart here): a wire caller
61
+ * that sends an attachment flag this engine does not know is asking for a lane that stays off, which
62
+ * is the family's own default posture — not a ceiling silently disarmed. Widening it would also be a
63
+ * behavior change for every existing caller, which this additive lane does not license.
64
+ * - the pre-existing `todoReminderMode` is NOT validated here. It has the same shape and the same
65
+ * weakness, but tightening it would refuse specs that run today; recorded as a follow-up rather than
66
+ * folded into this lane's change.
67
+ *
68
+ * A NON-OBJECT CONTAINER (`attachments: null` off a wire, above all) is neither refused nor crashed on
69
+ * (review finding, verified by reading): every consumer reads this config through `attachmentsCfg?.…`,
70
+ * so a null container already means "no lanes are wired" and the spec RUNS today. An unguarded property
71
+ * read here — the first cut — turned that running spec into an untyped `TypeError` from inside a door
72
+ * that promises a typed `config.attachment_invalid`, and refusing it instead would newly reject a spec
73
+ * that works. There is no container-shape door in this family yet; that is a separate widening, not
74
+ * something this lane gets to invent on the way past.
75
+ *
76
+ * The bypass is narrowed to values that CANNOT supply attachment fields (round-2 review finding): a
77
+ * plain-JS caller can hand this a FUNCTION carrying `totalTokensReminderMode` as a property, and every
78
+ * downstream `attachmentsCfg?.…` read would happily find it — so skipping a function here would fold a
79
+ * typo'd mode to the default through exactly the hole this door exists to close. Functions are
80
+ * property-bearing containers and are validated like objects; only `null` and primitives pass through.
81
+ */
82
+ export declare function resolveAttachmentsConfig(attachments: TaskSpec["attachments"]): TaskSpec["attachments"];
49
83
  /**
50
84
  * R2 双形轴(追加令 2026-07-18): CC 2.1.212's fable-variant prompt gate (b9e —
51
85
  * `fable_5_mitigations` capability / claude-mythos-5), ORTHOGONAL to the simple/classic profile.
@@ -1,12 +1,14 @@
1
1
  import { resolveBrainCallGuardrailMs } from "../../brain/timeout.js";
2
2
  import { assertReadFaceValue } from "../../tools/fs/index.js";
3
3
  import { resolveCheckpointStore } from "../checkpoint-store.js";
4
+ import { isModelPriced, malformedPricingField, modelCostToPricing } from "../pricing.js";
4
5
  import { preflightLockedConfig } from "../locked-config.js";
5
6
  import { assertRetentionCapability } from "../retention.js";
6
7
  import { resolveModel, resolveTaskModel, roleModelIfSet } from "../roles.js";
7
8
  import { applyToolModelGate, assertRestoreGatedToolsValue, modelIdTail } from "../tool-model-gate.js";
8
9
  import { resolveUsageWindows } from "../usage-window-store.js";
9
10
  import { deriveAskEffective, resolveAskSeamForm, resolveQuestionSeam } from "../wiring-manifest.js";
11
+ import { TOTAL_TOKENS_REMINDER_MODES } from "./turn-attachments.js";
10
12
  const TASK_LIMIT_KEY_DICT = {
11
13
  maxTokens: true,
12
14
  maxCostUsd: true,
@@ -31,6 +33,16 @@ export function limitConfigError(code, message) {
31
33
  e.code = code;
32
34
  return e;
33
35
  }
36
+ function describeRejectedValue(value) {
37
+ try {
38
+ const quoted = JSON.stringify(value);
39
+ if (quoted !== undefined)
40
+ return quoted;
41
+ }
42
+ catch {
43
+ }
44
+ return `a ${typeof value} value`;
45
+ }
34
46
  export function resolveTaskLimits(limits) {
35
47
  if (limits === undefined)
36
48
  return undefined;
@@ -73,6 +85,19 @@ export function resolveTaskLimits(limits) {
73
85
  }
74
86
  return limits;
75
87
  }
88
+ export function resolveAttachmentsConfig(attachments) {
89
+ if (attachments === null || (typeof attachments !== "object" && typeof attachments !== "function"))
90
+ return attachments;
91
+ const mode = attachments.totalTokensReminderMode;
92
+ if (mode !== undefined) {
93
+ const legal = TOTAL_TOKENS_REMINDER_MODES;
94
+ if (typeof mode !== "string" || !legal.includes(mode)) {
95
+ throw limitConfigError("config.attachment_invalid", `TaskSpec.attachments.totalTokensReminderMode must be one of ${TOTAL_TOKENS_REMINDER_MODES.map((m) => `"${m}"`).join(", ")} ` +
96
+ `(got ${describeRejectedValue(mode)}) — refused rather than folded to the default, which would publish a different readout than the one the deployment asked for.`);
97
+ }
98
+ }
99
+ return attachments;
100
+ }
76
101
  export function isFableFamilyModelId(id) {
77
102
  const tail = modelIdTail(id);
78
103
  return /^claude-fable-\d/.test(tail) || /^claude-mythos-5(?!\d)/.test(tail);
@@ -290,6 +315,7 @@ export function prepareConfigDoors(input) {
290
315
  }
291
316
  }
292
317
  resolveTaskLimits(spec.limits);
318
+ resolveAttachmentsConfig(spec.attachments);
293
319
  if (spec.resourceSuspend !== undefined) {
294
320
  const rsus = spec.resourceSuspend;
295
321
  if (typeof rsus.scope !== "string" || rsus.scope === "") {
@@ -327,6 +353,35 @@ export function prepareConfigDoors(input) {
327
353
  }
328
354
  const resolvedRole = resolveTaskModel(spec, deps);
329
355
  const model = resolvedRole.model;
356
+ if (usageWindows !== undefined && deps.usageWindowStore !== undefined) {
357
+ const costed = usageWindows.find((w) => w.maxCostUsd !== undefined);
358
+ const why = costed === undefined
359
+ ? undefined
360
+ : !isModelPriced(model, deps.pricing)
361
+ ? `model ${JSON.stringify(model.id)} is unpriced — it has neither a RunnerDeps.pricing entry nor a Model.cost declaration, so this run produces NO cost figure to charge the window with (an unpriced run has no cost, which is not the same as a cost of 0). Price the model, or drop maxCostUsd from the window.`
362
+ : (() => {
363
+ const bad = malformedPricingField(deps.pricing?.[model.id] ?? modelCostToPricing(model.cost));
364
+ return bad === undefined
365
+ ? undefined
366
+ : `the price table for model ${JSON.stringify(model.id)} declares an unevaluable ${bad} — every rate must be a finite, non-negative number of USD per 1M tokens. A table like this prices every turn at zero (or below), so the ceiling would never fill: refused rather than enforced in name only.`;
367
+ })();
368
+ if (costed !== undefined && why !== undefined) {
369
+ const e = new Error(`RunnerDeps.usageWindows declares a cost ceiling (maxCostUsd ${String(costed.maxCostUsd)} over ${String(costed.windowMs)}ms, ${costed.anchor}) but ${why}`);
370
+ e.code = "config.usage_window_unpriced";
371
+ throw e;
372
+ }
373
+ }
374
+ const dollarCeiling = spec.limits?.maxCostUsd !== undefined
375
+ ? "limits.maxCostUsd"
376
+ : spec.resourceSuspend?.totalBudgetUsd !== undefined
377
+ ? "resourceSuspend.totalBudgetUsd"
378
+ : undefined;
379
+ if (dollarCeiling !== undefined && isModelPriced(model, deps.pricing)) {
380
+ const bad = malformedPricingField(deps.pricing?.[model.id] ?? modelCostToPricing(model.cost));
381
+ if (bad !== undefined) {
382
+ throw limitConfigError("config.limit_invalid", `TaskSpec.${dollarCeiling} is a money ceiling, but the price table for model ${JSON.stringify(model.id)} declares an unevaluable ${bad} — every rate must be a finite, non-negative number of USD per 1M tokens. A table like this prices every turn at zero (or below), so the ceiling would never trip: refused rather than enforced in name only.`);
383
+ }
384
+ }
330
385
  const fableMitigations = resolveModelPromptTraits(model, spec, internals).fableMitigations;
331
386
  const gateDecision = applyToolModelGate({
332
387
  tools: spec.tools,
@@ -73,14 +73,24 @@ export declare const USAGE_WINDOW_REAP_MARGIN_MS: number;
73
73
  export interface UsageGovernance {
74
74
  /** The ledger key this run is governed under — the principal, or the shared global key. */
75
75
  readonly key: string;
76
+ /** Does any governed window carry a MONEY ceiling? Read by the run loop's pricing seats: only a
77
+ * cost-governing deployment has to treat an unevaluable price table as unpriced spend. */
78
+ readonly governsCost: boolean;
76
79
  /** Ms the caller must wait before ANY window would admit work again, or `undefined` when none is
77
80
  * exhausted as of `now`. Reads the ledger; propagates a store failure (an unreadable ceiling must not
78
81
  * read as an open one). */
79
82
  check(now: number): Promise<number | undefined>;
80
- /** Charge whatever of `cumulativeTokens` has not been charged yet. Takes the run's CUMULATIVE total
81
- * rather than a delta so no caller can double-charge by calling twice, and so a caller that skips a
82
- * boundary loses nothing. */
83
- commit(cumulativeTokens: number, now: number): Promise<void>;
83
+ /**
84
+ * Charge whatever of the run's cumulative spend has not been charged yet. Takes the run's CUMULATIVE
85
+ * totals rather than deltas so no caller can double-charge by calling twice, and so a caller that skips
86
+ * a boundary loses nothing.
87
+ *
88
+ * `cumulativeCostMicroUsd` is the MONEY half (integer micro-USD, `stats.costMicroUsd`). Pass `undefined`
89
+ * when the run's spend has no cost figure at all (RB-368's unpriced state) — a deployment governing a
90
+ * `maxCostUsd` window then REFUSES here rather than charging the fabricated 0 that would let the ceiling
91
+ * silently stop applying. A token-only deployment ignores the argument entirely.
92
+ */
93
+ commit(cumulativeTokens: number, cumulativeCostMicroUsd: number | undefined, now: number): Promise<void>;
84
94
  }
85
95
  /**
86
96
  * design/164 件四 — resolve the moment an env's lifetime EXPIRES (epoch ms), or `undefined` when the env
@@ -133,7 +143,7 @@ export declare function checkpointScopeOf(spec: {
133
143
  principal?: string;
134
144
  }): string;
135
145
  export { resolveCheckpointStore } from "../checkpoint-store.js";
136
- export { isFableFamilyModelId, resolveModelPromptTraits, resolveTaskLimits } from "./prepare-config-doors.js";
146
+ export { isFableFamilyModelId, resolveAttachmentsConfig, resolveModelPromptTraits, resolveTaskLimits } from "./prepare-config-doors.js";
137
147
  export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./prepare-workspace-restore.js";
138
148
  export interface Prepared {
139
149
  harness: AgentHarness;
@@ -1463,8 +1473,8 @@ export interface RunInternals {
1463
1473
  * receive a subagent's live `task_progress` ticks (which otherwise stay in the child's ISOLATED stream). Threaded
1464
1474
  * recursively down the delegation tree (via `ctx.forwardEvent`), so every nested subagent's ticks bubble to the
1465
1475
  * SAME sink. The Runner's ctx wrapper forwards `task_progress` always; when the run's spec sets
1466
- * `forwardSubagentEvents: true` it ALSO forwards the child's content events (`text_delta` / `reasoning_delta` /
1467
- * `tool_start` / `tool_end` — the subagent viewing pane, carrying the same UNTRUSTED-RAW/consumer-must-redact
1476
+ * `forwardSubagentEvents: true` it ALSO forwards the child's content events (`text_delta` / `text_end` (#447) /
1477
+ * `reasoning_delta` / `tool_start` / `tool_end` — the subagent viewing pane, carrying the same UNTRUSTED-RAW/consumer-must-redact
1468
1478
  * contract as the main stream's tool events). Either way the child stream is NEVER merged into the parent's
1469
1479
  * MODEL context (this is purely a render channel). Absent unless the deployment opted in.
1470
1480
  */
@@ -1490,9 +1500,12 @@ export interface RunInternals {
1490
1500
  * `TaskStream.detach(toolCallId)`; the hands Bash tool threads `signalFor(toolCallId)` into env.exec. */
1491
1501
  detachHub?: import("../tool-detach.js").ToolDetachHub;
1492
1502
  onTaskNotification?: (notification: TaskNotificationPayload,
1493
- /** CC injection priority (design/116 §7, re-anchored 2026-08-05): every priority delivers at
1494
- * the next turn boundary (arrival order, consecutive frames batch); "later" (default) vs
1495
- * "next" now differs only on the park/uplink forwarding path. */
1503
+ /** Injection tier (design/373 the ladder is LIVE): "next" = the running turn's next boundary
1504
+ * (arrival order, consecutive frames batch); "later" = the run's would-otherwise-stop seat
1505
+ * (never folded into work in progress); "now" = class-head + earliest natural boundary on this
1506
+ * lane (interrupt authority belongs to the steer face, never to notifications). Internal
1507
+ * producers declare their tier explicitly (§3.7 census — completion-class lanes are "next");
1508
+ * the parameterless default "later" serves the external verb's omitting callers only. */
1496
1509
  opts?: {
1497
1510
  priority?: import("../task-notification.js").SystemInjectionPriority;
1498
1511
  }) => void;
@@ -1626,6 +1639,35 @@ export interface ResolvedWorkspace {
1626
1639
  * which `cwd` is a host path a consumer may diff / merge / remove. */
1627
1640
  remote: boolean;
1628
1641
  }
1642
+ /**
1643
+ * RB-330 + 5.38 r2 件2 — the SINGLE effective-delegation derivation for a leg, minted once per prepare
1644
+ * and read by EVERY consumer face; two facets, one source:
1645
+ *
1646
+ * - `isDelegatedChild` — the raw delegation fact, forks included. Trusted internals first; on a
1647
+ * resume leg where the caller supplied NO delegation fact, the checkpoint's persisted axis
1648
+ * ({@link import("../checkpoint-store.js").CheckpointState.isDelegatedChild}) stands in — else a
1649
+ * deps-only `resume(token, outcome, config)` of a parked delegation read `false` at every station:
1650
+ * persona flip (subagent consent/notes pack dropped), ask refusal texts on the parent-thread arm
1651
+ * ("wait for the user" in a transcript no user turn ever lands in), and a lifecycle-observer gate
1652
+ * that never opened. Consumed by `askSourceIdentity` (a fork's refusal posture is the child's) and
1653
+ * the `hookIdentity` mint.
1654
+ * - `isNonForkChild` — the fact minus forks, for the authority/context faces: the `loadProjectMemory`
1655
+ * ctx flag ({@link RunnerDeps.loadProjectMemory} `isSubagent`) and the prompt runtime fact
1656
+ * `isSubagent` (RB-204's `SUBAGENT_CONSENT_NOTICE` gate). A FORK is excluded on both, deliberately:
1657
+ * a fork IS the parent continuing (design/110), so it inherits the parent's authority — and, on the
1658
+ * memory face, the parent's own project context; trimming a fork's CLAUDE.md would hand the
1659
+ * continuation LESS context than the run it continues. (RB-330 history: the memory face once used
1660
+ * `parentTaskId !== undefined` alone, which {@link RunInternals.isDelegatedChild}'s own docstring
1661
+ * forbids — a directly-started workflow's children have no nameable parent.)
1662
+ *
1663
+ * `insideFork` is NOT persisted (the checkpoint's documented honest absence), so on a deps-only resume
1664
+ * a FORK child reads as a plain delegated child on the non-fork facet — the honest residue is written
1665
+ * on the checkpoint field's own list, not papered over with a new persistence axis.
1666
+ */
1667
+ export declare function effectiveDelegationFacts(internals: Pick<RunInternals, "isDelegatedChild" | "insideFork"> | undefined, seedIsDelegatedChild: boolean | undefined): {
1668
+ isDelegatedChild: boolean;
1669
+ isNonForkChild: boolean;
1670
+ };
1629
1671
  /**
1630
1672
  * From the resumed/active transcript, the batch position of `currentId` (design/45 §4.ter): the tool-call
1631
1673
  * ids of the assistant message that issued it (the batch), and the subset already resolved (executed