@rulvar/core 1.225.0 → 1.227.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -5225,6 +5225,17 @@ interface BudgetHooks {
5225
5225
  */
5226
5226
  remainingUsd?: () => number | undefined;
5227
5227
  /**
5228
+ * Layer 2b asked of the IN-FLIGHT EXPOSURE ceiling (RV2503), wired
5229
+ * only when the cap is configured: the output tokens the exposure
5230
+ * room still affords for this prompt. The dispatch clamps to it too,
5231
+ * so a turn whose full plan overshoots the exposure line is SHORTENED
5232
+ * rather than refused while the budget can still pay for it. An
5233
+ * answer below the serving model's output floor is ignored, so a
5234
+ * genuine exposure exhaustion still refuses through
5235
+ * `admitTurnExposure` with its own typed reason.
5236
+ */
5237
+ maxExposureOutputTokens?: (servedBy: ModelRef, estimatedInputTokens: number) => number | undefined;
5238
+ /**
5228
5239
  * The in-flight exposure admission (RV711), wired only when the cap
5229
5240
  * is configured. Called synchronously right before each provider
5230
5241
  * dispatch attempt with the attempt's own request estimate: the
@@ -6270,6 +6281,8 @@ declare class RunBudget {
6270
6281
  * reservation surface is inert and reserveTurnExposure never binds.
6271
6282
  */
6272
6283
  readonly maxInFlightExposureUsd?: number;
6284
+ /** The opt-in lone-dispatch clamp (RV2503); see maxExposureOutputTokens. */
6285
+ private readonly clampTurnToExposure;
6273
6286
  private readonly lifetimeSpawnCap;
6274
6287
  private readonly events?;
6275
6288
  private readonly priceUsd?;
@@ -6334,7 +6347,8 @@ declare class RunBudget {
6334
6347
  private readonly invalidPriceWarned;
6335
6348
  constructor(options: {
6336
6349
  ceilingUsd?: number; /** The opt-in in-flight exposure cap (RV711); see reserveTurnExposure. */
6337
- maxInFlightExposureUsd?: number;
6350
+ maxInFlightExposureUsd?: number; /** The opt-in lone-dispatch clamp (RV2503); see maxExposureOutputTokens. */
6351
+ clampTurnToExposure?: boolean;
6338
6352
  lifetimeSpawnCap?: number;
6339
6353
  events?: RuntimeEventSink;
6340
6354
  priceUsd?: (servedBy: ModelRef, usage: Usage) => number | undefined; /** Raw price-row resolution for the layer-2b output bound. */
@@ -6637,6 +6651,53 @@ declare class RunBudget {
6637
6651
  remainingUsd(accountScope?: string): number | undefined;
6638
6652
  maxAffordableOutputTokens(servedBy: ModelRef, estimatedInputTokens: number, accountScope?: string): number | undefined;
6639
6653
  /**
6654
+ * The same layer-2b question asked of the IN-FLIGHT EXPOSURE ceiling
6655
+ * (RV2503): the output tokens `cap - spent - live estimates` still
6656
+ * affords from `servedBy` for an estimated prompt, priced by the
6657
+ * settlement function like every other estimate here.
6658
+ *
6659
+ * The clamp above has always existed for the budget ceiling while
6660
+ * {@link reserveTurnExposure} only ever answered yes or no, so a
6661
+ * turn whose FULL planned output overshot the exposure line was
6662
+ * refused outright even when a shorter one fit and the budget could
6663
+ * pay for it. The 1.226.0 comparison run died exactly there: it held
6664
+ * 0.8642 USD of budget, the exposure ceiling had 0.5642 USD of room,
6665
+ * the mandatory repair turn was estimated at 0.7066 USD against an
6666
+ * 18000 token output plan, and the dispatch was refused before any
6667
+ * provider call. The same turn, re-issued after the operator raised
6668
+ * the ceiling, wrote 12840 output tokens and cost 0.4788 USD: it fit
6669
+ * the ceiling that refused it, and a clamp to the ~13253 tokens the
6670
+ * room afforded would have let it run.
6671
+ *
6672
+ * Answered ONLY for a dispatch that is alone in flight, which is the
6673
+ * whole difference between a refusal that means something and one
6674
+ * that means nothing. With siblings live the refusal is TRANSIENT:
6675
+ * RV1902 parks on it and the turn runs at its full planned length
6676
+ * the moment one of them releases, so shortening it would trade a
6677
+ * complete answer for a truncated one and buy nothing. With nothing
6678
+ * live the refusal is PERMANENT (RV2003's sweep wakes such a waiter
6679
+ * 'drained' precisely because no hold will ever return), and the
6680
+ * only choices left are a shorter turn or no turn at all. The
6681
+ * concurrent-wave bound of RV711 is therefore untouched.
6682
+ *
6683
+ * Opt-in through `RunOptions.clampTurnToExposure`, so the drained
6684
+ * refusal terminals RV1902, RV2002 and RV2003 built out of live
6685
+ * parity deaths keep their shapes until a host asks for this one.
6686
+ *
6687
+ * Undefined when the clamp is not armed, when the cap is not
6688
+ * configured, when anything is in flight, or when the model has no
6689
+ * price row, so a run that declares nothing keeps every byte of its
6690
+ * historical path. Zero or
6691
+ * negative when the room cannot even pay for the prompt, the same
6692
+ * convention {@link maxAffordableOutputTokens} inherits from
6693
+ * `affordableOutputTokens`; the caller decides what a sub-floor
6694
+ * answer means, and the loop deliberately ignores one so a true
6695
+ * exposure exhaustion still refuses through
6696
+ * {@link reserveTurnExposure} with its own typed reason instead of
6697
+ * an output-floor verdict.
6698
+ */
6699
+ maxExposureOutputTokens(servedBy: ModelRef, estimatedInputTokens: number): number | undefined;
6700
+ /**
6640
6701
  * Live accounting; spend propagates from `accountScope` to every
6641
6702
  * ancestor. Crossing a ceiling severs the crossing account's subtree
6642
6703
  * via its layer-3 AbortSignal (overshoot bounded by one turn per
@@ -7630,6 +7691,33 @@ interface RunOptions {
7630
7691
  */
7631
7692
  maxInFlightExposureUsd?: number;
7632
7693
  /**
7694
+ * Layer 2b against the exposure ceiling (RV2503), opt-in and
7695
+ * meaningful only beside `maxInFlightExposureUsd`. Armed, a dispatch
7696
+ * with NOTHING else in flight has its planned output clamped to the
7697
+ * tokens the remaining exposure room affords instead of being
7698
+ * refused outright, exactly as the budget ceiling has always clamped
7699
+ * it. The 1.226.0 comparison run is the case: nothing was live, the
7700
+ * budget still held 0.8642 USD, the mandatory repair turn's FULL
7701
+ * 18000 token plan priced 0.7066 USD against 0.5642 USD of room, and
7702
+ * the dispatch was refused before any provider call; the same work,
7703
+ * re-issued after an operator raised the ceiling, wrote 12840 output
7704
+ * tokens for 0.4788 USD. A refusal with nothing live buys nothing,
7705
+ * because no hold will ever release to fund the full plan.
7706
+ *
7707
+ * Deliberately scoped and deliberately off by default. With siblings
7708
+ * in flight the refusal is transient and the RV1902/RV2002 waits
7709
+ * park on it, so the wave keeps the full-length turn RV711 promised
7710
+ * and nothing here applies. When the room cannot even fund the
7711
+ * serving model's output floor, the clamp stands aside and the
7712
+ * dispatch refuses through the usual typed `in-flight-exposure`
7713
+ * path, so the drained-refusal terminals (RV1902, RV2002, RV2003)
7714
+ * keep their shapes. Absent, every byte of dispatch behavior is
7715
+ * historical. Like `strictPricing`, this is a per-segment posture: it
7716
+ * is not recorded in RunMeta and a resumed segment carries only what
7717
+ * its own options declare.
7718
+ */
7719
+ clampTurnToExposure?: boolean;
7720
+ /**
7633
7721
  * The opt-in strict pre-egress pricing gate (RV1508): every paid
7634
7722
  * dispatch must resolve a well-formed price row for its serving
7635
7723
  * model BEFORE the wire call, or the dispatch refuses typed
@@ -7988,6 +8076,15 @@ interface FinishValidationInput {
7988
8076
  * evidence the children actually produced.
7989
8077
  */
7990
8078
  readonly children?: readonly FinishValidationChild[];
8079
+ /**
8080
+ * The id of the run being judged (RV2501). Optional in the TYPE only
8081
+ * so hand built inputs stay source compatible; the orchestrator
8082
+ * runtime always supplies it, at every gate that judges a finish
8083
+ * (the validator-bound finish, the contract draft gate, and the
8084
+ * skipWhenDraftValid pre-pass), so a validator can accept the run's
8085
+ * own id as the artifact a claim about THIS run points at.
8086
+ */
8087
+ readonly runId?: string;
7991
8088
  }
7992
8089
  /** The verdict of one validator over one finish attempt. */
7993
8090
  type FinishValidationVerdict = {
@@ -8281,7 +8378,22 @@ declare const DEFAULT_ARTIFACT_PATTERN = "(?:run[ -]?[0-9A-HJKMNP-TV-Z]{6,26}|[\
8281
8378
  * paragraphs away no longer satisfies the grade. Purely textual: what
8282
8379
  * the referenced artifact contains is
8283
8380
  * {@link citedValueValidator}'s question, and whether it exists on
8284
- * disk is the host's. Default name 'evidence-grade'.
8381
+ * disk is the host's.
8382
+ *
8383
+ * The run's OWN id is an artifact (RV2501). `DEFAULT_ARTIFACT_PATTERN`
8384
+ * only ever matched the literal word `run` followed by a ULID, so the
8385
+ * escape the verdict advertised was unreachable for every run whose id
8386
+ * the engine did not mint in that exact shape: the comparison run's
8387
+ * `comparison-rulvar-v12260-aug09-...` matched nothing, its synthesis
8388
+ * had no artifact it could name, and a document that told the truth
8389
+ * about the run it was part of could not be written at all. When
8390
+ * {@link FinishValidationInput.runId} is supplied (the orchestrator
8391
+ * runtime always supplies it), a sentence carrying that id verbatim as
8392
+ * a whole token satisfies the grade, and the verdict names the id so
8393
+ * the repair instruction is executable rather than aspirational. An id
8394
+ * shorter than `MIN_RUN_ID_ARTIFACT_CHARS` (six) is ignored, and
8395
+ * without an id the verdict is byte identical to the historical one.
8396
+ * Default name 'evidence-grade'.
8285
8397
  */
8286
8398
  declare function evidenceGradeValidator(options?: {
8287
8399
  /** Overrides {@link DEFAULT_EVIDENCE_GRADE_PHRASES}; matched case-insensitively. */phrases?: readonly string[]; /** Overrides {@link DEFAULT_ARTIFACT_PATTERN}. */
@@ -8314,6 +8426,27 @@ interface CitationTarget {
8314
8426
  * ({@link citationTargetsValidator} judges every citation with no such
8315
8427
  * precondition).
8316
8428
  *
8429
+ * One span class is IDENTITY, not assertion (RV2502, the 1.226.0
8430
+ * comparison run): a span naming the artefact under review says which
8431
+ * commit, run, or release the document is about, and asserts nothing
8432
+ * about any cited line. That run's synthesis wrote its frozen commit
8433
+ * sha beside source citations and the validator demanded the sha appear
8434
+ * in the cited source, an impossible repair, in the same verdict that
8435
+ * demanded three real value fixes; two granted repairs burned and the
8436
+ * finish was rejected. Three shapes are structural and always excluded:
8437
+ * a commit sha (12 to 64 hex characters, long enough that ordinary hex
8438
+ * literals stay judged), a release version (`1.2.3`, `v1.2.3`, with an
8439
+ * optional prerelease or build tail), and the run's own id when the
8440
+ * runtime supplies `runId`. Host vocabulary is declared: `notValues`
8441
+ * lists spans this document writes as identity, verdict words like
8442
+ * `conditionally ready` among them.
8443
+ *
8444
+ * The run-id exclusion is what makes the bundle self consistent
8445
+ * (RV2501, RV2202): the evidence grade instructs a failing model to
8446
+ * write this run's id inside the offending sentence, and before RV2502
8447
+ * doing so beside a citation traded an evidence-grade failure for a
8448
+ * cited-value one. The two repair instructions now compose.
8449
+ *
8317
8450
  * `resolve` is host code and must be PURE over a snapshot the host
8318
8451
  * froze before the run, exactly like every other finish validator: a
8319
8452
  * resolver that reads the filesystem live would make a verdict depend
@@ -8326,6 +8459,12 @@ declare function citedValueValidator(options: {
8326
8459
  resolve: (target: CitationTarget) => string | undefined; /** Lines AFTER the cited one that may carry the value; default 0. */
8327
8460
  window?: number; /** Overrides {@link DEFAULT_CITATION_PATTERN}; must capture `path:line`. */
8328
8461
  pattern?: string;
8462
+ /**
8463
+ * Spans this host writes as IDENTITY rather than as a value asserted
8464
+ * about a citation (RV2502), matched whole and case sensitively.
8465
+ * Commit shas, versions, and the run's own id need no declaration.
8466
+ */
8467
+ notValues?: readonly string[];
8329
8468
  name?: string;
8330
8469
  }): FinishValidator;
8331
8470
  /**
@@ -8606,16 +8745,26 @@ declare function pairRunFactClaims(draftText: string, sheet: RunFactsSheet, opti
8606
8745
  *
8607
8746
  * - `'full'`: every citing sentence the draft carries had at least one
8608
8747
  * judged pair, nothing was cut by a bound, no declared critical
8609
- * anchor was missed, and the judge (when needed) settled ok. A draft
8610
- * with zero citing sentences grades `'full'` vacuously: there was
8611
- * nothing to verify, and saying `'partial'` would imply a subset was
8612
- * chosen.
8748
+ * anchor was missed, and the judge (when needed) settled ok.
8749
+ * - `'vacuous'` (RV2508): the draft carried NO citing sentence, so the
8750
+ * configured pass verified nothing. This used to grade `'full'` on
8751
+ * the reasoning that saying `'partial'` would imply a subset was
8752
+ * chosen, which is true and beside the point: `'full'` is the
8753
+ * strongest word in the vocabulary and it was standing over a
8754
+ * denominator of zero, the same silent green the grade exists to
8755
+ * abolish, at its extreme.
8613
8756
  * - `'partial'`: the pass verified a strict subset: the pair bound
8614
8757
  * truncated the fold, a run-facts bound truncated the run-claim
8615
8758
  * pairs, or citing sentences exist that no judged pair covers.
8616
8759
  * - `'critical-uncovered'`: at least one DECLARED critical anchor got
8617
8760
  * no judged pair; stronger than `'partial'` because the caller named
8618
8761
  * exactly these claims as the ones that must not go unverified.
8762
+ * - `'judge-declined'` (RV2508): the judge invocation was refused
8763
+ * ADMISSION and never dispatched (RV2106), so nothing was judged at
8764
+ * all. It ranks with a failed judge and above everything the counts
8765
+ * could say, because those counts describe a pass that did not
8766
+ * happen; before this the flag was invisible to the grade and a
8767
+ * declined judge over a citation-free draft graded `'full'`.
8619
8768
  * - `'judge-failed'`: the judge invocation did not settle ok, so
8620
8769
  * nothing was judged at all; every other reading of the meta is
8621
8770
  * moot.
@@ -8624,7 +8773,7 @@ declare function pairRunFactClaims(draftText: string, sheet: RunFactsSheet, opti
8624
8773
  * and total over metas written BEFORE the grade shipped, so a consumer
8625
8774
  * can grade a persisted outcome from an older engine.
8626
8775
  */
8627
- type ClaimCoverageGrade = "full" | "partial" | "critical-uncovered" | "judge-failed";
8776
+ type ClaimCoverageGrade = "full" | "vacuous" | "partial" | "critical-uncovered" | "judge-declined" | "judge-failed";
8628
8777
  /** The subset of the claim-consistency meta the grade derives from. */
8629
8778
  interface ClaimCoverageInput {
8630
8779
  /** Draft sentences carrying at least one parsable anchor. */
@@ -8639,6 +8788,12 @@ interface ClaimCoverageInput {
8639
8788
  runFactPairsTruncated?: true;
8640
8789
  /** True when the judge invocation did not settle ok. */
8641
8790
  judgeFailed?: true;
8791
+ /**
8792
+ * True when the judge invocation was refused ADMISSION and never
8793
+ * dispatched (RV2106). The orchestrator already spreads the flag into
8794
+ * the meta it grades, so nothing at the call site changes.
8795
+ */
8796
+ judgeDeclined?: true;
8642
8797
  }
8643
8798
  /** Derives the {@link ClaimCoverageGrade} of a claim-consistency meta. */
8644
8799
  declare function claimCoverageOf(meta: ClaimCoverageInput): ClaimCoverageGrade;
@@ -10296,6 +10451,39 @@ interface OrchestrateSynthesis {
10296
10451
  */
10297
10452
  carryDraftGaps?: boolean;
10298
10453
  /**
10454
+ * The no-regression floor under the synthesis (RV2505, the 1.226.0
10455
+ * comparison run). That run's coordination draft satisfied the FULL
10456
+ * declared contract, `skipWhenDraftValid` was off because the
10457
+ * operator wanted the composing pass anyway, and the synthesis then
10458
+ * failed the same bundle three times and died mid repair: the run
10459
+ * settled with NO result at all, having paid for four workers, the
10460
+ * draft that would have passed, and three rejected compositions.
10461
+ * With `true`, a synthesis that fails terminally does not throw away
10462
+ * a draft the contract accepts. The failure is caught at the
10463
+ * post-fan-in chokepoint, the coordination draft is judged by the
10464
+ * same `finishValidation.validators` that bind the synthesis finish,
10465
+ * and a draft every validator accepts becomes the run result under a
10466
+ * journaled 'orchestrator_synthesis_regressed' decision (the failure
10467
+ * message, the validator names, the draft hash, the contract
10468
+ * generation) plus a warn 'orchestrator synthesis regressed' log; the
10469
+ * envelope carries `synthesisRegressed`. A draft that fails too
10470
+ * journals 'orchestrator_synthesis_fallback_declined' naming ITS
10471
+ * failing validators and the original failure rethrows untouched, so
10472
+ * the decline is auditable instead of silent. Deterministic by
10473
+ * construction: only the declared contract judges, never a quality
10474
+ * heuristic, and the verdict is a pure function of the draft, so a
10475
+ * resume re-derives it without re-running the paid invocation.
10476
+ * Requires `finishValidation` (a ConfigError at intake otherwise:
10477
+ * without a contract there is nothing to judge either document by),
10478
+ * which transitively limits it to mode 'single'. Orthogonal to
10479
+ * `skipWhenDraftValid`: that gate decides whether to PAY for the
10480
+ * synthesis, this floor decides what to do when the paid one comes
10481
+ * back worse than the draft, and with both on a valid draft skips
10482
+ * before there is anything to regress. Default false: no catch, no
10483
+ * decision entry, no envelope field, byte for byte.
10484
+ */
10485
+ fallbackToValidDraft?: boolean;
10486
+ /**
10299
10487
  * The structured evidence index (RV808b): a deterministic per-child
10300
10488
  * citation map in the 'single' synthesis prompt, so the composing
10301
10489
  * model can target its reads instead of re-reading the whole
@@ -13277,7 +13465,11 @@ interface PreflightInput {
13277
13465
  * Mirrors FinishValidationSpec.maxRepairs (default
13278
13466
  * {@link DEFAULT_FINISH_MAX_REPAIRS}): with zero, the first
13279
13467
  * rejection is final and there is no repair exchange to fund, so
13280
- * the repair-reserve-unfunded warning stays silent.
13468
+ * the repair-reserve-unfunded warning stays silent. It also SIZES
13469
+ * the mandatory synthesis tail (RV2504): every granted repair can
13470
+ * write to the output allowance, so the tail
13471
+ * `synthesis-reserve-below-cap-composition` prices is one
13472
+ * composition plus this many turns, whatever the turn reserve says.
13281
13473
  */
13282
13474
  maxRepairs?: number;
13283
13475
  /**
package/dist/index.js CHANGED
@@ -10926,11 +10926,16 @@ function applyOutputBudget(req, target, budget) {
10926
10926
  const floor = outputFloorOf(target);
10927
10927
  if (req.maxOutputTokens !== void 0 && req.maxOutputTokens < floor) throw new ConfigError(`the per-turn output cap ${String(req.maxOutputTokens)} is below the ${String(floor)} token output floor of ${target.resolved.ref}; the provider would reject every dispatch, so raise limits.maxOutputTokensPerTurn to at least the floor`);
10928
10928
  const hook = budget?.maxAffordableOutputTokens;
10929
- if (hook === void 0) return req;
10930
- const affordable = hook(target.resolved.ref, estimateInputTokens(req.messages));
10929
+ const exposureHook = budget?.maxExposureOutputTokens;
10930
+ if (hook === void 0 && exposureHook === void 0) return req;
10931
+ const estimatedInput = estimateInputTokens(req.messages);
10932
+ const budgetAffordable = hook?.(target.resolved.ref, estimatedInput);
10933
+ const exposureAffordable = exposureHook?.(target.resolved.ref, estimatedInput);
10934
+ const usableExposure = exposureAffordable !== void 0 && exposureAffordable >= floor ? exposureAffordable : void 0;
10935
+ const affordable = budgetAffordable === void 0 ? usableExposure : usableExposure === void 0 ? budgetAffordable : Math.min(budgetAffordable, usableExposure);
10931
10936
  if (affordable === void 0) return req;
10932
10937
  if (affordable < floor) {
10933
- const zeroInputAffordable = hook(target.resolved.ref, 0);
10938
+ const zeroInputAffordable = hook?.(target.resolved.ref, 0);
10934
10939
  if (zeroInputAffordable !== void 0 && zeroInputAffordable < floor) throw new BudgetExhaustedError(floor === 1 ? `the remaining budget cannot afford one output token from ${target.resolved.ref}; the turn was not dispatched` : `the remaining budget cannot afford the ${String(floor)} token output floor of ${target.resolved.ref}; the turn was not dispatched`, { data: { reason: "output-floor" } });
10935
10940
  return {
10936
10941
  ...req,
@@ -13572,7 +13577,12 @@ async function runAgent(options) {
13572
13577
  * model, and a turn that cannot afford one output token is denied before
13573
13578
  * dispatch. Layer 3: the AbortSignal ceiling severing live streams, with
13574
13579
  * partial usage written usageApprox.
13575
- * B0 is immutable after start: no API tops it up.
13580
+ * B0 is immutable WITHIN a segment: no API tops up a live run's ceiling
13581
+ * (RV2511 corrects the older "immutable after start", which RV2208 made
13582
+ * false). The one thing that can change it is `ResumeOptions.run`, an
13583
+ * explicit host decision journaled as its own decision entry, and it
13584
+ * takes effect only by opening a NEW segment: a live run can never
13585
+ * raise the bound it is already being measured against.
13576
13586
  *
13577
13587
  * The account tree: the run root plus one
13578
13588
  * sub-account per admitted child workflow (and, from M7, the orchestrator
@@ -13677,6 +13687,8 @@ var RunBudget = class {
13677
13687
  * reservation surface is inert and reserveTurnExposure never binds.
13678
13688
  */
13679
13689
  maxInFlightExposureUsd;
13690
+ /** The opt-in lone-dispatch clamp (RV2503); see maxExposureOutputTokens. */
13691
+ clampTurnToExposure = false;
13680
13692
  lifetimeSpawnCap;
13681
13693
  events;
13682
13694
  priceUsd;
@@ -13745,6 +13757,7 @@ var RunBudget = class {
13745
13757
  requireValidCeiling(options.maxInFlightExposureUsd, "maxInFlightExposureUsd");
13746
13758
  this.maxInFlightExposureUsd = options.maxInFlightExposureUsd;
13747
13759
  }
13760
+ this.clampTurnToExposure = options.clampTurnToExposure === true;
13748
13761
  this.lifetimeSpawnCap = options.lifetimeSpawnCap ?? 500;
13749
13762
  if (options.events !== void 0) this.events = options.events;
13750
13763
  if (options.priceUsd !== void 0) this.priceUsd = options.priceUsd;
@@ -14327,6 +14340,59 @@ var RunBudget = class {
14327
14340
  return affordableOutputTokens(pricing, remainingUsd, estimatedInputTokens);
14328
14341
  }
14329
14342
  /**
14343
+ * The same layer-2b question asked of the IN-FLIGHT EXPOSURE ceiling
14344
+ * (RV2503): the output tokens `cap - spent - live estimates` still
14345
+ * affords from `servedBy` for an estimated prompt, priced by the
14346
+ * settlement function like every other estimate here.
14347
+ *
14348
+ * The clamp above has always existed for the budget ceiling while
14349
+ * {@link reserveTurnExposure} only ever answered yes or no, so a
14350
+ * turn whose FULL planned output overshot the exposure line was
14351
+ * refused outright even when a shorter one fit and the budget could
14352
+ * pay for it. The 1.226.0 comparison run died exactly there: it held
14353
+ * 0.8642 USD of budget, the exposure ceiling had 0.5642 USD of room,
14354
+ * the mandatory repair turn was estimated at 0.7066 USD against an
14355
+ * 18000 token output plan, and the dispatch was refused before any
14356
+ * provider call. The same turn, re-issued after the operator raised
14357
+ * the ceiling, wrote 12840 output tokens and cost 0.4788 USD: it fit
14358
+ * the ceiling that refused it, and a clamp to the ~13253 tokens the
14359
+ * room afforded would have let it run.
14360
+ *
14361
+ * Answered ONLY for a dispatch that is alone in flight, which is the
14362
+ * whole difference between a refusal that means something and one
14363
+ * that means nothing. With siblings live the refusal is TRANSIENT:
14364
+ * RV1902 parks on it and the turn runs at its full planned length
14365
+ * the moment one of them releases, so shortening it would trade a
14366
+ * complete answer for a truncated one and buy nothing. With nothing
14367
+ * live the refusal is PERMANENT (RV2003's sweep wakes such a waiter
14368
+ * 'drained' precisely because no hold will ever return), and the
14369
+ * only choices left are a shorter turn or no turn at all. The
14370
+ * concurrent-wave bound of RV711 is therefore untouched.
14371
+ *
14372
+ * Opt-in through `RunOptions.clampTurnToExposure`, so the drained
14373
+ * refusal terminals RV1902, RV2002 and RV2003 built out of live
14374
+ * parity deaths keep their shapes until a host asks for this one.
14375
+ *
14376
+ * Undefined when the clamp is not armed, when the cap is not
14377
+ * configured, when anything is in flight, or when the model has no
14378
+ * price row, so a run that declares nothing keeps every byte of its
14379
+ * historical path. Zero or
14380
+ * negative when the room cannot even pay for the prompt, the same
14381
+ * convention {@link maxAffordableOutputTokens} inherits from
14382
+ * `affordableOutputTokens`; the caller decides what a sub-floor
14383
+ * answer means, and the loop deliberately ignores one so a true
14384
+ * exposure exhaustion still refuses through
14385
+ * {@link reserveTurnExposure} with its own typed reason instead of
14386
+ * an output-floor verdict.
14387
+ */
14388
+ maxExposureOutputTokens(servedBy, estimatedInputTokens) {
14389
+ const cap = this.maxInFlightExposureUsd;
14390
+ if (!this.clampTurnToExposure || cap === void 0 || this.inFlightExposureUsd > 0) return;
14391
+ const pricing = this.pricingOf?.(servedBy);
14392
+ if (pricing === void 0) return;
14393
+ return affordableOutputTokens(pricing, Math.max(0, cap - this.root.spentUsd), estimatedInputTokens);
14394
+ }
14395
+ /**
14330
14396
  * Live accounting; spend propagates from `accountScope` to every
14331
14397
  * ancestor. Crossing a ceiling severs the crossing account's subtree
14332
14398
  * via its layer-3 AbortSignal (overshoot bounded by one turn per
@@ -18132,6 +18198,7 @@ function createCtx(internals, rootWorkflow) {
18132
18198
  remainingUsd: () => internals.budget.remainingUsd(budgetAccount),
18133
18199
  ...internals.budget.strictPricing === void 0 ? {} : { assertPricedDispatch: (servedBy) => internals.budget.assertPricedDispatch(servedBy) },
18134
18200
  ...internals.budget.maxInFlightExposureUsd === void 0 ? {} : {
18201
+ maxExposureOutputTokens: (servedBy, estimatedInputTokens) => internals.budget.maxExposureOutputTokens(servedBy, estimatedInputTokens),
18135
18202
  admitTurnExposure: (servedBy, estimatedInputTokens, plannedOutputTokens) => internals.budget.reserveTurnExposure(servedBy, estimatedInputTokens, plannedOutputTokens, `agent:${running.seq}`),
18136
18203
  awaitExposureRelease: (signal) => internals.budget.awaitExposureRelease(signal),
18137
18204
  liveExposureUsd: () => internals.budget.liveExposureUsd
@@ -19954,6 +20021,29 @@ const MAX_LISTED_CITATIONS = 20;
19954
20021
  */
19955
20022
  const MAX_NAMED_OFFENDING_SENTENCES = 5;
19956
20023
  const MAX_OFFENDING_SENTENCE_CHARS = 240;
20024
+ /**
20025
+ * The shortest run id {@link evidenceGradeValidator} will accept as an
20026
+ * artifact (RV2501). The floor mirrors the id half of
20027
+ * {@link DEFAULT_ARTIFACT_PATTERN}: a two character id would satisfy
20028
+ * nearly every sentence by accident, which is the fail-open the empty
20029
+ * pattern guard exists to prevent. The id is additionally matched as a
20030
+ * whole identifier, so it cannot be credited from inside a longer
20031
+ * word, and only inside the sentence making the claim.
20032
+ */
20033
+ const MIN_RUN_ID_ARTIFACT_CHARS = 6;
20034
+ /**
20035
+ * True when `value` occurs in `haystack` as a whole IDENTIFIER
20036
+ * (RV2501). Deliberately not {@link containsToken}: that boundary
20037
+ * class carries the dot, so an id written at the end of a sentence is
20038
+ * followed by the sentence period and would never be credited, which
20039
+ * is the most natural place to write one. Word characters alone bound
20040
+ * an id, so `x<id>y` is refused while `` `<id>` `` and `<id>.` are
20041
+ * credited. The value is matched literally (metacharacters escaped).
20042
+ */
20043
+ function containsIdentifier(haystack, value) {
20044
+ const escaped = value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
20045
+ return new RegExp(`(?<!\\w)${escaped}(?!\\w)`, "u").test(haystack);
20046
+ }
19957
20047
  function listCitations(values) {
19958
20048
  return values.length <= MAX_LISTED_CITATIONS ? values.join(", ") : `${values.slice(0, MAX_LISTED_CITATIONS).join(", ")} and ${String(values.length - MAX_LISTED_CITATIONS)} more`;
19959
20049
  }
@@ -20228,7 +20318,22 @@ const DEFAULT_ARTIFACT_PATTERN = "(?:run[ -]?[0-9A-HJKMNP-TV-Z]{6,26}|[\\w./-]+\
20228
20318
  * paragraphs away no longer satisfies the grade. Purely textual: what
20229
20319
  * the referenced artifact contains is
20230
20320
  * {@link citedValueValidator}'s question, and whether it exists on
20231
- * disk is the host's. Default name 'evidence-grade'.
20321
+ * disk is the host's.
20322
+ *
20323
+ * The run's OWN id is an artifact (RV2501). `DEFAULT_ARTIFACT_PATTERN`
20324
+ * only ever matched the literal word `run` followed by a ULID, so the
20325
+ * escape the verdict advertised was unreachable for every run whose id
20326
+ * the engine did not mint in that exact shape: the comparison run's
20327
+ * `comparison-rulvar-v12260-aug09-...` matched nothing, its synthesis
20328
+ * had no artifact it could name, and a document that told the truth
20329
+ * about the run it was part of could not be written at all. When
20330
+ * {@link FinishValidationInput.runId} is supplied (the orchestrator
20331
+ * runtime always supplies it), a sentence carrying that id verbatim as
20332
+ * a whole token satisfies the grade, and the verdict names the id so
20333
+ * the repair instruction is executable rather than aspirational. An id
20334
+ * shorter than `MIN_RUN_ID_ARTIFACT_CHARS` (six) is ignored, and
20335
+ * without an id the verdict is byte identical to the historical one.
20336
+ * Default name 'evidence-grade'.
20232
20337
  */
20233
20338
  function evidenceGradeValidator(options) {
20234
20339
  const phrases = options?.phrases === void 0 ? [...DEFAULT_EVIDENCE_GRADE_PHRASES] : requireNonEmptyStrings(options.phrases, "evidenceGradeValidator phrases");
@@ -20246,10 +20351,11 @@ function evidenceGradeValidator(options) {
20246
20351
  validate: (input) => {
20247
20352
  const unsupported = [];
20248
20353
  const offenders = [];
20354
+ const runId = typeof input.runId === "string" && input.runId.trim().length >= MIN_RUN_ID_ARTIFACT_CHARS ? input.runId.trim() : void 0;
20249
20355
  for (const sentence of sentencesOf(input.text)) {
20250
20356
  const haystack = sentence.toLowerCase();
20251
20357
  const found = lowered.filter((phrase) => haystack.includes(phrase));
20252
- if (found.length === 0 || new RegExp(artifactPattern, "").test(sentence)) continue;
20358
+ if (found.length === 0 || new RegExp(artifactPattern, "").test(sentence) || runId !== void 0 && containsIdentifier(sentence, runId)) continue;
20253
20359
  offenders.push(sentence);
20254
20360
  for (const phrase of found) if (!unsupported.includes(phrase)) unsupported.push(phrase);
20255
20361
  }
@@ -20262,7 +20368,7 @@ function evidenceGradeValidator(options) {
20262
20368
  return {
20263
20369
  ok: false,
20264
20370
  reasons: [
20265
- `evidence-grade claims cite no run or repro artifact in their own sentence: ${listCitations(unsupported)}; give each such claim a file:line citation in its own sentence, or state its run id in a SEPARATE sentence carrying no source citation (a run id written beside a path:line citation is not in the cited window and trades this failure for a cited-value one)`,
20371
+ runId === void 0 ? `evidence-grade claims cite no run or repro artifact in their own sentence: ${listCitations(unsupported)}; give each such claim a file:line citation in its own sentence, or state its run id in a SEPARATE sentence carrying no source citation (a run id written beside a path:line citation is not in the cited window and trades this failure for a cited-value one)` : `evidence-grade claims cite no run or repro artifact in their own sentence: ${listCitations(unsupported)}; write this run's id ${runId} inside each such sentence, or give the claim a file:line citation instead (the id may share a sentence with a source citation: cited-value reads a run id as identity, not as a value asserted about the cited line)`,
20266
20372
  ...named,
20267
20373
  ...overflow > 0 ? [`and ${String(overflow)} more offending sentences`] : []
20268
20374
  ]
@@ -20273,6 +20379,15 @@ function evidenceGradeValidator(options) {
20273
20379
  /** Splits a citation into its path and line halves at the LAST colon. */
20274
20380
  const CITATION_TAIL$1 = /^(.*):(\d+)$/u;
20275
20381
  /**
20382
+ * A commit sha span (RV2502). Twelve hex characters is the floor: real
20383
+ * abbreviations run 7 to 12 and full shas 40, while shorter hex words
20384
+ * (`deadbeef`) are ordinary literals a document may legitimately assert
20385
+ * about a cited line, so they stay judged.
20386
+ */
20387
+ const COMMIT_SHA_SPAN = /^[0-9a-f]{12,64}$/u;
20388
+ /** A release version span (RV2502), with an optional tail. */
20389
+ const VERSION_SPAN = /^v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z][0-9A-Za-z.-]*)?$/u;
20390
+ /**
20276
20391
  * True when `value` occurs in `haystack` as a WHOLE token (RV1402, the
20277
20392
  * seventeenth comparison experiment P0-1). Substring matching credited
20278
20393
  * an asserted `3` against a line that says `30`, so the validator
@@ -20308,6 +20423,27 @@ function containsToken(haystack, value) {
20308
20423
  * ({@link citationTargetsValidator} judges every citation with no such
20309
20424
  * precondition).
20310
20425
  *
20426
+ * One span class is IDENTITY, not assertion (RV2502, the 1.226.0
20427
+ * comparison run): a span naming the artefact under review says which
20428
+ * commit, run, or release the document is about, and asserts nothing
20429
+ * about any cited line. That run's synthesis wrote its frozen commit
20430
+ * sha beside source citations and the validator demanded the sha appear
20431
+ * in the cited source, an impossible repair, in the same verdict that
20432
+ * demanded three real value fixes; two granted repairs burned and the
20433
+ * finish was rejected. Three shapes are structural and always excluded:
20434
+ * a commit sha (12 to 64 hex characters, long enough that ordinary hex
20435
+ * literals stay judged), a release version (`1.2.3`, `v1.2.3`, with an
20436
+ * optional prerelease or build tail), and the run's own id when the
20437
+ * runtime supplies `runId`. Host vocabulary is declared: `notValues`
20438
+ * lists spans this document writes as identity, verdict words like
20439
+ * `conditionally ready` among them.
20440
+ *
20441
+ * The run-id exclusion is what makes the bundle self consistent
20442
+ * (RV2501, RV2202): the evidence grade instructs a failing model to
20443
+ * write this run's id inside the offending sentence, and before RV2502
20444
+ * doing so beside a citation traded an evidence-grade failure for a
20445
+ * cited-value one. The two repair instructions now compose.
20446
+ *
20311
20447
  * `resolve` is host code and must be PURE over a snapshot the host
20312
20448
  * froze before the run, exactly like every other finish validator: a
20313
20449
  * resolver that reads the filesystem live would make a verdict depend
@@ -20318,6 +20454,9 @@ function containsToken(haystack, value) {
20318
20454
  */
20319
20455
  function citedValueValidator(options) {
20320
20456
  if (typeof options.resolve !== "function") throw new ConfigError("citedValueValidator resolve must be a function");
20457
+ if (options.notValues !== void 0) {
20458
+ if (!Array.isArray(options.notValues) || options.notValues.some((value) => typeof value !== "string" || value.length === 0)) throw new ConfigError("citedValueValidator notValues must be an array of non empty strings");
20459
+ }
20321
20460
  const window = options.window ?? 0;
20322
20461
  if (!Number.isInteger(window) || window < 0) throw new ConfigError(`citedValueValidator window must be a non negative integer; got ${String(window)}`);
20323
20462
  const pattern = options.pattern ?? "[\\w./-]+\\.\\w+:\\d+";
@@ -20326,10 +20465,17 @@ function citedValueValidator(options) {
20326
20465
  } catch (thrown) {
20327
20466
  throw new ConfigError(`citedValueValidator pattern does not compile: ${thrown instanceof Error ? thrown.message : String(thrown)}`);
20328
20467
  }
20468
+ const declaredNotValues = new Set(options.notValues ?? []);
20329
20469
  return {
20330
20470
  name: options.name ?? "cited-value",
20331
20471
  validate: (input) => {
20332
20472
  const reasons = [];
20473
+ const runId = typeof input.runId === "string" && input.runId.trim().length >= MIN_RUN_ID_ARTIFACT_CHARS ? input.runId.trim() : void 0;
20474
+ /**
20475
+ * True when the span NAMES the artefact under review instead of
20476
+ * asserting something about a cited line (RV2502).
20477
+ */
20478
+ const isIdentity = (span) => declaredNotValues.has(span) || span === runId || COMMIT_SHA_SPAN.test(span) || VERSION_SPAN.test(span);
20333
20479
  for (const sentence of sentencesOf(input.text)) {
20334
20480
  const spans = [...sentence.matchAll(/`([^`]+)`/gu)].map((match) => match[1]);
20335
20481
  const citations = [];
@@ -20341,7 +20487,7 @@ function citedValueValidator(options) {
20341
20487
  path: parsed[1],
20342
20488
  line
20343
20489
  });
20344
- else values.push(span);
20490
+ else if (!isIdentity(span)) values.push(span);
20345
20491
  }
20346
20492
  if (citations.length === 0 || values.length === 0) continue;
20347
20493
  for (const citation of citations) {
@@ -20917,8 +21063,10 @@ function pairRunFactClaims(draftText, sheet, options) {
20917
21063
  /** Derives the {@link ClaimCoverageGrade} of a claim-consistency meta. */
20918
21064
  function claimCoverageOf(meta) {
20919
21065
  if (meta.judgeFailed === true) return "judge-failed";
21066
+ if (meta.judgeDeclined === true) return "judge-declined";
20920
21067
  if ((meta.criticalUncoveredTotal ?? 0) > 0) return "critical-uncovered";
20921
21068
  if (meta.truncated || meta.runFactPairsTruncated === true || meta.coveredCitingSentences < meta.draftCitingSentences) return "partial";
21069
+ if (meta.draftCitingSentences === 0) return "vacuous";
20922
21070
  return "full";
20923
21071
  }
20924
21072
  //#endregion
@@ -21545,6 +21693,11 @@ function validateOrchestrateOptions(opts) {
21545
21693
  if (typeof conditional.carryDraftGaps !== "boolean") throw new ConfigError("orchestrate synthesis.carryDraftGaps must be a boolean; got " + typeof conditional.carryDraftGaps);
21546
21694
  if (conditional.carryDraftGaps && conditional.skipWhenDraftValid !== true) throw new ConfigError("orchestrate synthesis.carryDraftGaps requires skipWhenDraftValid: the gaps ARE the failed pre-pass verdict, and without the pre-pass there is nothing to carry");
21547
21695
  }
21696
+ const floor = synthesis.fallbackToValidDraft;
21697
+ if (floor !== void 0) {
21698
+ if (typeof floor !== "boolean") throw new ConfigError("orchestrate synthesis.fallbackToValidDraft must be a boolean; got " + typeof floor);
21699
+ if (floor && opts.finishValidation === void 0) throw new ConfigError("orchestrate synthesis.fallbackToValidDraft requires finishValidation: without a declared finish contract there is nothing to judge the draft valid by");
21700
+ }
21548
21701
  if (symmetry.context !== void 0 && symmetry.context !== "digests" && symmetry.context !== "full") throw new ConfigError("orchestrate synthesis.context must be 'digests' or 'full'; got " + JSON.stringify(symmetry.context));
21549
21702
  const index = synthesis.evidenceIndex;
21550
21703
  if (index !== void 0) {
@@ -22900,7 +23053,8 @@ function makeOrchestratorWorkflow(goal, opts) {
22900
23053
  const input = {
22901
23054
  result,
22902
23055
  text: typeof result === "string" ? result : JSON.stringify(result),
22903
- children: validationChildren()
23056
+ children: validationChildren(),
23057
+ runId: internals.runId
22904
23058
  };
22905
23059
  const failed = [];
22906
23060
  for (const validator of validationSpec.validators) {
@@ -23013,7 +23167,8 @@ function makeOrchestratorWorkflow(goal, opts) {
23013
23167
  const input = {
23014
23168
  result,
23015
23169
  text,
23016
- children: validationChildren()
23170
+ children: validationChildren(),
23171
+ runId: internals.runId
23017
23172
  };
23018
23173
  for (const validator of validationSpec?.validators ?? []) {
23019
23174
  let verdict;
@@ -23848,7 +24003,8 @@ function makeOrchestratorWorkflow(goal, opts) {
23848
24003
  const input = {
23849
24004
  result: draftValue,
23850
24005
  text: typeof draftValue === "string" ? draftValue : JSON.stringify(draftValue),
23851
- children: validationChildren()
24006
+ children: validationChildren(),
24007
+ runId: internals.runId
23852
24008
  };
23853
24009
  const failed = [];
23854
24010
  for (const validator of validationSpec.validators) {
@@ -24036,13 +24192,14 @@ function makeOrchestratorWorkflow(goal, opts) {
24036
24192
  }
24037
24193
  return `RUN FACTS: ${JSON.stringify({
24038
24194
  scope: "settled-children-only",
24195
+ runId: internals.runId,
24039
24196
  children: settledEntries.length,
24040
24197
  byStatus: Object.fromEntries(Object.keys(byStatus).sort().map((status) => [status, byStatus[status]])),
24041
24198
  wireRequests,
24042
24199
  wireIdsMissing,
24043
24200
  inputTokens,
24044
24201
  outputTokens
24045
- })} (live-observed by this run's own harness; production evidence it is not; the settled children ONLY, excluding this orchestrator, judges, and synthesis; the whole run's totals are the terminal envelope and invoice)`;
24202
+ })} (live-observed by run ${internals.runId}, this run's own harness; production evidence it is not; the settled children ONLY, excluding this orchestrator, judges, and synthesis; the whole run's totals are the terminal envelope and invoice)`;
24046
24203
  })()] : [],
24047
24204
  `GOAL: ${goal}`,
24048
24205
  `DRAFT: ${draftJson}`,
@@ -24403,6 +24560,83 @@ function makeOrchestratorWorkflow(goal, opts) {
24403
24560
  } : { ran: true },
24404
24561
  synthesis
24405
24562
  });
24563
+ /**
24564
+ * The no-regression fallback verdict (RV2505), set only when the
24565
+ * floor actually caught a failing synthesis: the truncated failure
24566
+ * message and the journal seq of the decision that recorded it.
24567
+ */
24568
+ let synthesisRegressed;
24569
+ /**
24570
+ * The no-regression floor under the synthesis (RV2505, the 1.226.0
24571
+ * comparison run): a synthesis that fails terminally must not throw
24572
+ * away a coordination draft the SAME declared contract accepts.
24573
+ * Judges the draft with the validator bundle, journals what it
24574
+ * found either way, and answers whether the caller should settle on
24575
+ * the draft instead of rethrowing. Pure: the verdict is a function
24576
+ * of the draft and the validators, so a resume that re-fails the
24577
+ * synthesis re-derives the identical answer, and the journaled
24578
+ * decision is reused rather than duplicated.
24579
+ */
24580
+ const draftFallbackOnRegression = async (draft, thrown) => {
24581
+ if (!(opts?.synthesis?.fallbackToValidDraft === true) || validationSpec === void 0) return { used: false };
24582
+ if (thrown instanceof ConfigError) return { used: false };
24583
+ const draftValue = draft ?? null;
24584
+ const draftHash = createHash("sha256").update(jcsSerialize(draftValue), "utf8").digest("hex");
24585
+ const validatorNames = validationSpec.validators.map((validator) => validator.name);
24586
+ const input = {
24587
+ result: draftValue,
24588
+ text: typeof draftValue === "string" ? draftValue : JSON.stringify(draftValue),
24589
+ children: validationChildren()
24590
+ };
24591
+ const failed = [];
24592
+ for (const validator of validationSpec.validators) {
24593
+ let verdict;
24594
+ try {
24595
+ verdict = validator.validate(input);
24596
+ } catch (validatorThrew) {
24597
+ throw new ConfigError(`finish validator '${validator.name}' threw instead of returning a verdict during the fallbackToValidDraft judgement: ` + (validatorThrew instanceof Error ? validatorThrew.message : String(validatorThrew)));
24598
+ }
24599
+ if (!verdict.ok) failed.push({
24600
+ name: validator.name,
24601
+ reasons: verdict.reasons
24602
+ });
24603
+ }
24604
+ const regressed = failed.length === 0;
24605
+ const reason = (thrown instanceof Error ? thrown.message : String(thrown)).slice(0, 300);
24606
+ const key = deriverV2.deriveKey({ kind: regressed ? "orchestrator-synthesis-regressed" : "orchestrator-synthesis-fallback-declined" });
24607
+ const entryRef = internals.replayer.snapshot().find((entry) => entry.kind === "decision" && entry.key === key)?.seq ?? (await internals.replayer.appendSinglePhase({
24608
+ scope: callingState.scope,
24609
+ key,
24610
+ kind: "decision",
24611
+ status: "ok",
24612
+ spanId: internals.spans.mint(callingState.spanId),
24613
+ site: "orchestrator-synthesis-fallback",
24614
+ value: {
24615
+ decisionType: regressed ? "orchestrator_synthesis_regressed" : "orchestrator_synthesis_fallback_declined",
24616
+ reason,
24617
+ validators: validatorNames,
24618
+ ...regressed ? {} : { failed },
24619
+ ...validationSpec.contract === void 0 ? {} : { contractHash: validationSpec.contract.hash },
24620
+ draftHash
24621
+ }
24622
+ })).seq;
24623
+ internals.events.emit({
24624
+ type: "log",
24625
+ level: "warn",
24626
+ msg: regressed ? "orchestrator synthesis regressed" : "orchestrator synthesis fallback declined",
24627
+ data: {
24628
+ reason,
24629
+ decisionRef: entryRef,
24630
+ ...regressed ? {} : { draftFailed: failed.map((row) => row.name) }
24631
+ }
24632
+ }, callingState.spanId);
24633
+ if (!regressed) return { used: false };
24634
+ synthesisRegressed = {
24635
+ reason,
24636
+ decisionRef: entryRef
24637
+ };
24638
+ return { used: true };
24639
+ };
24406
24640
  const enrichSynthesisFailure = (thrown, snapshot) => {
24407
24641
  const passTruth = {
24408
24642
  ...claimConsistencyMeta === void 0 ? {} : { claimConsistencyMeta },
@@ -24445,6 +24679,7 @@ function makeOrchestratorWorkflow(goal, opts) {
24445
24679
  return await runSynthesis(result.output);
24446
24680
  } catch (thrown) {
24447
24681
  await journalSynthesisAdmissionDecline(thrown);
24682
+ if ((await draftFallbackOnRegression(result.output, thrown)).used) return result.output;
24448
24683
  return enrichSynthesisFailure(thrown);
24449
24684
  }
24450
24685
  }
@@ -24652,7 +24887,8 @@ function makeOrchestratorWorkflow(goal, opts) {
24652
24887
  synthesizedFinal = await runSynthesis(result.output);
24653
24888
  } catch (thrown) {
24654
24889
  await journalSynthesisAdmissionDecline(thrown);
24655
- enrichSynthesisFailure(thrown, {
24890
+ if ((await draftFallbackOnRegression(result.output, thrown)).used) synthesizedFinal = result.output;
24891
+ else enrichSynthesisFailure(thrown, {
24656
24892
  completion: decision.completion,
24657
24893
  childStatusCounts: decision.childStatusCounts,
24658
24894
  degradedReasons: decision.degradedReasons,
@@ -24676,6 +24912,7 @@ function makeOrchestratorWorkflow(goal, opts) {
24676
24912
  ...envelopeSchemaRecovered === 0 ? {} : { schemaRecoveredFinishExchanges: envelopeSchemaRecovered },
24677
24913
  ...synthesisReserveLifecycle === void 0 ? {} : { synthesisReserve: synthesisReserveLifecycle },
24678
24914
  ...synthesisSkippedByValidDraft ? { synthesisSkipped: "synthesis_skipped_by_valid_draft" } : {},
24915
+ ...synthesisRegressed === void 0 ? {} : { synthesisRegressed },
24679
24916
  ...contradictionsFound === void 0 ? {} : {
24680
24917
  contradictions: contradictionsFound,
24681
24918
  contradictionsMeta
@@ -25337,12 +25574,18 @@ function preflightEstimate(input) {
25337
25574
  cacheReadTokens: 0,
25338
25575
  cacheWriteTokens: 0
25339
25576
  });
25340
- const repairTurns = finishRepairReserve > 0 ? 1 : 0;
25341
- const requiredUsd = compositionUsd * (1 + repairTurns);
25342
- if (declared < requiredUsd) say({
25343
- severity: "warning",
25577
+ const grantedRepairs = input.finishValidation === void 0 ? 0 : input.finishValidation.maxRepairs ?? 1;
25578
+ const tailTurns = 1 + grantedRepairs;
25579
+ const requiredUsd = compositionUsd * tailTurns;
25580
+ const exposureCapUsd = input.run?.maxInFlightExposureUsd;
25581
+ const reserveLineUsd = ceilingUsd === void 0 ? void 0 : ceilingUsd - declared;
25582
+ const exposureRoomUsd = exposureCapUsd === void 0 || reserveLineUsd === void 0 ? void 0 : exposureCapUsd - reserveLineUsd;
25583
+ const reserveShort = declared < requiredUsd;
25584
+ const exposureShort = exposureRoomUsd !== void 0 && exposureRoomUsd < requiredUsd;
25585
+ if (reserveShort || exposureShort) say({
25586
+ severity: reserveShort && exposureShort ? "error" : "warning",
25344
25587
  code: "synthesis-reserve-below-cap-composition",
25345
- message: `a synthesis turn writing to its ${String(outputBound)} token output allowance over the declared ${String(synthesis.estInputTokens ?? 0)} input floor prices about ${compositionUsd.toFixed(4)} USD at the rates of '${servedBy}'` + (repairTurns > 0 ? `, and the declared repair reserve prices one more such turn (about ${requiredUsd.toFixed(4)} USD total)` : "") + `; the committed synthesis reserve holds only ${declared.toFixed(4)} USD, so a composition cut at the allowance can fail its validators with no money left for the granted repair; hold the reserve at the cap arithmetic or lower maxOutputTokensPerTurn`,
25588
+ message: `the mandatory synthesis tail is ${String(tailTurns)} turn(s) (one composition ` + (grantedRepairs === 0 ? "and no granted repair" : `plus the ${String(grantedRepairs)} granted repair(s)`) + `), each writing to its ${String(outputBound)} token output allowance over the declared ${String(synthesis.estInputTokens ?? 0)} input floor: ${String(tailTurns)} x ${compositionUsd.toFixed(4)} = ${requiredUsd.toFixed(4)} USD at the rates of '${servedBy}'` + (reserveShort ? `; the committed synthesis reserve holds only ${declared.toFixed(4)} USD` : `; the committed ${declared.toFixed(4)} USD reserve covers it`) + (exposureRoomUsd === void 0 ? "" : exposureShort ? `, and maxInFlightExposureUsd ${(exposureCapUsd ?? 0).toFixed(4)} leaves only ${exposureRoomUsd.toFixed(4)} USD above the reserve line ${(reserveLineUsd ?? 0).toFixed(4)} USD` : `, and the exposure ceiling leaves ${exposureRoomUsd.toFixed(4)} USD above the reserve line`) + ": a composition cut at the allowance can fail its validators with no room left for the repairs the runtime will grant; hold the reserve at the tail arithmetic, raise maxInFlightExposureUsd toward the ceiling, lower maxOutputTokensPerTurn, or grant fewer repairs",
25346
25589
  spawn: "synthesis"
25347
25590
  });
25348
25591
  }
@@ -26513,6 +26756,7 @@ function createEngine(options) {
26513
26756
  if (wf.kind !== "workflow" && wf.kind !== "compiled-workflow") throw new ConfigError("engine.run accepts in-process Workflow values or compileScript CompiledWorkflow values");
26514
26757
  if (opts?.budgetUsd !== void 0) requireNonNegativeNumber(opts.budgetUsd, "RunOptions.budgetUsd");
26515
26758
  if (opts?.maxInFlightExposureUsd !== void 0) requireNonNegativeNumber(opts.maxInFlightExposureUsd, "RunOptions.maxInFlightExposureUsd");
26759
+ if (opts?.clampTurnToExposure !== void 0 && typeof opts.clampTurnToExposure !== "boolean") throw new ConfigError("RunOptions.clampTurnToExposure must be a boolean; got " + JSON.stringify(opts.clampTurnToExposure));
26516
26760
  if (opts?.strictPricing !== void 0 && typeof opts.strictPricing !== "boolean" && (typeof opts.strictPricing !== "object" || opts.strictPricing === null || Array.isArray(opts.strictPricing))) throw new ConfigError("RunOptions.strictPricing must be a boolean or an options object; got " + JSON.stringify(opts.strictPricing));
26517
26761
  if (opts?.limits !== void 0) validateUsageLimits(opts.limits, "RunOptions.limits");
26518
26762
  const deadlineAtMs = opts?.deadlineAt === void 0 ? void 0 : parseDeadlineAt(opts.deadlineAt);
@@ -26550,6 +26794,7 @@ function createEngine(options) {
26550
26794
  const makeBudget = () => new RunBudget({
26551
26795
  ...ceilingUsd === void 0 ? {} : { ceilingUsd },
26552
26796
  ...exposureCapUsd === void 0 ? {} : { maxInFlightExposureUsd: exposureCapUsd },
26797
+ ...opts?.clampTurnToExposure === true ? { clampTurnToExposure: true } : {},
26553
26798
  ...strictPricing === void 0 ? {} : {
26554
26799
  strictPricing,
26555
26800
  now: realNow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.225.0",
3
+ "version": "1.227.0",
4
4
  "description": "Rulvar core: L0 contracts, journal kernel, ctx primitives, agent runtime, model router, tool system, dynamic orchestrator, InMemory and JSONL stores, event stream.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",