@rulvar/core 1.235.0 → 1.236.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
@@ -1217,6 +1217,15 @@ type RunMeta = {
1217
1217
  allowUnpriced?: string[];
1218
1218
  };
1219
1219
  /**
1220
+ * The host-declared config identity (RunOptions.configFingerprint,
1221
+ * RV3210): an opaque pin over what the workflow body closes over,
1222
+ * recorded at genesis and compared on every resume that asserts one.
1223
+ * Absent when the run declared none. A store that drops the field
1224
+ * degrades the check to the UNRECORDED warning, never a false pass
1225
+ * or a false refusal (absence means NOT RECORDED).
1226
+ */
1227
+ configFingerprint?: string;
1228
+ /**
1220
1229
  * Count of execution segments this run has STARTED (a fresh start
1221
1230
  * writes 1; every resume writes prior + 1, durably, BEFORE the
1222
1231
  * segment emits its first event). The engine derives each segment's
@@ -7784,6 +7793,24 @@ interface RunOptions {
7784
7793
  /** Explicit id; otherwise the engine mints a ULID. */
7785
7794
  runId?: string;
7786
7795
  /**
7796
+ * An opaque host-declared identity over the config the workflow body
7797
+ * CLOSES OVER (RV3210, the honest answer to `hashWorkflowBody`'s
7798
+ * closure blindness: the body-text hash cannot see captured values,
7799
+ * so two byte-identical bodies over different closures pin
7800
+ * identically). Recorded in RunMeta at genesis and compared on every
7801
+ * resume that supplies one: a mismatch refuses the resume typed
7802
+ * BEFORE ownership, meta writes, and appends, because the host
7803
+ * itself asserted the identity; a recorded fingerprint the resume
7804
+ * does not supply warns (`RULVAR_RESUME_FINGERPRINT_UNCHECKED`), and
7805
+ * a supplied fingerprint the run never recorded warns
7806
+ * (`RULVAR_RESUME_FINGERPRINT_UNRECORDED`) instead of failing,
7807
+ * because absence means NOT RECORDED. The preferred pattern is still
7808
+ * to close over nothing and pass config through args; the
7809
+ * fingerprint is the pin for what must stay closed over. A non-empty
7810
+ * string of at most 512 characters.
7811
+ */
7812
+ configFingerprint?: string;
7813
+ /**
7787
7814
  * Run ceiling B0; immutable after start. Enforced by projected
7788
7815
  * admission (a spawn whose reserve does not fit is denied before any
7789
7816
  * dispatch), the per-turn guard with a budget-derived maxOutputTokens
@@ -7931,6 +7958,18 @@ interface ResumeOptions {
7931
7958
  */
7932
7959
  bodyHash?: "warn" | "refuse";
7933
7960
  /**
7961
+ * The host's asserted config identity for this resume (RV3210),
7962
+ * compared against the RunMeta-recorded
7963
+ * {@link RunOptions.configFingerprint} BEFORE ownership, meta
7964
+ * writes, or any append. Both present and unequal is a typed
7965
+ * ConfigError always, no posture knob: supplying the fingerprint IS
7966
+ * the assertion. A recorded fingerprint the resume does not supply
7967
+ * warns (`RULVAR_RESUME_FINGERPRINT_UNCHECKED`); a supplied one the
7968
+ * run never recorded warns (`RULVAR_RESUME_FINGERPRINT_UNRECORDED`),
7969
+ * because absence means NOT RECORDED, never a verdict.
7970
+ */
7971
+ configFingerprint?: string;
7972
+ /**
7934
7973
  * Dry-run: replay-strict matching; the first would-be-live call throws
7935
7974
  * JournalMissError and the run settles with that typed error, zero live
7936
7975
  * calls performed.
@@ -14304,6 +14343,17 @@ interface PreflightOrchestratorSpec {
14304
14343
  * the finding entirely. Default 2.
14305
14344
  */
14306
14345
  headroomTurns?: number;
