@rulvar/core 1.108.0 → 1.109.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
@@ -5973,7 +5973,11 @@ declare class AdmissionController {
5973
5973
  * Folds the per-run attribution buckets into the normative CostReport.
5974
5974
  * Live attribution buckets never see abandoned subtrees, so a host
5975
5975
  * that tracked abandoned spend itself passes it as `abandoned`;
5976
- * omitted, the report shows a gross equal to the net.
5976
+ * omitted, the report shows a gross equal to the net. Non-finite
5977
+ * numbers anywhere in the inputs are a typed refusal (RV705): this
5978
+ * exported builder is the same public surface as
5979
+ * {@link costReportFromJournal} and holds the same RV610 doctrine,
5980
+ * instead of letting an Infinity or NaN serialize into null downstream.
5977
5981
  */
5978
5982
  declare function buildCostReport(attribution: CostAttribution, totalUsd: number, abandoned?: CostReport["abandoned"]): CostReport;
5979
5983
  /**
@@ -9430,6 +9434,13 @@ declare class JsonlFileStore implements MetaLookupStore {
9430
9434
  private metaPath;
9431
9435
  append(runId: string, e: JournalEntry): Promise<void>;
9432
9436
  load(runId: string): Promise<JournalEntry[]>;
9437
+ /**
9438
+ * Restores the trailing '\n' of a parseable-but-unterminated tail
9439
+ * (RV701). One byte appended in place terminates the record exactly
9440
+ * where the crash left it; the file's bytes before it stay untouched.
9441
+ * No-op on a missing, empty, or already-terminated journal.
9442
+ */
9443
+ private terminateUnterminatedTail;
9433
9444
  private repairTornTail;
9434
9445
  putMeta(m: RunMeta): Promise<void>;
9435
9446
  getMeta(runId: string): Promise<RunMeta | undefined>;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createCipheriv, createDecipheriv, createHash, createHmac, getRandomValues, hkdfSync, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
2
- import { appendFileSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { appendFileSync, closeSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import path, { dirname, join, resolve, sep } from "node:path";
4
4
  import { Client } from "@modelcontextprotocol/sdk/client";
5
5
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -8080,7 +8080,13 @@ async function reconcileRunMeta(store, runId, opts) {
8080
8080
  *
8081
8081
  * Contract (DEF-4 tightening):
8082
8082
  * - A1 atomicity: a torn trailing line (crash mid-append) is never
8083
- * visible in load; it is dropped and overwritten by the next append.
8083
+ * visible in load; the incomplete fragment is dropped and overwritten
8084
+ * by the next append. Whole records on that line are data, never
8085
+ * fragment (RV701): a crash that persisted every JSON byte but not
8086
+ * the '\n' leaves a parseable tail that load serves and append
8087
+ * terminates before writing, and repair salvages complete records a
8088
+ * glued line carries instead of discarding the line, so an entry a
8089
+ * load has served can never be un-served by a later repair.
8084
8090
  * - A2 total per-run order: load returns append order, stable across
8085
8091
  * calls (the kernel's per-run queue serializes appends).
8086
8092
  * - A3 read-your-writes: append resolves after the line is written.
@@ -8097,6 +8103,62 @@ function safeName(runId) {
8097
8103
  if (!/^[A-Za-z0-9._-]+$/.test(runId)) throw new JournalOrderViolation(`JsonlFileStore: runId '${runId}' is not filesystem-safe ([A-Za-z0-9._-] only)`);
8098
8104
  return runId;
8099
8105
  }
8106
+ /**
8107
+ * Whole JSON values glued on one line, split apart without parser
8108
+ * ambiguity (RV701): depth is tracked outside string literals only, and
8109
+ * every candidate must still round-trip JSON.parse. A line that is not a
8110
+ * clean concatenation from its first byte salvages its whole prefix
8111
+ * values and returns everything after them as the torn fragment, so the
8112
+ * caller keeps accepted records and drops exactly the unacknowledged
8113
+ * tail a crash tore.
8114
+ */
8115
+ function splitConcatenatedJson(line) {
8116
+ const whole = [];
8117
+ let start = 0;
8118
+ let depth = 0;
8119
+ let inString = false;
8120
+ let escaped = false;
8121
+ for (let i = 0; i < line.length; i += 1) {
8122
+ const ch = line[i];
8123
+ if (inString) {
8124
+ if (escaped) escaped = false;
8125
+ else if (ch === "\\") escaped = true;
8126
+ else if (ch === "\"") inString = false;
8127
+ continue;
8128
+ }
8129
+ if (ch === "\"") {
8130
+ inString = true;
8131
+ continue;
8132
+ }
8133
+ if (ch === "{" || ch === "[") {
8134
+ depth += 1;
8135
+ continue;
8136
+ }
8137
+ if (ch === "}" || ch === "]") {
8138
+ depth -= 1;
8139
+ if (depth < 0) return {
8140
+ whole,
8141
+ fragment: line.slice(start)
8142
+ };
8143
+ if (depth === 0) {
8144
+ const candidate = line.slice(start, i + 1);
8145
+ try {
8146
+ whole.push(JSON.parse(candidate));
8147
+ } catch {
8148
+ return {
8149
+ whole,
8150
+ fragment: line.slice(start)
8151
+ };
8152
+ }
8153
+ start = i + 1;
8154
+ }
8155
+ }
8156
+ }
8157
+ return {
8158
+ whole,
8159
+ fragment: line.slice(start)
8160
+ };
8161
+ }
8100
8162
  var JsonlFileStore = class {
8101
8163
  dir;
8102
8164
  /**
@@ -8119,6 +8181,7 @@ var JsonlFileStore = class {
8119
8181
  let tail = this.lastSeq.get(runId);
8120
8182
  if (tail === void 0) {
8121
8183
  const existing = await this.load(runId);
8184
+ this.terminateUnterminatedTail(runId);
8122
8185
  const last = existing[existing.length - 1];
8123
8186
  tail = last !== void 0 && Number.isFinite(last.seq) ? last.seq : Number.NEGATIVE_INFINITY;
8124
8187
  this.lastSeq.set(runId, tail);
@@ -8144,6 +8207,7 @@ var JsonlFileStore = class {
8144
8207
  entries.push(JSON.parse(line));
8145
8208
  } catch (thrown) {
8146
8209
  if (lines.slice(i + 1).every((rest) => rest === "")) {
8210
+ for (const value of splitConcatenatedJson(line).whole) entries.push(value);
8147
8211
  this.repairTornTail(runId, entries);
8148
8212
  break;
8149
8213
  }
@@ -8152,6 +8216,34 @@ var JsonlFileStore = class {
8152
8216
  }
8153
8217
  return entries;
8154
8218
  }
8219
+ /**
8220
+ * Restores the trailing '\n' of a parseable-but-unterminated tail
8221
+ * (RV701). One byte appended in place terminates the record exactly
8222
+ * where the crash left it; the file's bytes before it stay untouched.
8223
+ * No-op on a missing, empty, or already-terminated journal.
8224
+ */
8225
+ terminateUnterminatedTail(runId) {
8226
+ const path = this.journalPath(runId);
8227
+ let fd;
8228
+ try {
8229
+ fd = openSync(path, "r");
8230
+ } catch (thrown) {
8231
+ if (thrown.code === "ENOENT") return;
8232
+ throw thrown;
8233
+ }
8234
+ let needsNewline = false;
8235
+ try {
8236
+ const size = fstatSync(fd).size;
8237
+ if (size > 0) {
8238
+ const lastByte = /* @__PURE__ */ new Uint8Array(1);
8239
+ readSync(fd, lastByte, 0, 1, size - 1);
8240
+ needsNewline = lastByte[0] !== 10;
8241
+ }
8242
+ } finally {
8243
+ closeSync(fd);
8244
+ }
8245
+ if (needsNewline) appendFileSync(path, "\n", "utf8");
8246
+ }
8155
8247
  repairTornTail(runId, whole) {
8156
8248
  const path = this.journalPath(runId);
8157
8249
  const temp = `${path}.tmp`;
@@ -8308,7 +8400,11 @@ function isOrchestratorAccount(scope) {
8308
8400
  * Folds the per-run attribution buckets into the normative CostReport.
8309
8401
  * Live attribution buckets never see abandoned subtrees, so a host
8310
8402
  * that tracked abandoned spend itself passes it as `abandoned`;
8311
- * omitted, the report shows a gross equal to the net.
8403
+ * omitted, the report shows a gross equal to the net. Non-finite
8404
+ * numbers anywhere in the inputs are a typed refusal (RV705): this
8405
+ * exported builder is the same public surface as
8406
+ * {@link costReportFromJournal} and holds the same RV610 doctrine,
8407
+ * instead of letting an Infinity or NaN serialize into null downstream.
8312
8408
  */
8313
8409
  function buildCostReport(attribution, totalUsd, abandoned = {
8314
8410
  usd: 0,
@@ -8322,7 +8418,7 @@ function buildCostReport(attribution, totalUsd, abandoned = {
8322
8418
  forcedFinish: false,
8323
8419
  reserveUsedUsd: 0
8324
8420
  };
8325
- return {
8421
+ const report = {
8326
8422
  totalUsd,
8327
8423
  grossUsd: totalUsd + abandoned.usd,
8328
8424
  abandoned,
@@ -8336,6 +8432,8 @@ function buildCostReport(attribution, totalUsd, abandoned = {
8336
8432
  },
8337
8433
  unpriced: attribution.unpriced
8338
8434
  };
8435
+ requireFiniteNumbersDeep(report, "costReport");
8436
+ return report;
8339
8437
  }
8340
8438
  /**
8341
8439
  * The pure journal fold: the complete CostReport from terminal entries,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.108.0",
3
+ "version": "1.109.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",