@rulvar/core 1.241.0 → 1.242.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
@@ -1122,6 +1122,16 @@ type JournalEntry = {
1122
1122
  cap?: number;
1123
1123
  };
1124
1124
  /**
1125
+ * Terminal agent entries whose invocation was aborted by the host's
1126
+ * finish rejection (RV3702): the declared finish contract rejected
1127
+ * the candidate past its repair bound, so the span died by host
1128
+ * hand with its wires fine. Stamped at settle from the typed abort
1129
+ * reason; never on a defective (throwing) validator, whose abort
1130
+ * carries its own reason, because a host defect is not a verdict on
1131
+ * the candidate. Policy, never identity, exactly like usageByModel.
1132
+ */
1133
+ hostRejected?: boolean;
1134
+ /**
1125
1135
  * Terminal escalated entries ONLY: the schema-validated
1126
1136
  * EscalationReport with runtime-filled costToDate and salvage; replay
1127
1137
  * synthesizes the byte-identical report from here (DEF-1).
@@ -2599,6 +2609,15 @@ type AgentEvents = {
2599
2609
  */
2600
2610
  retryCount?: number;
2601
2611
  /**
2612
+ * Present and true when the invocation was aborted by the host's
2613
+ * finish rejection (RV3702): the declared finish contract
2614
+ * rejected the candidate past its repair bound. Journaled on the
2615
+ * terminal agent entry (unlike retryCount), so a replayed
2616
+ * agent:end carries it too and both surfaces of the RV3404 cut
2617
+ * read the same count.
2618
+ */
2619
+ hostRejected?: boolean;
2620
+ /**
2602
2621
  * The exploration guard counters (RV-210). Present live whenever
2603
2622
  * any exploration guard limit was configured for the invocation;
2604
2623
  * on replay present only when the guard abort journaled it in the
@@ -3826,6 +3845,8 @@ interface TerminalPatch {
3826
3845
  used: number;
3827
3846
  cap?: number;
3828
3847
  };
3848
+ /** Terminal agent entries: the host finish rejection stamp (RV3702); see JournalEntry. */
3849
+ hostRejected?: boolean;
3829
3850
  /** Terminal escalated entries: the validated EscalationReport. */
3830
3851
  escalation?: unknown;
3831
3852
  /**
@@ -6428,6 +6449,8 @@ interface BudgetAccountView {
6428
6449
  finalizeReserveUsd: number;
6429
6450
  /** The synthesis payload hold (cycle 76); zero when none is committed. */
6430
6451
  synthesisReserveUsd: number;
6452
+ /** The repair round's verdict hold (RV3701); zero when none is committed. */
6453
+ convergenceReserveUsd: number;
6431
6454
  parentScope?: string;
6432
6455
  }
6433
6456
  /**
@@ -6727,6 +6750,28 @@ declare class RunBudget {
6727
6750
  commitSynthesisReserve(scope: string, reserveUsd: number): void;
6728
6751
  /** The synthesis dispatch consumes its reserve; see commitSynthesisReserve. */
6729
6752
  releaseSynthesisReserve(scope: string): void;
6753
+ /**
6754
+ * Registers the repair round's verdict reserve (RV3701, the third
6755
+ * comparison experiment's arc): absolute dollars held on the
6756
+ * orchestrator account AND the run root for the verdict pass (the
6757
+ * round's second judge invocation) that must follow a DISPATCHED
6758
+ * claim repair round. The third comparison run
6759
+ * proved the round's two invocation tail is only as convergent as
6760
+ * the money left when the candidate materializes; with the verdict
6761
+ * money held from the moment the round is admitted, the round's own
6762
+ * repair turns (the layer-2b clamp prices output from a remainder
6763
+ * this hold shrinks) and any concurrent admission (the hold joins
6764
+ * the projected admission sum) cannot eat it, so a round the budget
6765
+ * can only START is refused before any wire call instead of being
6766
+ * paid for and left unjudgeable. Exactly the synthesis reserve
6767
+ * mechanics: released to the invocation it was held FOR (the
6768
+ * verdict pass dispatch), never joined to the severing check.
6769
+ * Idempotent per account: registering again adjusts the root by the
6770
+ * delta.
6771
+ */
6772
+ commitConvergenceReserve(scope: string, reserveUsd: number): void;
6773
+ /** The verdict pass dispatch consumes its reserve; see commitConvergenceReserve. */
6774
+ releaseConvergenceReserve(scope: string): void;
6730
6775
  /** The reserve is replaced by real spend when the spawn settles. */
6731
6776
  releaseReserve(reserveUsd: number, accountScope?: string): void;
6732
6777
  /**
@@ -13641,6 +13686,15 @@ interface JournaledCriticalPath {
13641
13686
  */
13642
13687
  lastCandidateMs?: number;