14346
+ /**
14347
+ * The `ceiling-headroom-thin` threshold as a fraction of the ceiling
14348
+ * (RV3208, the 2026-08-11 experiment's admission cliff: a $7.00
14349
+ * ceiling over a $6.80 required minimum left 2.86 percent headroom,
14350
+ * and a small pricing or context drift would have refused the whole
14351
+ * workflow at admission). The finding warns when
14352
+ * `ceilingHeadroomShare` sits below this fraction. A number in
14353
+ * [0, 1]; 0 (the default) keeps the finding silent, so declared
14354
+ * configs are byte identical until a host opts in.
14355
+ */
14356
+ minCeilingHeadroomShare?: number;
14307
14357
  }
14308
14358
  /** The full input: engine surface, run surface, and the declared wave. */
14309
14359
  interface PreflightInput {
@@ -14531,6 +14581,20 @@ interface PreflightReport {
14531
14581
  */
14532
14582
  requiredMinimumCeilingUsd?: number;
14533
14583
  /**
14584
+ * The ceiling minus the required minimum (RV3208): the absolute
14585
+ * dollars of drift the admission survives before the wave stops
14586
+ * seating. Present beside requiredMinimumCeilingUsd whenever a
14587
+ * ceiling is declared.
14588
+ */
14589
+ ceilingHeadroomUsd?: number;
14590
+ /**
14591
+ * The same headroom as a fraction of the ceiling (RV3208): the
14592
+ * one-field read of the admission cliff (the 2026-08-11 experiment
14593
+ * ran at 0.0286). Present beside ceilingHeadroomUsd on positive
14594
+ * ceilings.
14595
+ */
14596
+ ceilingHeadroomShare?: number;
14597
+ /**
14534
14598
  * The live-root-exposure term of the wave projection (RV2004): the
14535
14599
  * orchestrator's own worst-case turn floor, the money coordination
14536
14600
  * has ALWAYS already spent (and holds in flight) by the time any
package/dist/index.js CHANGED
@@ -26769,6 +26769,14 @@ function preflightEstimate(input) {
26769
26769
  code: "reserve-line-headroom",
26770
26770
  message: `the admitted wave's steady state sits ${reserveLineHeadroomUsd.toFixed(4)} USD under the reserve line ${reserveLineUsd.toFixed(4)} USD (the ceiling minus the synthesis reserve), less than ${String(headroomTurns)} coordination turn floors of headroom (${liveRootExposureTermUsd.toFixed(4)} USD each): child spend past the declared estimates eats that headroom, the coordination loop is then refused at the line, and the run settles partial with the synthesis redeemed from its reserve (RV2101); size the wave below the line or raise the ceiling to keep coordinating past it`
26771
26771
  });
26772
+ const ceilingHeadroomUsd = ceilingUsd === void 0 || requiredMinimumCeilingUsd === void 0 ? void 0 : ceilingUsd - requiredMinimumCeilingUsd;
26773
+ const ceilingHeadroomShare = ceilingHeadroomUsd === void 0 || ceilingUsd === void 0 || ceilingUsd <= 0 ? void 0 : ceilingHeadroomUsd / ceilingUsd;
26774
+ const minCeilingHeadroomShare = input.orchestrator?.minCeilingHeadroomShare ?? 0;
26775
+ if (ceilingHeadroomShare !== void 0 && minCeilingHeadroomShare > 0 && ceilingHeadroomShare < minCeilingHeadroomShare) say({
26776
+ severity: "warning",
26777
+ code: "ceiling-headroom-thin",
26778
+ message: `the ceiling headroom is ${(ceilingHeadroomShare * 100).toFixed(2)} percent of the ceiling (${(ceilingHeadroomUsd ?? 0).toFixed(4)} USD over the required minimum ${(requiredMinimumCeilingUsd ?? 0).toFixed(4)} USD), below the declared ${(minCeilingHeadroomShare * 100).toFixed(2)} percent floor: a small pricing or context drift refuses the whole wave at admission; raise the ceiling or slim the wave`
26779
+ });
26772
26780
  {
26773
26781
  const judgeEstUsd = input.orchestrator?.claimConsistency?.judge?.estCost;
26774
26782
  if (judgeEstUsd !== void 0 && effectiveCapUsd !== void 0 && synthesisHoldUsd > 0) {
@@ -27052,6 +27060,8 @@ function preflightEstimate(input) {
27052
27060
  reservedForFinalizationUsd,
27053
27061
  synthesisReserveUsd: synthesisHoldUsd,
27054
27062
  ...requiredMinimumCeilingUsd === void 0 ? {} : { requiredMinimumCeilingUsd },
27063
+ ...ceilingHeadroomUsd === void 0 ? {} : { ceilingHeadroomUsd },
27064
+ ...ceilingHeadroomShare === void 0 ? {} : { ceilingHeadroomShare },
27055
27065
  ...liveRootExposureTermUsd > 0 ? { liveRootExposureTermUsd } : {},
27056
27066
  ...reserveLineUsd === void 0 ? {} : { reserveLineUsd },
27057
27067
  ...reserveLineHeadroomUsd === void 0 ? {} : { reserveLineHeadroomUsd },
@@ -27588,6 +27598,10 @@ function parseDeadlineAt(value) {
27588
27598
  if (month < 1 || month > 12 || day < 1 || day > daysInMonth) refuse();
27589
27599
  return parsed;
27590
27600
  }
27601
+ /** Validates a declared config fingerprint (RV3210): a non-empty string of at most 512 chars. */
27602
+ function requireConfigFingerprint(value, site) {
27603
+ if (typeof value !== "string" || value.length === 0 || value.length > 512) throw new ConfigError(`${site} must be a non-empty string of at most 512 characters; got ` + (typeof value === "string" ? `${String(value.length)} characters` : JSON.stringify(value)));
27604
+ }
27591
27605
  /** Content hash of an in-process workflow body (run-to-definition binding). */
27592
27606
  function hashWorkflowBody(wf) {
27593
27607
  return createHash("sha256").update(wf.body.toString(), "utf8").digest("hex");
@@ -27894,6 +27908,7 @@ function createEngine(options) {
27894
27908
  if (wf.kind !== "workflow" && wf.kind !== "compiled-workflow") throw new ConfigError("engine.run accepts in-process Workflow values or compileScript CompiledWorkflow values");
27895
27909
  if (opts?.budgetUsd !== void 0) requireNonNegativeNumber(opts.budgetUsd, "RunOptions.budgetUsd");
27896
27910
  if (opts?.maxInFlightExposureUsd !== void 0) requireNonNegativeNumber(opts.maxInFlightExposureUsd, "RunOptions.maxInFlightExposureUsd");
27911
+ if (opts?.configFingerprint !== void 0) requireConfigFingerprint(opts.configFingerprint, "RunOptions.configFingerprint");
27897
27912
  if (opts?.clampTurnToExposure !== void 0 && typeof opts.clampTurnToExposure !== "boolean") throw new ConfigError("RunOptions.clampTurnToExposure must be a boolean; got " + JSON.stringify(opts.clampTurnToExposure));
27898
27913
  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));
27899
27914
  if (opts?.limits !== void 0) validateUsageLimits(opts.limits, "RunOptions.limits");
@@ -27929,6 +27944,7 @@ function createEngine(options) {
27929
27944
  ...opts.strictPricing.maxRatesAgeDays === void 0 ? {} : { maxRatesAgeDays: opts.strictPricing.maxRatesAgeDays },
27930
27945
  ...opts.strictPricing.allowUnpriced === void 0 ? {} : { allowUnpriced: [...opts.strictPricing.allowUnpriced] }
27931
27946
  };
27947
+ const configFingerprint = opts?.configFingerprint ?? resumeCtx?.configFingerprint;
27932
27948
  const makeBudget = () => new RunBudget({
27933
27949
  ...ceilingUsd === void 0 ? {} : { ceilingUsd },
27934
27950
  ...exposureCapUsd === void 0 ? {} : { maxInFlightExposureUsd: exposureCapUsd },
@@ -28109,6 +28125,7 @@ function createEngine(options) {
28109
28125
  ...ceilingUsd === void 0 ? {} : { budgetUsd: ceilingUsd },
28110
28126
  ...exposureCapUsd === void 0 ? {} : { maxInFlightExposureUsd: exposureCapUsd },
28111
28127
  ...strictPricing === void 0 ? {} : { strictPricing },
28128
+ ...configFingerprint === void 0 ? {} : { configFingerprint },
28112
28129
  ...argsBinding.argsProvided === void 0 ? {} : { argsProvided: argsBinding.argsProvided },
28113
28130
  ...argsBinding.argsHash === void 0 ? {} : { argsHash: argsBinding.argsHash },
28114
28131
  ...genesis === void 0 ? {} : { genesis },
@@ -28508,6 +28525,20 @@ function createEngine(options) {
28508
28525
  }
28509
28526
  bound = supplied;
28510
28527
  }
28528
+ {
28529
+ const supplied = resumeOptions?.configFingerprint;
28530
+ if (supplied !== void 0) requireConfigFingerprint(supplied, "ResumeOptions.configFingerprint");
28531
+ const recorded = typeof meta?.configFingerprint === "string" ? meta.configFingerprint : void 0;
28532
+ if (supplied !== void 0 && recorded !== void 0 && supplied !== recorded) throw new ConfigError(`resume: the supplied configFingerprint does not match the one run '${runId}' recorded at genesis; the config the workflow closes over changed, and the host declared exactly this check. Resume under the original config, or drop the option to proceed under the loud warning`);
28533
+ if (supplied !== void 0 && recorded === void 0) process.emitWarning(`resume: a configFingerprint was supplied but run '${runId}' never recorded one; the assertion cannot be verified (absence means NOT RECORDED)`, {
28534
+ code: "RULVAR_RESUME_FINGERPRINT_UNRECORDED",
28535
+ type: "RulvarWarning"
28536
+ });
28537
+ if (supplied === void 0 && recorded !== void 0) process.emitWarning(`resume: run '${runId}' recorded a configFingerprint at genesis and this resume did not supply one; the declared config identity goes unchecked`, {
28538
+ code: "RULVAR_RESUME_FINGERPRINT_UNCHECKED",
28539
+ type: "RulvarWarning"
28540
+ });
28541
+ }
28511
28542
  const priorEntries = (await journal.load(runId)).map((entry) => normalizeEntry(entry));
28512
28543
  scanJournalCompatibility(runId, priorEntries, buildDeriverRegistry(options.extraDerivers));
28513
28544
  if (priorEntries.some((entry) => entry.usageSemantics === void 0 && (entry.servedBy?.startsWith("openai:") === true && (entry.usage?.cacheWriteTokens ?? 0) > 0 || (entry.usageByModel?.some((slice) => slice.servedBy.startsWith("openai:") && slice.usage.cacheWriteTokens > 0) ?? false)))) process.emitWarning(`resume: run '${runId}' contains OpenAI cache-write usage recorded without a usage-semantics stamp. Entries written by rulvar v1.19.0 double-counted cache writes into inputTokens, so their recorded cost and budget debits are OVERSTATED; unstamped entries from v1.20.0 are correct. Resuming keeps the recorded debits. Audit procedure: https://docs.rulvar.com/guide/providers#openai-legacy-cache-journals`, {
@@ -28534,6 +28565,7 @@ function createEngine(options) {
28534
28565
  ...typeof meta?.argsHash === "string" ? { argsHash: meta.argsHash } : {},
28535
28566
  ...typeof meta?.genesis === "string" ? { genesis: meta.genesis } : {},
28536
28567
  ...typeof meta?.execKeyDerivation === "number" ? { execKeyDerivation: meta.execKeyDerivation } : {},
28568
+ ...typeof meta?.configFingerprint === "string" ? { configFingerprint: meta.configFingerprint } : {},
28537
28569
  previewResolve
28538
28570
  });
28539
28571
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.235.0",
3
+ "version": "1.236.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",