@rulvar/executor 1.102.0 → 1.104.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 +38 -15
- package/dist/index.js +186 -54
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -244,11 +244,22 @@ declare function containerExecutor(options: ContainerExecutorOptions): ToolExecu
|
|
|
244
244
|
* `subprocessExecutor({ ledger })` or `containerExecutor({ ledger })`;
|
|
245
245
|
* scan it back with {@link loadEffectLedger}. The first append lazily
|
|
246
246
|
* repairs a torn tail left by a crashed predecessor (RV502).
|
|
247
|
+
*
|
|
248
|
+
* Writer contract (RV606), stated publicly: appends are whole-line
|
|
249
|
+
* O_APPEND writes, and the destructive tail repair is mutually
|
|
250
|
+
* exclusive across processes (a sidecar `<path>.repair-lock` taken with
|
|
251
|
+
* O_EXCL, the file re-read after capture, a stale lock stolen after a
|
|
252
|
+
* ten-second TTL), so several writer processes on one LOCAL path can no
|
|
253
|
+
* longer truncate away each other's confirmed rows while repairing.
|
|
254
|
+
* Still, prefer ONE WRITER PER PATH, a `effects.<worker>.jsonl` file
|
|
255
|
+
* per worker process merged at reconciliation time: per-line append
|
|
256
|
+
* atomicity is a local-filesystem property, and neither O_APPEND nor
|
|
257
|
+
* O_EXCL is dependable on network filesystems.
|
|
247
258
|
*/
|
|
248
259
|
declare function jsonlEffectLedger(path: string, options?: {
|
|
249
260
|
now?: () => number;
|
|
250
261
|
}): ToolEffectLedger;
|
|
251
|
-
/** One
|
|
262
|
+
/** One malformed line of the ledger file, surfaced for triage. */
|
|
252
263
|
interface CorruptLedgerLine {
|
|
253
264
|
/** 1-based physical line number in the file. */
|
|
254
265
|
line: number;
|
|
@@ -256,7 +267,8 @@ interface CorruptLedgerLine {
|
|
|
256
267
|
offset: number;
|
|
257
268
|
/** sha256 (hex) of the raw line bytes: forensics without re-reading. */
|
|
258
269
|
sha256: string;
|
|
259
|
-
/** The first 120 characters of the line
|
|
270
|
+
/** The first 120 characters of the line (lossy-decoded when the bytes
|
|
271
|
+
* are not valid UTF-8; the hash pins the exact bytes). */
|
|
260
272
|
preview: string;
|
|
261
273
|
}
|
|
262
274
|
/** A torn fragment the writer quarantined while repairing a tail (RV502). */
|
|
@@ -267,12 +279,15 @@ interface TornLedgerArtifact {
|
|
|
267
279
|
recoveredAt: number;
|
|
268
280
|
}
|
|
269
281
|
/**
|
|
270
|
-
* The fail-closed refusal of {@link loadEffectLedger} (RV502
|
|
271
|
-
* holds at least one
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
282
|
+
* The fail-closed refusal of {@link loadEffectLedger} (RV502, widened
|
|
283
|
+
* by RV607): the file holds at least one line the scan cannot admit,
|
|
284
|
+
* unparseable bytes on an interior line, invalid UTF-8, a JSON value
|
|
285
|
+
* that is not an object, a missing or mistyped required field, or an
|
|
286
|
+
* unknown phase, none of which the writer's tail repair can produce, so
|
|
287
|
+
* it means external damage or a foreign writer, never a normal crash
|
|
288
|
+
* artifact. Reconciling from a partial scan would silently drop
|
|
289
|
+
* intents; triage the named lines instead (`tolerateCorrupt: true`
|
|
290
|
+
* surfaces them as data).
|
|
276
291
|
*/
|
|
277
292
|
declare class LedgerCorruptionError extends Error {
|
|
278
293
|
readonly lines: CorruptLedgerLine[];
|
|
@@ -296,9 +311,11 @@ interface EffectLedgerScan {
|
|
|
296
311
|
*/
|
|
297
312
|
orphanedIntents: ToolEffectIntent[];
|
|
298
313
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
314
|
+
* Lines the scan refused to admit (RV607): unparseable interior
|
|
315
|
+
* bytes, invalid UTF-8, non-object JSON, a missing or mistyped
|
|
316
|
+
* required field, or an unknown phase. Populated only under
|
|
317
|
+
* `tolerateCorrupt` (the default scan throws
|
|
318
|
+
* {@link LedgerCorruptionError} instead). Empty on a healthy file.
|
|
302
319
|
*/
|
|
303
320
|
corrupt: CorruptLedgerLine[];
|
|
304
321
|
/** Fragments the writer quarantined while repairing torn tails (RV502). */
|
|
@@ -306,7 +323,10 @@ interface EffectLedgerScan {
|
|
|
306
323
|
/**
|
|
307
324
|
* A live unterminated, unparseable trailing fragment: the artifact of
|
|
308
325
|
* a crash mid-write no writer has repaired yet. Tolerated and named,
|
|
309
|
-
* never silent.
|
|
326
|
+
* never silent. (An unterminated line that PARSES but fails the shape
|
|
327
|
+
* is corruption instead: a torn prefix of the writer's own flat
|
|
328
|
+
* record can never parse, so such a line is foreign, not a crash
|
|
329
|
+
* artifact.)
|
|
310
330
|
*/
|
|
311
331
|
tornTail?: {
|
|
312
332
|
preview: string;
|
|
@@ -316,9 +336,12 @@ interface EffectLedgerScan {
|
|
|
316
336
|
* Scans a JSONL ledger file into intents, outcomes, and the orphaned
|
|
317
337
|
* intents a host must reconcile, pairing attempts exactly (RV501). A
|
|
318
338
|
* torn TRAILING fragment (the crash-mid-write artifact) is tolerated
|
|
319
|
-
* and reported;
|
|
320
|
-
*
|
|
321
|
-
*
|
|
339
|
+
* and reported; everything else the scan cannot decode, parse, and
|
|
340
|
+
* validate, invalid UTF-8, non-object JSON, a missing required field,
|
|
341
|
+
* an unknown phase (RV607), fails the scan closed with a typed
|
|
342
|
+
* {@link LedgerCorruptionError} unless `tolerateCorrupt` asks for the
|
|
343
|
+
* lines as data (RV502). Under `tolerateCorrupt` the scan never throws
|
|
344
|
+
* anything rawer than that: a malformed line is data, not an exception.
|
|
322
345
|
*/
|
|
323
346
|
declare function loadEffectLedger(path: string, options?: {
|
|
324
347
|
tolerateCorrupt?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { appendFile, mkdtemp, readFile, rm, truncate } from "node:fs/promises";
|
|
2
|
+
import { appendFile, mkdtemp, readFile, rm, stat, truncate, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tool } from "@rulvar/core";
|
|
@@ -580,11 +580,12 @@ function containerExecutor(options) {
|
|
|
580
580
|
//#region src/ledger.ts
|
|
581
581
|
/**
|
|
582
582
|
* The durable JSONL reference of the two-phase effect ledger (RV404,
|
|
583
|
-
* hardened by RV501/RV502
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
583
|
+
* hardened by RV501/RV502, cross-process repair exclusion RV606,
|
|
584
|
+
* fail-closed scan validation RV607): one JSON line per phase, appended
|
|
585
|
+
* and awaited, so the intent row is on disk before the external effect
|
|
586
|
+
* is dispatched and survives a host process crash between the effect
|
|
587
|
+
* and the outcome write. What it is NOT: a transactional outbox,
|
|
588
|
+
* business authorization, or monetary reconciliation; those stay host
|
|
588
589
|
* obligations, this file is the strict interface the host reconciles
|
|
589
590
|
* FROM.
|
|
590
591
|
*
|
|
@@ -595,12 +596,15 @@ function containerExecutor(options) {
|
|
|
595
596
|
* the logical idempotency key is the host reconciler's job, against
|
|
596
597
|
* the effect provider's receipt.
|
|
597
598
|
*
|
|
598
|
-
* Recovery (RV502): before its first append the
|
|
599
|
-
* tail (the artifact of a crash mid-write): a
|
|
600
|
-
* only its newline is terminated in place, an
|
|
601
|
-
* truncated and quarantined verbatim as a
|
|
602
|
-
*
|
|
603
|
-
*
|
|
599
|
+
* Recovery (RV502, exclusive per RV606): before its first append the
|
|
600
|
+
* writer repairs a torn tail (the artifact of a crash mid-write): a
|
|
601
|
+
* complete record missing only its newline is terminated in place, an
|
|
602
|
+
* unparseable fragment is truncated and quarantined verbatim as a
|
|
603
|
+
* `{"phase":"torn"}` line. The destructive step runs under a sidecar
|
|
604
|
+
* O_EXCL lock and re-reads the file after capture, so two writer
|
|
605
|
+
* processes repairing the same file can no longer erase each other's
|
|
606
|
+
* confirmed rows. A later scan therefore treats an unparseable INTERIOR
|
|
607
|
+
* line as real damage and fails closed instead of skipping it silently.
|
|
604
608
|
*
|
|
605
609
|
* Durability boundary, stated honestly: an awaited append survives a
|
|
606
610
|
* process crash (the write has entered the kernel), not necessarily a
|
|
@@ -608,12 +612,96 @@ function containerExecutor(options) {
|
|
|
608
612
|
* durability wraps the seam over its own fsync or database write.
|
|
609
613
|
*/
|
|
610
614
|
const wallClock = Date.now.bind(globalThis);
|
|
615
|
+
/** How stale a repair lock's mtime must be before a writer may presume
|
|
616
|
+
* its holder crashed and steal it. Repairs take milliseconds; ten
|
|
617
|
+
* seconds is a crash verdict, not a performance budget. */
|
|
618
|
+
const REPAIR_LOCK_TTL_MS = 1e4;
|
|
619
|
+
/** How often a writer waiting on another process's repair re-checks. */
|
|
620
|
+
const REPAIR_POLL_MS = 25;
|
|
621
|
+
const delay = (ms) => new Promise((resolve) => {
|
|
622
|
+
setTimeout(resolve, ms);
|
|
623
|
+
});
|
|
624
|
+
/**
|
|
625
|
+
* The destructive half of the tail repair, mutually exclusive between
|
|
626
|
+
* processes (RV606): a sidecar `<path>.repair-lock` created with O_EXCL
|
|
627
|
+
* serializes repairers, and the ledger file is re-read AFTER the lock
|
|
628
|
+
* is held, so a boundary computed from a read another repairer has
|
|
629
|
+
* since invalidated is never truncated. A writer that loses the race
|
|
630
|
+
* polls; a lock whose mtime is further than the TTL from now (either
|
|
631
|
+
* direction, so a skewed clock cannot pin the file forever) is presumed
|
|
632
|
+
* abandoned by a crashed holder and stolen. The residual window (a
|
|
633
|
+
* holder stalled past the TTL, stolen mid-repair) is shrunk by
|
|
634
|
+
* re-verifying ownership immediately before the truncate; against a
|
|
635
|
+
* millisecond repair and a ten-second TTL it needs a scheduler pause no
|
|
636
|
+
* healthy host exhibits.
|
|
637
|
+
*/
|
|
638
|
+
async function repairUnderLock(path, now) {
|
|
639
|
+
const lockPath = `${path}.repair-lock`;
|
|
640
|
+
const token = `${String(process.pid)}-${randomUUID()}`;
|
|
641
|
+
for (;;) {
|
|
642
|
+
let acquired = true;
|
|
643
|
+
try {
|
|
644
|
+
await writeFile(lockPath, token, { flag: "wx" });
|
|
645
|
+
} catch (err) {
|
|
646
|
+
if (err.code !== "EEXIST") throw err;
|
|
647
|
+
acquired = false;
|
|
648
|
+
}
|
|
649
|
+
if (!acquired) {
|
|
650
|
+
let held;
|
|
651
|
+
try {
|
|
652
|
+
held = await stat(lockPath);
|
|
653
|
+
} catch (err) {
|
|
654
|
+
if (err.code !== "ENOENT") throw err;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (Math.abs(wallClock() - held.mtimeMs) > REPAIR_LOCK_TTL_MS) {
|
|
658
|
+
await rm(lockPath, { force: true });
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
await delay(REPAIR_POLL_MS);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
let raw;
|
|
666
|
+
try {
|
|
667
|
+
raw = await readFile(path);
|
|
668
|
+
} catch (err) {
|
|
669
|
+
if (err.code === "ENOENT") return;
|
|
670
|
+
throw err;
|
|
671
|
+
}
|
|
672
|
+
if (raw.length === 0 || raw[raw.length - 1] === 10) return;
|
|
673
|
+
const boundary = raw.lastIndexOf(10) + 1;
|
|
674
|
+
const fragment = raw.subarray(boundary).toString("utf8");
|
|
675
|
+
let parseable = true;
|
|
676
|
+
try {
|
|
677
|
+
JSON.parse(fragment);
|
|
678
|
+
} catch {
|
|
679
|
+
parseable = false;
|
|
680
|
+
}
|
|
681
|
+
if (parseable) {
|
|
682
|
+
await appendFile(path, "\n", "utf8");
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (await readFile(lockPath, "utf8").catch(() => "") !== token) continue;
|
|
686
|
+
await truncate(path, boundary);
|
|
687
|
+
await appendFile(path, `${JSON.stringify({
|
|
688
|
+
phase: "torn",
|
|
689
|
+
bytes: fragment,
|
|
690
|
+
recoveredAt: now()
|
|
691
|
+
})}\n`, "utf8");
|
|
692
|
+
return;
|
|
693
|
+
} finally {
|
|
694
|
+
if (await readFile(lockPath, "utf8").catch(() => "") === token) await rm(lockPath, { force: true });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
611
698
|
/**
|
|
612
699
|
* Repairs the file's tail so an append can never glue onto a torn
|
|
613
700
|
* fragment (RV502): a parseable unterminated record gets its newline, a
|
|
614
701
|
* torn fragment is truncated and preserved verbatim in a quarantine
|
|
615
|
-
* line old readers parse and skip.
|
|
616
|
-
*
|
|
702
|
+
* line old readers parse and skip. A clean file returns untouched, byte
|
|
703
|
+
* for byte, without the lock ever existing; the repair itself runs
|
|
704
|
+
* under the cross-process exclusion of {@link repairUnderLock} (RV606).
|
|
617
705
|
*/
|
|
618
706
|
async function repairTail(path, now) {
|
|
619
707
|
let raw;
|
|
@@ -624,20 +712,7 @@ async function repairTail(path, now) {
|
|
|
624
712
|
throw err;
|
|
625
713
|
}
|
|
626
714
|
if (raw.length === 0 || raw[raw.length - 1] === 10) return;
|
|
627
|
-
|
|
628
|
-
const fragment = raw.subarray(boundary).toString("utf8");
|
|
629
|
-
try {
|
|
630
|
-
JSON.parse(fragment);
|
|
631
|
-
} catch {
|
|
632
|
-
await truncate(path, boundary);
|
|
633
|
-
await appendFile(path, `${JSON.stringify({
|
|
634
|
-
phase: "torn",
|
|
635
|
-
bytes: fragment,
|
|
636
|
-
recoveredAt: now()
|
|
637
|
-
})}\n`, "utf8");
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
await appendFile(path, "\n", "utf8");
|
|
715
|
+
await repairUnderLock(path, now);
|
|
641
716
|
}
|
|
642
717
|
/**
|
|
643
718
|
* A two-phase ToolEffectLedger appending JSON lines to `path`
|
|
@@ -645,6 +720,17 @@ async function repairTail(path, now) {
|
|
|
645
720
|
* `subprocessExecutor({ ledger })` or `containerExecutor({ ledger })`;
|
|
646
721
|
* scan it back with {@link loadEffectLedger}. The first append lazily
|
|
647
722
|
* repairs a torn tail left by a crashed predecessor (RV502).
|
|
723
|
+
*
|
|
724
|
+
* Writer contract (RV606), stated publicly: appends are whole-line
|
|
725
|
+
* O_APPEND writes, and the destructive tail repair is mutually
|
|
726
|
+
* exclusive across processes (a sidecar `<path>.repair-lock` taken with
|
|
727
|
+
* O_EXCL, the file re-read after capture, a stale lock stolen after a
|
|
728
|
+
* ten-second TTL), so several writer processes on one LOCAL path can no
|
|
729
|
+
* longer truncate away each other's confirmed rows while repairing.
|
|
730
|
+
* Still, prefer ONE WRITER PER PATH, a `effects.<worker>.jsonl` file
|
|
731
|
+
* per worker process merged at reconciliation time: per-line append
|
|
732
|
+
* atomicity is a local-filesystem property, and neither O_APPEND nor
|
|
733
|
+
* O_EXCL is dependable on network filesystems.
|
|
648
734
|
*/
|
|
649
735
|
function jsonlEffectLedger(path, options) {
|
|
650
736
|
const now = options?.now ?? wallClock;
|
|
@@ -670,29 +756,60 @@ function jsonlEffectLedger(path, options) {
|
|
|
670
756
|
};
|
|
671
757
|
}
|
|
672
758
|
/**
|
|
673
|
-
* The fail-closed refusal of {@link loadEffectLedger} (RV502
|
|
674
|
-
* holds at least one
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
759
|
+
* The fail-closed refusal of {@link loadEffectLedger} (RV502, widened
|
|
760
|
+
* by RV607): the file holds at least one line the scan cannot admit,
|
|
761
|
+
* unparseable bytes on an interior line, invalid UTF-8, a JSON value
|
|
762
|
+
* that is not an object, a missing or mistyped required field, or an
|
|
763
|
+
* unknown phase, none of which the writer's tail repair can produce, so
|
|
764
|
+
* it means external damage or a foreign writer, never a normal crash
|
|
765
|
+
* artifact. Reconciling from a partial scan would silently drop
|
|
766
|
+
* intents; triage the named lines instead (`tolerateCorrupt: true`
|
|
767
|
+
* surfaces them as data).
|
|
679
768
|
*/
|
|
680
769
|
var LedgerCorruptionError = class extends Error {
|
|
681
770
|
lines;
|
|
682
771
|
constructor(path, lines) {
|
|
683
772
|
const first = lines[0];
|
|
684
|
-
super(`effect ledger '${path}' has ${String(lines.length)} corrupt
|
|
773
|
+
super(`effect ledger '${path}' has ${String(lines.length)} corrupt line(s); first at line ${String(first?.line)}, byte ${String(first?.offset)}, sha256 ${String(first?.sha256)}. A line the scan cannot decode, parse, and validate means external damage or a foreign writer; reconciliation must not proceed from a partial scan (loadEffectLedger with { tolerateCorrupt: true } surfaces the lines for triage).`);
|
|
685
774
|
this.name = "LedgerCorruptionError";
|
|
686
775
|
this.lines = lines;
|
|
687
776
|
}
|
|
688
777
|
};
|
|
778
|
+
/** Strict per-line decode (RV607): invalid UTF-8 is damage, never a
|
|
779
|
+
* replacement character that forges a key. */
|
|
780
|
+
const decodeStrict = (bytes) => new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
781
|
+
const isRecordObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
782
|
+
const hasIntentShape = (value) => typeof value.idempotencyKey === "string" && typeof value.runId === "string" && typeof value.spanId === "string" && typeof value.tool === "string" && typeof value.argsHash === "string" && typeof value.executor === "string" && typeof value.workdir === "string" && typeof value.startedAt === "number" && (value.attemptId === void 0 || typeof value.attemptId === "string");
|
|
783
|
+
const OUTCOME_CLASSES = /* @__PURE__ */ new Set([
|
|
784
|
+
"ok",
|
|
785
|
+
"error",
|
|
786
|
+
"timeout"
|
|
787
|
+
]);
|
|
788
|
+
/**
|
|
789
|
+
* Validates one parsed line's shape BEFORE anything downstream touches
|
|
790
|
+
* it (RV607): the phase must be exactly one of the three the writer
|
|
791
|
+
* emits, and every required field of that phase must carry its type
|
|
792
|
+
* (extra fields pass through untouched). Anything else, a primitive, a
|
|
793
|
+
* null, a missing field, an unknown phase, returns undefined and
|
|
794
|
+
* becomes a {@link CorruptLedgerLine}: forward compatibility with
|
|
795
|
+
* future phases is versioning's job, never silence's.
|
|
796
|
+
*/
|
|
797
|
+
function asLedgerLine(parsed) {
|
|
798
|
+
if (!isRecordObject(parsed)) return void 0;
|
|
799
|
+
if (parsed.phase === "intent") return hasIntentShape(parsed) ? parsed : void 0;
|
|
800
|
+
if (parsed.phase === "outcome") return hasIntentShape(parsed) && typeof parsed.durationMs === "number" && OUTCOME_CLASSES.has(parsed.outcome) && (typeof parsed.exitCode === "number" || parsed.exitCode === null) && (typeof parsed.signal === "string" || parsed.signal === null) ? parsed : void 0;
|
|
801
|
+
if (parsed.phase === "torn") return typeof parsed.bytes === "string" && typeof parsed.recoveredAt === "number" ? parsed : void 0;
|
|
802
|
+
}
|
|
689
803
|
/**
|
|
690
804
|
* Scans a JSONL ledger file into intents, outcomes, and the orphaned
|
|
691
805
|
* intents a host must reconcile, pairing attempts exactly (RV501). A
|
|
692
806
|
* torn TRAILING fragment (the crash-mid-write artifact) is tolerated
|
|
693
|
-
* and reported;
|
|
694
|
-
*
|
|
695
|
-
*
|
|
807
|
+
* and reported; everything else the scan cannot decode, parse, and
|
|
808
|
+
* validate, invalid UTF-8, non-object JSON, a missing required field,
|
|
809
|
+
* an unknown phase (RV607), fails the scan closed with a typed
|
|
810
|
+
* {@link LedgerCorruptionError} unless `tolerateCorrupt` asks for the
|
|
811
|
+
* lines as data (RV502). Under `tolerateCorrupt` the scan never throws
|
|
812
|
+
* anything rawer than that: a malformed line is data, not an exception.
|
|
696
813
|
*/
|
|
697
814
|
async function loadEffectLedger(path, options) {
|
|
698
815
|
const raw = await readFile(path);
|
|
@@ -703,6 +820,14 @@ async function loadEffectLedger(path, options) {
|
|
|
703
820
|
let tornTail;
|
|
704
821
|
let line = 0;
|
|
705
822
|
let start = 0;
|
|
823
|
+
const markCorrupt = (lineNo, offset, bytes, preview) => {
|
|
824
|
+
corrupt.push({
|
|
825
|
+
line: lineNo,
|
|
826
|
+
offset,
|
|
827
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
828
|
+
preview: preview.slice(0, 120)
|
|
829
|
+
});
|
|
830
|
+
};
|
|
706
831
|
for (let i = 0; i <= raw.length; i += 1) {
|
|
707
832
|
const atEnd = i === raw.length;
|
|
708
833
|
if (!atEnd && raw[i] !== 10) continue;
|
|
@@ -712,30 +837,37 @@ async function loadEffectLedger(path, options) {
|
|
|
712
837
|
start = i + 1;
|
|
713
838
|
if (atEnd && bytes.length === 0) break;
|
|
714
839
|
line += 1;
|
|
715
|
-
|
|
840
|
+
let text;
|
|
841
|
+
try {
|
|
842
|
+
text = decodeStrict(bytes);
|
|
843
|
+
} catch {
|
|
844
|
+
if (terminated) markCorrupt(line, offset, bytes, bytes.toString("utf8"));
|
|
845
|
+
else tornTail = { preview: bytes.toString("utf8").slice(0, 120) };
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
716
848
|
if (text.trim() === "") continue;
|
|
717
849
|
let parsed;
|
|
718
850
|
try {
|
|
719
851
|
parsed = JSON.parse(text);
|
|
720
852
|
} catch {
|
|
721
|
-
if (terminated)
|
|
722
|
-
line,
|
|
723
|
-
offset,
|
|
724
|
-
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
725
|
-
preview: text.slice(0, 120)
|
|
726
|
-
});
|
|
853
|
+
if (terminated) markCorrupt(line, offset, bytes, text);
|
|
727
854
|
else tornTail = { preview: text.slice(0, 120) };
|
|
728
855
|
continue;
|
|
729
856
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
857
|
+
const entry = asLedgerLine(parsed);
|
|
858
|
+
if (entry === void 0) {
|
|
859
|
+
markCorrupt(line, offset, bytes, text);
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
if (entry.phase === "intent") {
|
|
863
|
+
const { phase: _phase, ...rest } = entry;
|
|
864
|
+
intents.push(rest);
|
|
865
|
+
} else if (entry.phase === "outcome") {
|
|
866
|
+
const { phase: _phase, ...rest } = entry;
|
|
867
|
+
outcomes.push(rest);
|
|
868
|
+
} else tornArtifacts.push({
|
|
869
|
+
bytes: entry.bytes,
|
|
870
|
+
recoveredAt: entry.recoveredAt
|
|
739
871
|
});
|
|
740
872
|
}
|
|
741
873
|
if (corrupt.length > 0 && options?.tolerateCorrupt !== true) throw new LedgerCorruptionError(path, corrupt);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/executor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.104.0",
|
|
4
4
|
"description": "Rulvar isolated tool executors: reference ToolExecutorProvider adapters that run tool work out of process (subprocess and container) so hostile or model-generated scripts cannot reach host capabilities.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@rulvar/core": "1.
|
|
25
|
+
"@rulvar/core": "1.104.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.20.1",
|
|
29
29
|
"tsdown": "^0.22.14",
|
|
30
30
|
"typescript": "~6.0.3",
|
|
31
|
-
"@rulvar/testing": "1.
|
|
31
|
+
"@rulvar/testing": "1.104.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|