13643
13688
  /**
13689
+ * Settled agent spans whose invocation was aborted by the host's
13690
+ * finish rejection (RV3702): the journaled `hostRejected` stamps
13691
+ * counted. Unconditional (the stamp is self contained: no label, no
13692
+ * segment condition) and zero when none, exactly the live reading
13693
+ * of the same run: the layer split (wires fine, document refused by
13694
+ * host) stays readable years after the process exited.
13695
+ */
13696
+ hostRejectedSpans: number;
13697
+ /**
13644
13698
  * The window itemization a journal CAN answer (RV3404); present
13645
13699
  * exactly when `postFanInMs` is.
13646
13700
  */
@@ -13925,6 +13979,28 @@ interface PinnedPricingSegment {
13925
13979
  pricingVersion?: string;
13926
13980
  /** The applied rows THIS settle pinned. */
13927
13981
  rows: AppliedPricingRow[];
13982
+ /**
13983
+ * sha256 over the canonical JSON of THIS pin's rows (RV3703): the
13984
+ * version string is a label the table author chose, and the third
13985
+ * experiment's arc found a price defect that a label cannot expose;
13986
+ * the hash is the content. Two tables sharing a version string but
13987
+ * disagreeing on rates are distinguishable, and two folds of one
13988
+ * journal always derive the same hex. Computed at read time from
13989
+ * the pinned bytes: the journal is unchanged and every existing pin
13990
+ * gains it.
13991
+ */
13992
+ rowsHash: string;
13993
+ /**
13994
+ * The freshness range of THIS pin's dated rows (RV3703): the oldest
13995
+ * and newest `ratesVerifiedAt` among rows carrying a parsable one,
13996
+ * the machine-readable age of the table that priced the segment.
13997
+ * Absent when no row is dated: freshness is then unattested, never
13998
+ * guessed.
13999
+ */
14000
+ ratesVerifiedAt?: {
14001
+ oldest: string;
14002
+ newest: string;
14003
+ };
13928
14004
  }
13929
14005
  /** What `journalPricingSnapshot` rebuilds from a pinned run settle. */
13930
14006
  interface JournalPricingSnapshot {
@@ -13932,6 +14008,16 @@ interface JournalPricingSnapshot {
13932
14008
  pricingVersion?: string;
13933
14009
  /** The last pin's rows: the union covering the whole settled journal. */
13934
14010
  rows: AppliedPricingRow[];
14011
+ /** The last pin's content hash (RV3703); see PinnedPricingSegment.rowsHash. */
14012
+ rowsHash: string;
14013
+ /**
14014
+ * The last pin's freshness range (RV3703); see the per-segment
14015
+ * field. Absent when no row of the last pin is dated.
14016
+ */
14017
+ ratesVerifiedAt?: {
14018
+ oldest: string;
14019
+ newest: string;
14020
+ };
13935
14021
  /**
13936
14022
  * The seq of the last pinning settle: rows at or past it belong to a
13937
14023
  * segment no pin covers yet, so a caller composing with a live table
@@ -14738,7 +14824,13 @@ interface PreflightOrchestratorSpec {
14738
14824
  * before the first wire, not the journal after the last. Pairings
14739
14825
  * orchestrate() refuses at intake (repair at the draft stage,
14740
14826
  * repair without a synthesis, carry at the final stage, RV3301)
14741
- * surface as error findings: the run would refuse to start.
14827
+ * surface as error findings: the run would refuse to start. This
14828
+ * static arithmetic has a runtime twin (RV3701): at the moment a
14829
+ * round actually dispatches, the engine holds the money of the round's second judge pass
14830
+ * (this same `judge.estCost` first, else the run's own observed
14831
+ * post draft judge price) until that pass admits, so the
14832
+ * declared estimate is not only judged before the run but enforced
14833
+ * inside it.
14742
14834
  */
14743
14835
  onFound?: "report" | "carry" | "fail" | "repair";
14744
14836
  /**
@@ -15559,6 +15651,14 @@ interface AgentInvocationRow {
15559
15651
  replayed: boolean;
15560
15652
  /** True when the span's agent:end never arrived. */
15561
15653
  open: boolean;
15654
+ /**
15655
+ * Present and true when the invocation was aborted by the host's
15656
+ * finish rejection (RV3702): the declared finish contract rejected
15657
+ * the candidate past its repair bound, so the span died by host
15658
+ * hand with its wires fine. From the agent:end stamp; absent
15659
+ * everywhere else.
15660
+ */
15661
+ hostRejected?: boolean;
15562
15662
  phases: PhaseRow[];
15563
15663
  }
15564
15664
  /** The reduced table plus the per-role aggregate across every span. */
@@ -15669,6 +15769,15 @@ interface CriticalPath {
15669
15769
  synthesisShare?: number;
15670
15770
  /** Settled non-coordination agent spans that anchored the fan-in. */
15671
15771
  workerSpans: number;
15772
+ /**
15773
+ * Settled spans whose invocation was aborted by the host's finish
15774
+ * rejection (RV3702): the `hostRejected` stamps counted. The count
15775
+ * is unconditional (the stamp is self contained, no labelling
15776
+ * condition applies) and zero when none: on the third comparison
15777
+ * run's shape it reads 1, the round's composition, telling the host
15778
+ * rejection apart from a provider death at the cut level.
15779
+ */
15780
+ hostRejectedSpans: number;
15672
15781
  /** The RV710 decomposition of the window; present with postFanInMs. */
15673
15782
  postFanIn?: PostFanInBreakdown;
15674
15783
  }
package/dist/index.js CHANGED
@@ -7972,6 +7972,7 @@ var Replayer = class {
7972
7972
  if (patch.evidence !== void 0) entry.evidence = patch.evidence;
7973
7973
  if (patch.evidenceEntries !== void 0) entry.evidenceEntries = patch.evidenceEntries;
7974
7974
  if (patch.toolBudget !== void 0) entry.toolBudget = patch.toolBudget;
7975
+ if (patch.hostRejected !== void 0) entry.hostRejected = patch.hostRejected;
7975
7976
  if (patch.artifacts !== void 0) entry.artifacts = toJournalValue(patch.artifacts, "terminal artifacts");
7976
7977
  if (patch.escalation !== void 0) entry.escalation = toJournalValue(patch.escalation, "escalation report");
7977
7978
  if (patch.memoizeOutcome !== void 0) entry.memoizeOutcome = patch.memoizeOutcome;
@@ -9311,6 +9312,7 @@ function reduceInvocationTable(events) {
9311
9312
  row.usageApprox = event.usageApprox === true;
9312
9313
  row.retryCount = event.retryCount ?? 0;
9313
9314
  if (event.toolBudget !== void 0) row.toolBudget = event.toolBudget;
9315
+ if (event.hostRejected === true) row.hostRejected = true;
9314
9316
  totalCostUsd += event.costUsd;
9315
9317
  break;
9316
9318
  }
@@ -9331,6 +9333,20 @@ function reduceInvocationTable(events) {
9331
9333
  */
9332
9334
  const CLAIM_JUDGE_LABEL = "claim-consistency-judge";
9333
9335
  /**
9336
+ * The live abort reason of a finish rejection (RV3702): the value
9337
+ * `orchestrate()` aborts a composition invocation's signal with when
9338
+ * the declared finish contract rejects its candidate past the repair
9339
+ * bound, and ONLY then; a defective (throwing) validator aborts with
9340
+ * its own distinct reason, because a host defect is not a verdict on
9341
+ * the candidate. The settle layer reads the reason back and stamps
9342
+ * `hostRejected` onto the terminal agent entry and the live
9343
+ * `agent:end` event, so both span surfaces can tell a host rejection
9344
+ * (wires fine, document refused) from a provider failure without a
9345
+ * journal dig; the third comparison run's reader had exactly that
9346
+ * span (two successful wires, span cancelled) and nothing to name it.
9347
+ */
9348
+ const FINISH_REJECTION_ABORT_REASON = "rulvar:finish-validation";
9349
+ /**
9334
9350
  * Whether a synthesize span's label names a claim-consistency judge
9335
9351
  * invocation: the exact {@link CLAIM_JUDGE_LABEL}, or a suffixed
9336
9352
  * variant of it (the final pass dispatches under
@@ -9413,6 +9429,7 @@ function reduceCriticalPath(events) {
9413
9429
  let finalJudgeMs = 0;
9414
9430
  let compositionSpans = 0;
9415
9431
  let judgeSpans = 0;
9432
+ let hostRejectedSpans = 0;
9416
9433
  let firstCompositionEnd;
9417
9434
  let lastCompositionEnd;
9418
9435
  const coordinationModel = [];
@@ -9453,6 +9470,7 @@ function reduceCriticalPath(events) {
9453
9470
  case "agent:end": {
9454
9471
  const started = startBySpan.get(event.spanId);
9455
9472
  if (started === void 0) break;
9473
+ if (event.hostRejected === true) hostRejectedSpans += 1;
9456
9474
  if (started.role === "synthesize") {
9457
9475
  const wall = Math.max(0, at - started.at);
9458
9476
  const stage = claimJudgeStageOf(started.label);
@@ -9491,7 +9509,8 @@ function reduceCriticalPath(events) {
9491
9509
  finalJudgeMs,
9492
9510
  compositionSpans,
9493
9511
  judgeSpans,
9494
- workerSpans
9512
+ workerSpans,
9513
+ hostRejectedSpans
9495
9514
  };
9496
9515
  if (runStart !== void 0 && runEnd !== void 0) path.runWallMs = Math.max(0, runEnd - runStart);
9497
9516
  if (runStart !== void 0 && firstCompositionEnd !== void 0) path.firstCandidateMs = Math.max(0, firstCompositionEnd - runStart);
@@ -9583,6 +9602,7 @@ function criticalPathFromJournal(entries) {
9583
9602
  let lastWorkerEnd;
9584
9603
  let workerSpans = 0;
9585
9604
  let unclassifiedSpans = 0;
9605
+ let hostRejectedSpans = 0;
9586
9606
  let synthesisMs = 0;
9587
9607
  let finalCompositionMs = 0;
9588
9608
  let semanticJudgeMs = 0;
@@ -9602,6 +9622,7 @@ function criticalPathFromJournal(entries) {
9602
9622
  const last = endedAt ?? startedAt;
9603
9623
  if (last !== void 0) runEnd = runEnd === void 0 ? last : Math.max(runEnd, last);
9604
9624
  if (entry.kind !== "agent" || entry.status === "running" || entry.status === "suspended") continue;
9625
+ if (entry.hostRejected === true) hostRejectedSpans += 1;
9605
9626
  const role = entry.costAttribution?.role;
9606
9627
  if (role === void 0) {
9607
9628
  unclassifiedSpans += 1;
@@ -9649,7 +9670,8 @@ function criticalPathFromJournal(entries) {
9649
9670
  workerSpans,
9650
9671
  synthesisMs,
9651
9672
  unclassifiedSpans,
9652
- segments
9673
+ segments,
9674
+ hostRejectedSpans
9653
9675
  };
9654
9676
  const splitLegible = labelledSynthesis && !unlabelledSynthesis;
9655
9677
  if (splitLegible) {
@@ -14875,6 +14897,7 @@ var RunBudget = class {
14875
14897
  committedReserveUsd: 0,
14876
14898
  finalizeReserveUsd: 0,
14877
14899
  synthesisReserveUsd: 0,
14900
+ convergenceReserveUsd: 0,
14878
14901
  controller: new AbortController()
14879
14902
  };
14880
14903
  if (options.ceilingUsd !== void 0) root.ceilingUsd = options.ceilingUsd;
@@ -14925,6 +14948,7 @@ var RunBudget = class {
14925
14948
  committedReserveUsd: 0,
14926
14949
  finalizeReserveUsd: options.finalizeReserveUsd ?? 0,
14927
14950
  synthesisReserveUsd: 0,
14951
+ convergenceReserveUsd: 0,
14928
14952
  parentScope,
14929
14953
  controller: new AbortController()
14930
14954
  };
@@ -15025,7 +15049,8 @@ var RunBudget = class {
15025
15049
  spentUsd: account.spentUsd,
15026
15050
  committedReserveUsd: account.committedReserveUsd,
15027
15051
  finalizeReserveUsd: account.finalizeReserveUsd,
15028
- synthesisReserveUsd: account.synthesisReserveUsd
15052
+ synthesisReserveUsd: account.synthesisReserveUsd,
15053
+ convergenceReserveUsd: account.convergenceReserveUsd
15029
15054
  };
15030
15055
  if (account.ceilingUsd !== void 0) view.ceilingUsd = account.ceilingUsd;
15031
15056
  if (account.parentScope !== void 0) view.parentScope = account.parentScope;
@@ -15039,7 +15064,7 @@ var RunBudget = class {
15039
15064
  remainderOf(scope) {
15040
15065
  const account = this.accounts.get(scope);
15041
15066
  if (account?.ceilingUsd === void 0) return;
15042
- return Math.max(0, account.ceilingUsd - account.spentUsd - account.committedReserveUsd - account.finalizeReserveUsd - account.synthesisReserveUsd);
15067
+ return Math.max(0, account.ceilingUsd - account.spentUsd - account.committedReserveUsd - account.finalizeReserveUsd - account.synthesisReserveUsd - account.convergenceReserveUsd);
15043
15068
  }
15044
15069
  /**
15045
15070
  * The tightest allowance headroom on the chain of `scope`: the minimum
@@ -15119,15 +15144,16 @@ var RunBudget = class {
15119
15144
  }
15120
15145
  for (const account of this.chainOf(accountScope)) {
15121
15146
  if (account.ceilingUsd === void 0) continue;
15122
- const committed = account.spentUsd + account.committedReserveUsd + account.finalizeReserveUsd + account.synthesisReserveUsd;
15147
+ const committed = account.spentUsd + account.committedReserveUsd + account.finalizeReserveUsd + account.synthesisReserveUsd + account.convergenceReserveUsd;
15123
15148
  if (committed >= account.ceilingUsd || committed + reserveUsd > account.ceilingUsd) {
15124
15149
  if (account.scope === "run") this.exhaustedInternal = true;
15125
- throw new BudgetExhaustedError(`budget ceiling reached on account '${account.scope}': spent ${account.spentUsd.toFixed(4)} USD plus committed reserves ${(account.committedReserveUsd + account.finalizeReserveUsd).toFixed(4)} USD ` + (account.synthesisReserveUsd > 0 ? `plus the held synthesis reserve ${account.synthesisReserveUsd.toFixed(4)} USD ` : "") + `plus the proposed reserve ${reserveUsd.toFixed(4)} USD does not fit the ceiling ${account.ceilingUsd.toFixed(4)} USD`, { data: {
15150
+ throw new BudgetExhaustedError(`budget ceiling reached on account '${account.scope}': spent ${account.spentUsd.toFixed(4)} USD plus committed reserves ${(account.committedReserveUsd + account.finalizeReserveUsd).toFixed(4)} USD ` + (account.synthesisReserveUsd > 0 ? `plus the held synthesis reserve ${account.synthesisReserveUsd.toFixed(4)} USD ` : "") + (account.convergenceReserveUsd > 0 ? `plus the held convergence reserve ${account.convergenceReserveUsd.toFixed(4)} USD ` : "") + `plus the proposed reserve ${reserveUsd.toFixed(4)} USD does not fit the ceiling ${account.ceilingUsd.toFixed(4)} USD`, { data: {
15126
15151
  account: account.scope,
15127
15152
  spentUsd: account.spentUsd,
15128
15153
  committedReserveUsd: account.committedReserveUsd,
15129
15154
  finalizeReserveUsd: account.finalizeReserveUsd,
15130
15155
  synthesisReserveUsd: account.synthesisReserveUsd,
15156
+ convergenceReserveUsd: account.convergenceReserveUsd,
15131
15157
  proposedReserveUsd: reserveUsd,
15132
15158
  ceilingUsd: account.ceilingUsd
15133
15159
  } });
@@ -15219,6 +15245,41 @@ var RunBudget = class {
15219
15245
  account.synthesisReserveUsd = 0;
15220
15246
  this.emitUpdate();
15221
15247
  }
15248
+ /**
15249
+ * Registers the repair round's verdict reserve (RV3701, the third
15250
+ * comparison experiment's arc): absolute dollars held on the
15251
+ * orchestrator account AND the run root for the verdict pass (the
15252
+ * round's second judge invocation) that must follow a DISPATCHED
15253
+ * claim repair round. The third comparison run
15254
+ * proved the round's two invocation tail is only as convergent as
15255
+ * the money left when the candidate materializes; with the verdict
15256
+ * money held from the moment the round is admitted, the round's own
15257
+ * repair turns (the layer-2b clamp prices output from a remainder
15258
+ * this hold shrinks) and any concurrent admission (the hold joins
15259
+ * the projected admission sum) cannot eat it, so a round the budget
15260
+ * can only START is refused before any wire call instead of being
15261
+ * paid for and left unjudgeable. Exactly the synthesis reserve
15262
+ * mechanics: released to the invocation it was held FOR (the
15263
+ * verdict pass dispatch), never joined to the severing check.
15264
+ * Idempotent per account: registering again adjusts the root by the
15265
+ * delta.
15266
+ */
15267
+ commitConvergenceReserve(scope, reserveUsd) {
15268
+ const account = this.accounts.get(scope);
15269
+ if (account === void 0) throw new ConfigError(`unknown budget account '${scope}' for the convergence reserve`);
15270
+ const previous = account.convergenceReserveUsd;
15271
+ account.convergenceReserveUsd = reserveUsd;
15272
+ if (account.scope !== "run") this.root.convergenceReserveUsd = Math.max(0, this.root.convergenceReserveUsd + reserveUsd - previous);
15273
+ this.emitUpdate();
15274
+ }
15275
+ /** The verdict pass dispatch consumes its reserve; see commitConvergenceReserve. */
15276
+ releaseConvergenceReserve(scope) {
15277
+ const account = this.accounts.get(scope);
15278
+ if (account === void 0 || account.convergenceReserveUsd === 0) return;
15279
+ if (account.scope !== "run") this.root.convergenceReserveUsd = Math.max(0, this.root.convergenceReserveUsd - account.convergenceReserveUsd);
15280
+ account.convergenceReserveUsd = 0;
15281
+ this.emitUpdate();
15282
+ }
15222
15283
  /** The reserve is replaced by real spend when the spawn settles. */
15223
15284
  releaseReserve(reserveUsd, accountScope = "run") {
15224
15285
  for (const account of this.chainOf(accountScope)) account.committedReserveUsd = Math.max(0, account.committedReserveUsd - reserveUsd);
@@ -15426,7 +15487,7 @@ var RunBudget = class {
15426
15487
  let remaining;
15427
15488
  for (const account of this.chainOf(accountScope)) {
15428
15489
  if (account.ceilingUsd === void 0) continue;
15429
- const headroom = account.ceilingUsd - account.spentUsd - account.synthesisReserveUsd;
15490
+ const headroom = account.ceilingUsd - account.spentUsd - account.synthesisReserveUsd - account.convergenceReserveUsd;
15430
15491
  remaining = remaining === void 0 ? headroom : Math.min(remaining, headroom);
15431
15492
  }
15432
15493
  return remaining === void 0 ? void 0 : Math.max(0, remaining);
@@ -16865,6 +16926,33 @@ function persistedTerminalEnvelope(input) {
16865
16926
  //#endregion
16866
16927
  //#region src/engine/pricing-snapshot.ts
16867
16928
  /**
16929
+ * The applied-pricing snapshot (RV407, the eighth-experiment review).
16930
+ * `invoiceFromJournal` and `costReportFromJournal` price at fold time
16931
+ * from the table the caller passes, so a live price-table update used
16932
+ * to silently re-price HISTORY: the same journal folded to different
16933
+ * invoices before and after the change. When `createEngine({ pricing })`
16934
+ * is configured, the settling segment now pins what it actually
16935
+ * applied: the resolved pricing row of every model the journal used
16936
+ * (table rows plus the caps-fallback rows of models the table misses),
16937
+ * and the table's version, additively inside the existing run-settle
16938
+ * decision value (the `outputHash` precedent; no journal shape change).
16939
+ * The gate is deliberate: caps-fallback pricing arrives ambiently from
16940
+ * adapters, and a setting the user never enabled must not change the
16941
+ * journal, so table-less runs settle byte for byte as before.
16942
+ * `journalPricingSnapshot` reads the pin back and
16943
+ * rebuilds a `priceUsd` from the pinned rows, so a repeated fold after
16944
+ * the live table changed reproduces the original numbers exactly.
16945
+ *
16946
+ * The snapshot governs REPORTING folds (the CLI invoice and inspect
16947
+ * cost views, and any host that opts in by passing the rebuilt
16948
+ * priceUsd; since RV611 those consumers pass `composedPriceUsd`, the
16949
+ * same pin-plus-current-table composition the engine's outcome mirror
16950
+ * applies, so a stored fold and the settled outcome can never
16951
+ * disagree). The engine's own live pricing, budget admission, and the
16952
+ * journaled spend debits are untouched: they were always priced at
16953
+ * write time and never re-priced by a fold.
16954
+ */
16955
+ /**
16868
16956
  * A pinnable row: every present rate is a finite non-negative number.
16869
16957
  * The fold already treats a broken rate as unpriced (a NaN or negative
16870
16958
  * price never poisons the CostReport), so the pin mirrors exactly that
@@ -16902,6 +16990,42 @@ function snapshotJournalPricing(entries, pricingOf) {
16902
16990
  }
16903
16991
  return rows.length === 0 ? void 0 : rows;
16904
16992
  }
16993
+ /**
16994
+ * The pin's content hash (RV3703): sha256 over the canonical JSON of
16995
+ * the rows, so the derivation is byte-stable across folds, engines and
16996
+ * platforms; JCS fixes the key order, and the row order is the pin's
16997
+ * own (sorted at write, preserved at read).
16998
+ */
16999
+ function rowsHashOf(rows) {
17000
+ return createHash("sha256").update(jcsSerialize(rows), "utf8").digest("hex");
17001
+ }
17002
+ /**
17003
+ * The freshness range of a pin's dated rows (RV3703): oldest and
17004
+ * newest parsable `ratesVerifiedAt`, original strings preserved.
17005
+ * Undefined when no row carries a parsable date.
17006
+ */
17007
+ function ratesVerifiedRangeOf(rows) {
17008
+ let oldest;
17009
+ let newest;
17010
+ for (const row of rows) {
17011
+ const raw = row.rates.ratesVerifiedAt;
17012
+ if (raw === void 0) continue;
17013
+ const at = Date.parse(raw);
17014
+ if (!Number.isFinite(at)) continue;
17015
+ if (oldest === void 0 || at < oldest.at) oldest = {
17016
+ at,
17017
+ raw
17018
+ };
17019
+ if (newest === void 0 || at > newest.at) newest = {
17020
+ at,
17021
+ raw
17022
+ };
17023
+ }
17024
+ return oldest === void 0 || newest === void 0 ? void 0 : {
17025
+ oldest: oldest.raw,
17026
+ newest: newest.raw
17027
+ };
17028
+ }
16905
17029
  function pinnedRows(value) {
16906
17030
  const candidate = value?.pricing;
16907
17031
  if (!Array.isArray(candidate) || candidate.length === 0) return;
@@ -16953,16 +17077,24 @@ function journalPricingSnapshot(entries) {
16953
17077
  const rates = ratesFor(servedBy, seq);
16954
17078
  return rates === void 0 ? void 0 : priceUsdOf(rates, usage);
16955
17079
  };
17080
+ const lastRange = ratesVerifiedRangeOf(last.rows);
16956
17081
  return {
16957
17082
  ...last.pricingVersion === void 0 ? {} : { pricingVersion: last.pricingVersion },
16958
17083
  rows: last.rows,
17084
+ rowsHash: rowsHashOf(last.rows),
17085
+ ...lastRange === void 0 ? {} : { ratesVerifiedAt: lastRange },
16959
17086
  pinnedThroughSeq,
16960
- segments: pins.map((pin, index) => ({
16961
- fromSeq: index === 0 ? 0 : pins[index - 1]?.seq ?? 0,
16962
- settleSeq: pin.seq,
16963
- ...pin.pricingVersion === void 0 ? {} : { pricingVersion: pin.pricingVersion },
16964
- rows: pin.rows
16965
- })),
17087
+ segments: pins.map((pin, index) => {
17088
+ const range = ratesVerifiedRangeOf(pin.rows);
17089
+ return {
17090
+ fromSeq: index === 0 ? 0 : pins[index - 1]?.seq ?? 0,
17091
+ settleSeq: pin.seq,
17092
+ ...pin.pricingVersion === void 0 ? {} : { pricingVersion: pin.pricingVersion },
17093
+ rows: pin.rows,
17094
+ rowsHash: rowsHashOf(pin.rows),
17095
+ ...range === void 0 ? {} : { ratesVerifiedAt: range }
17096
+ };
17097
+ }),
16966
17098
  priceUsd,
16967
17099
  composedPriceUsd: (current) => (servedBy, usage, seq) => seq !== void 0 && seq < pinnedThroughSeq ? priceUsd(servedBy, usage, seq) ?? current(servedBy, usage) : current(servedBy, usage)
16968
17100
  };
@@ -18810,6 +18942,7 @@ function createCtx(internals, rootWorkflow) {
18810
18942
  costBasis: replayBasis,
18811
18943
  entryRef: terminal?.seq ?? matched.running.seq,
18812
18944
  ...terminal?.usageApprox === true ? { usageApprox: true } : {},
18945
+ ...terminal?.hostRejected === true ? { hostRejected: true } : {},
18813
18946
  ...result.exploration === void 0 ? {} : { exploration: result.exploration },
18814
18947
  ...result.toolBudget === void 0 ? {} : { toolBudget: result.toolBudget }
18815
18948
  }, spanId, true);
@@ -19538,6 +19671,8 @@ function createCtx(internals, rootWorkflow) {
19538
19671
  }
19539
19672
  };
19540
19673
  }
19674
+ const settleSignal = state.signal ?? internals.runSignal;
19675
+ if (result.status !== "ok" && settleSignal?.aborted === true && settleSignal.reason === "rulvar:finish-validation") terminalPatch.hostRejected = true;
19541
19676
  if (checkpointWritten) terminalPatch.checkpointRef = ckptRef;
19542
19677
  const terminal = await internals.replayer.appendTerminal(running.seq, terminalPatch);
19543
19678
  internals.events.emit({
@@ -19551,6 +19686,7 @@ function createCtx(internals, rootWorkflow) {
19551
19686
  entryRef: terminal.seq,
19552
19687
  ...resultUsageApprox ? { usageApprox: true } : {},
19553
19688
  ...result.transportRetries !== void 0 && result.transportRetries > 0 ? { retryCount: result.transportRetries } : {},
19689
+ ...terminalPatch.hostRejected === true ? { hostRejected: true } : {},
19554
19690
  ...result.quotaDenials === void 0 ? {} : { quotaDenials: result.quotaDenials },
19555
19691
  ...result.exploration === void 0 ? {} : { exploration: result.exploration },
19556
19692
  ...result.toolBudget === void 0 ? {} : { toolBudget: result.toolBudget }
@@ -24326,7 +24462,7 @@ function makeOrchestratorWorkflow(goal, opts) {
24326
24462
  verdict = validator.validate(input);
24327
24463
  } catch (thrown) {
24328
24464
  validationTermination = new ConfigError(`finish validator '${validator.name}' threw instead of returning a verdict: ` + (thrown instanceof Error ? thrown.message : String(thrown)));
24329
- validationAbort.abort("rulvar:finish-validation");
24465
+ validationAbort.abort("rulvar:finish-validation-defect");
24330
24466
  return {
24331
24467
  ok: false,
24332
24468
  feedback: { error: `finish validator '${validator.name}' is defective; the run fails` }
@@ -24389,7 +24525,7 @@ function makeOrchestratorWorkflow(goal, opts) {
24389
24525
  if (decision.verdict === "rejected") {
24390
24526
  if (contractGenerationCurrent(decision)) {
24391
24527
  validationTermination = finishValidationError(decision);
24392
- validationAbort.abort("rulvar:finish-validation");
24528
+ validationAbort.abort(FINISH_REJECTION_ABORT_REASON);
24393
24529
  }
24394
24530
  return {
24395
24531
  ok: false,
@@ -24924,6 +25060,15 @@ function makeOrchestratorWorkflow(goal, opts) {
24924
25060
  * not settle ok, because an empty list would claim the pool agreed.
24925
25061
  */
24926
25062
  let claimFindingsFound;
25063
+ /**
25064
+ * The observed price of this run's own latest post draft claim
25065
+ * judge pass (RV3701): the fallback sizing of the repair round's
25066
+ * convergence hold when the host declared no `judge.estCost`. By
25067
+ * the time the bounded round can dispatch, a post draft pass has
25068
+ * always settled (the round's findings came from it), so the
25069
+ * fallback is this run's own money, never an invented constant.
25070
+ */
25071
+ let observedFinalJudgeCostUsd;
24927
25072
  /** Set whenever the pass ran, findings or not (the RV1404 pairing). */
24928
25073
  let claimConsistencyMeta;
24929
25074
  /**
@@ -25208,6 +25353,7 @@ function makeOrchestratorWorkflow(goal, opts) {
25208
25353
  try {
25209
25354
  judged = await runtime.runInScope(judgeState, () => ctx.agent(judgePrompt, judgeOpts));
25210
25355
  noteInternalSettle(judged);
25356
+ if (stage !== "draft" && typeof judged.costUsd === "number") observedFinalJudgeCostUsd = judged.costUsd;
25211
25357
  } catch (declined) {
25212
25358
  if (!(declined instanceof BudgetExhaustedError)) throw declined;
25213
25359
  claimConsistencyMeta = finishMeta({
@@ -26391,6 +26537,9 @@ function makeOrchestratorWorkflow(goal, opts) {
26391
26537
  const hashOfDocument = (value) => createHash("sha256").update(jcsSerialize(value ?? null), "utf8").digest("hex");
26392
26538
  const preRepairHash = hashOfDocument(synthesizedFinal);
26393
26539
  const carried = claimFindingsFound;
26540
+ const convergenceHoldUsd = opts?.claimConsistency?.judge?.estCost ?? observedFinalJudgeCostUsd ?? 0;
26541
+ const convergenceScope = orchestratorAccount ?? "run";
26542
+ if (convergenceHoldUsd > 0) internals.budget.commitConvergenceReserve(convergenceScope, convergenceHoldUsd);
26394
26543
  try {
26395
26544
  synthesizedFinal = await runSynthesis(result.output);
26396
26545
  } catch (thrown) {
@@ -26422,8 +26571,21 @@ function makeOrchestratorWorkflow(goal, opts) {
26422
26571
  preRepairHash,
26423
26572
  ...acceptanceSnapshot
26424
26573
  } });
26574
+ } finally {
26575
+ if (convergenceHoldUsd > 0) internals.budget.releaseConvergenceReserve(convergenceScope);
26576
+ }
26577
+ try {
26578
+ await runClaimConsistencyPass(synthesizedFinal, acceptanceSnapshot, "final");
26579
+ } catch (thrown) {
26580
+ if (thrown instanceof FailRunError && typeof thrown.data === "object" && thrown.data !== null && !Array.isArray(thrown.data) && thrown.data.source === "orchestrator_claim_consistency") throw new FailRunError(thrown.message, { data: {
26581
+ ...thrown.data,
26582
+ ...thrown.data.claimContradictions === void 0 ? { claimContradictions: carried } : {},
26583
+ roundDispatched: true,
26584
+ repairsUsed: 1,
26585
+ preRepairHash
26586
+ } });
26587
+ throw thrown;
26425
26588
  }
26426
- await runClaimConsistencyPass(synthesizedFinal, acceptanceSnapshot, "final");
26427
26589
  if (claimFindingsFound !== void 0 && claimFindingsFound.length > 0) throw new FailRunError(`the claim-consistency judge still found ${String(claimFindingsFound.length)} contradiction${claimFindingsFound.length === 1 ? "" : "s"} after the bounded repair round: the repaired composition keeps contradicting the settled pool`, { data: {
26428
26590
  source: "orchestrator_claim_consistency",
26429
26591
  claimContradictions: claimFindingsFound,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.241.0",
3
+ "version": "1.242.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",