@sema-agent/core 7.29.0 → 7.30.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/CHANGELOG.md +361 -0
- package/dist/agents/child-model-seat.d.ts +59 -7
- package/dist/agents/child-model-seat.js +33 -7
- package/dist/agents/cross-session-judge.d.ts +27 -5
- package/dist/agents/cross-session-judge.js +23 -12
- package/dist/agents/list-agents-tool.d.ts +31 -6
- package/dist/agents/list-agents-tool.js +66 -14
- package/dist/agents/peer-admission.d.ts +20 -8
- package/dist/agents/peer-admission.js +21 -12
- package/dist/agents/peer-directory.d.ts +29 -0
- package/dist/agents/peer-directory.js +20 -0
- package/dist/agents/peer-held-queue.d.ts +31 -4
- package/dist/agents/peer-held-queue.js +32 -1
- package/dist/agents/peer-session-drain.d.ts +91 -13
- package/dist/agents/peer-session-drain.js +409 -102
- package/dist/agents/read-inbox-tool.d.ts +22 -0
- package/dist/agents/read-inbox-tool.js +39 -0
- package/dist/agents/roster-store.d.ts +2 -4
- package/dist/agents/roster-store.js +1 -1
- package/dist/agents/send-message-tool.d.ts +4 -4
- package/dist/agents/send-message-tool.js +22 -2
- package/dist/agents/subagent.js +62 -44
- package/dist/agents/team-role.d.ts +34 -0
- package/dist/agents/team-role.js +17 -0
- package/dist/agents/verify.js +2 -2
- package/dist/core/auto-compaction.d.ts +42 -0
- package/dist/core/auto-compaction.js +35 -2
- package/dist/core/child-clamp-seats.d.ts +11 -0
- package/dist/core/child-clamp-seats.js +3 -0
- package/dist/core/context-edit.d.ts +3 -2
- package/dist/core/context-edit.js +6 -5
- package/dist/core/context-guard.d.ts +2 -1
- package/dist/core/context-guard.js +3 -2
- package/dist/core/context-settings.d.ts +103 -0
- package/dist/core/context-settings.js +60 -0
- package/dist/core/engine-notice.d.ts +11 -1
- package/dist/core/frame-settlement.d.ts +97 -0
- package/dist/core/frame-settlement.js +36 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +10 -10
- package/dist/core/mailbox-store.d.ts +41 -7
- package/dist/core/mailbox-store.js +9 -3
- package/dist/core/parent-spec-seat.d.ts +14 -1
- package/dist/core/parent-spec-seat.js +6 -0
- package/dist/core/permission-rule-model.js +1 -1
- package/dist/core/roles.d.ts +20 -0
- package/dist/core/roles.js +35 -16
- package/dist/core/runner/assemble-result.d.ts +2 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/compaction-call-options.d.ts +26 -2
- package/dist/core/runner/compaction-call-options.js +9 -0
- package/dist/core/runner/compaction-seams.d.ts +4 -3
- package/dist/core/runner/compaction-seams.js +13 -6
- package/dist/core/runner/contracts.d.ts +56 -7
- package/dist/core/runner/decide-continuation.d.ts +2 -1
- package/dist/core/runner/decide-continuation.js +11 -10
- package/dist/core/runner/initial-run-state.js +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +9 -1
- package/dist/core/runner/prepare-config-doors.js +4 -1
- package/dist/core/runner/prepare-context-lane.d.ts +9 -2
- package/dist/core/runner/prepare-context-lane.js +15 -10
- package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
- package/dist/core/runner/prepare-delegation-surface.js +9 -3
- package/dist/core/runner/prepare-file-history.js +10 -8
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
- package/dist/core/runner/prepare-gate-stations.js +3 -1
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +6 -0
- package/dist/core/runner/prepare-prompt-inputs.js +2 -1
- package/dist/core/runner/prepare-task.js +22 -9
- package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -1
- package/dist/core/runner/prepare-wiring-manifest.js +6 -1
- package/dist/core/runner/retired-runner-deps.d.ts +3 -1
- package/dist/core/runner/retired-runner-deps.js +1 -0
- package/dist/core/runner/run-compaction-machinery.d.ts +4 -2
- package/dist/core/runner/run-compaction-machinery.js +5 -3
- package/dist/core/runner/run-harness-handlers.js +1 -1
- package/dist/core/runner/run-identity-wiring.js +19 -1
- package/dist/core/runner/run-leg.js +1 -1
- package/dist/core/runner/run-notification-lane.js +8 -2
- package/dist/core/runner/run-recovery-lanes.js +9 -5
- package/dist/core/runner/run-settle-and-teardown.d.ts +4 -22
- package/dist/core/runner/run-settle-and-teardown.js +6 -10
- package/dist/core/runner/run-stop-and-final-verify.js +1 -0
- package/dist/core/runner/run-terminal-adoption.js +5 -5
- package/dist/core/runner/run-turn-boundary.js +32 -5
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +32 -1
- package/dist/core/runner-deps.d.ts +44 -13
- package/dist/core/swappable-deps.d.ts +20 -4
- package/dist/core/swappable-deps.js +6 -1
- package/dist/core/task-event.d.ts +5 -0
- package/dist/core/task-notification.d.ts +144 -2
- package/dist/core/task-notification.js +154 -5
- package/dist/core/task-result.d.ts +14 -0
- package/dist/core/task-spec.d.ts +7 -0
- package/dist/core/tool-catalog-entries.js +2 -1
- package/dist/core/tool-conformance.js +1 -0
- package/dist/core/tool-policy.d.ts +9 -4
- package/dist/core/tool-spec.d.ts +14 -0
- package/dist/core/wiring-manifest.d.ts +12 -0
- package/dist/core/wiring-manifest.js +5 -1
- package/dist/engine/harness/agent-harness.d.ts +14 -1
- package/dist/engine/harness/agent-harness.js +28 -3
- package/dist/engine/harness/types.d.ts +3 -1
- package/dist/engine/llm/types.d.ts +8 -7
- package/dist/engine/llm/types.js +1 -1
- package/dist/engine/session/import-validate.js +4 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.js +7 -5
- package/dist/orchestration/run-workflow-tool.d.ts +4 -4
- package/dist/orchestration/workflow.d.ts +1 -1
- package/dist/orchestration/workflow.js +8 -5
- package/dist/shell/lex.d.ts +30 -51
- package/dist/shell/lex.js +237 -284
- package/dist/stores/cc/mailbox-store.js +3 -2
- package/dist/stores/file/corrupt-read-sink.d.ts +28 -2
- package/dist/stores/file/file-history-store.d.ts +2 -4
- package/dist/stores/file/file-history-store.js +1 -1
- package/dist/stores/file/fs-atomic.d.ts +16 -11
- package/dist/stores/file/fs-atomic.js +17 -6
- package/dist/stores/file/index.d.ts +5 -6
- package/dist/stores/file/index.js +1 -1
- package/dist/stores/file/mailbox-store.d.ts +4 -9
- package/dist/stores/file/mailbox-store.js +29 -30
- package/dist/stores/file/session-policy-store.d.ts +2 -0
- package/dist/stores/file/session-policy-store.js +1 -1
- package/dist/stores/file/session-store.d.ts +11 -7
- package/dist/stores/file/session-store.js +23 -16
- package/dist/stores/file/shared-ledger.js +3 -14
- package/dist/stores/file/tool-result-store.d.ts +2 -0
- package/dist/stores/file/tool-result-store.js +5 -2
- package/dist/stores/file/workflow-journal-store.js +14 -9
- package/dist/tools/fs/bash-program-position.d.ts +15 -24
- package/dist/tools/fs/bash-program-position.js +11 -29
- package/dist/tools/fs/bash-readonly-classifier.d.ts +125 -117
- package/dist/tools/fs/bash-readonly-classifier.js +400 -272
- package/dist/tools/fs/shell-read-check.js +3 -3
- package/dist/tools/fs/shell-read-establish.js +1 -1
- package/dist/tools/fs/shell-read-plan.d.ts +11 -6
- package/dist/tools/fs/shell-read-plan.js +167 -183
- package/dist/tools/fs/shell-read-word.d.ts +50 -0
- package/dist/tools/fs/shell-read-word.js +59 -0
- package/dist/tools/fs/shell-reentry-text.d.ts +10 -0
- package/dist/tools/fs/shell-reentry-text.js +73 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +58 -8
- package/wire-changes/core@7.30.0.json +1821 -0
|
@@ -202,9 +202,9 @@ export function createCcFileMailboxStore(opts) {
|
|
|
202
202
|
const path = inboxPath(handle);
|
|
203
203
|
const held = leases.get(leaseKey(handle));
|
|
204
204
|
if (held === undefined || held.owner !== owner)
|
|
205
|
-
return Promise.resolve();
|
|
205
|
+
return Promise.resolve({ acked: false });
|
|
206
206
|
if (!existsSync(path))
|
|
207
|
-
return Promise.resolve();
|
|
207
|
+
return Promise.resolve({ acked: false });
|
|
208
208
|
const notes = deferredNotes();
|
|
209
209
|
return withCcLock(path, () => {
|
|
210
210
|
const box = loadBox(path, notes.note);
|
|
@@ -219,6 +219,7 @@ export function createCcFileMailboxStore(opts) {
|
|
|
219
219
|
saveBox(path, box);
|
|
220
220
|
if (held.maxSeq <= upToSeq)
|
|
221
221
|
leases.delete(leaseKey(handle));
|
|
222
|
+
return { acked: true };
|
|
222
223
|
}).finally(() => {
|
|
223
224
|
notes.flush();
|
|
224
225
|
});
|
|
@@ -4,8 +4,34 @@
|
|
|
4
4
|
* hand-written four times. Three copies contained an async sink's rejection; the mailbox's caught only a
|
|
5
5
|
* synchronous throw, so an `async` sink there became an unhandled rejection.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import type { CorruptBodyForm } from "../../core/store-contracts/read-back-shape-contract.js";
|
|
8
|
+
/** One disclosure. Every level carries `path` + `reason`; `kind` names the level. */
|
|
9
|
+
export type CorruptReadInfo = CorruptRecordRead | CorruptReplayTally | CorruptFileRead;
|
|
10
|
+
/** One JSONL record a replay skipped: fired per skipped record, in file order, before its read's tally. */
|
|
11
|
+
export interface CorruptRecordRead {
|
|
12
|
+
kind: "record";
|
|
13
|
+
path: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
/** 1-based line of the skipped record. */
|
|
16
|
+
line: number;
|
|
17
|
+
form: CorruptBodyForm;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The count of one replay that skipped at least one record, fired once after its record disclosures; a
|
|
21
|
+
* read that skipped nothing fires none. `total` = `kept` + `dropped` (an unterminated crash tail is not a
|
|
22
|
+
* record); `kept === 0` = nothing in the file was readable.
|
|
23
|
+
*/
|
|
24
|
+
export interface CorruptReplayTally {
|
|
25
|
+
kind: "replay";
|
|
26
|
+
path: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
kept: number;
|
|
29
|
+
dropped: number;
|
|
30
|
+
total: number;
|
|
31
|
+
}
|
|
32
|
+
/** A disclosure about a file or directory as a whole, outside any JSONL replay. */
|
|
33
|
+
export interface CorruptFileRead {
|
|
34
|
+
kind: "file";
|
|
9
35
|
path: string;
|
|
10
36
|
reason: string;
|
|
11
37
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
2
|
import { type FileHistoryDiffStats, type FileHistoryExport, type FileHistoryResult, type FileHistoryRestoreResult, type FileHistoryStore, type FileHistoryTrackResult } from "../../core/file-history-store.js";
|
|
3
3
|
import { type FileHistoryRetentionPolicy } from "../../core/file-history-retention.js";
|
|
4
|
+
import { type CorruptReadInfo } from "./corrupt-read-sink.js";
|
|
4
5
|
/**
|
|
5
6
|
* design/381 slice 2 — the DURABLE file-backed {@link FileHistoryStore}: the per-edited-file rewind
|
|
6
7
|
* history that survives a process restart (slice 1 shipped the contract + the in-memory reference,
|
|
@@ -75,10 +76,7 @@ export interface FileFileHistoryStoreOptions {
|
|
|
75
76
|
* are then kept — leaked, never lost). Never fires on plain ENOENT: a scope with no history yet
|
|
76
77
|
* really is empty. Swallow-guarded.
|
|
77
78
|
*/
|
|
78
|
-
onCorruptRead?: (info:
|
|
79
|
-
path: string;
|
|
80
|
-
reason: string;
|
|
81
|
-
}) => void;
|
|
79
|
+
onCorruptRead?: (info: CorruptReadInfo) => void;
|
|
82
80
|
/**
|
|
83
81
|
* design/381 §片2 — the built-in boundary bound and its override. Omitted ⇒ the engine default
|
|
84
82
|
* ({@link import("../../core/file-history-retention.js").DEFAULT_FILE_HISTORY_BOUNDARY_KEEP}
|
|
@@ -195,7 +195,7 @@ export class FileFileHistoryStore {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
disclose(path, reason) {
|
|
198
|
-
this.onCorruptRead?.({ path, reason });
|
|
198
|
+
this.onCorruptRead?.({ kind: "file", path, reason });
|
|
199
199
|
}
|
|
200
200
|
scopeDir(scope) {
|
|
201
201
|
return join(this.scopesDir, sanitizeScope(scope));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CorruptRecordRead, CorruptReplayTally } from "./corrupt-read-sink.js";
|
|
1
2
|
/**
|
|
2
3
|
* Validate an engine-minted id (sessionId / tool-result ref) before using it as a path component. These are
|
|
3
4
|
* `uuidv7` / an encoded tool-result ref — already filename-safe — but a strict guard is cheap
|
|
@@ -42,19 +43,23 @@ export declare function atomicWriteFile(tmpDir: string, target: string, bytes: s
|
|
|
42
43
|
*
|
|
43
44
|
* SHAPE (`@contract store.read_back_shape_judged`): parseability is not the judgement. Every store on this
|
|
44
45
|
* format writes ONE JSON OBJECT per line, so a line parsing to a number, string, boolean, `null` or array
|
|
45
|
-
* takes the road a torn line takes — disclosed
|
|
46
|
-
*
|
|
46
|
+
* takes the road a torn line takes — disclosed and skipped — because it is the same fact: these bytes are
|
|
47
|
+
* not what this store wrote. `judge` (optional) is the consuming store's own floor on an object ("does it
|
|
48
|
+
* carry my members at all?"); a record it rejects is skipped as `missing-required` on the same road, so
|
|
49
|
+
* the count below covers every skip.
|
|
47
50
|
*
|
|
48
|
-
* `onCorrupt` (optional
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* to absorb. The callback is invoked as given — a seat that must not throw swallow-guards on ITS side.
|
|
51
|
+
* `onCorrupt` (optional): one `record` disclosure per skipped INTERIOR record, then — iff any was skipped —
|
|
52
|
+
* one `replay` tally. The torn TAIL stays out of both by design: dying mid-write is the crash shape this
|
|
53
|
+
* format exists to absorb. The callback is invoked as given — a seat that must not throw swallow-guards on
|
|
54
|
+
* ITS side.
|
|
53
55
|
*/
|
|
54
|
-
export declare function readJsonlRecords<T>(path: string, onCorrupt?: (info:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
export declare function readJsonlRecords<T>(path: string, onCorrupt?: (info: CorruptRecordRead | CorruptReplayTally) => void, judge?: JsonlRecordJudge): T[];
|
|
57
|
+
/** A consuming store's floor on a parsed JSONL object: `accepts` = one of its records; `what` names a
|
|
58
|
+
* rejected one in the disclosure's words. */
|
|
59
|
+
export interface JsonlRecordJudge {
|
|
60
|
+
accepts(record: object): boolean;
|
|
61
|
+
what: string;
|
|
62
|
+
}
|
|
58
63
|
/**
|
|
59
64
|
* An append-only JSONL log opened once with `O_APPEND` and held for the file's lifetime. One record =
|
|
60
65
|
* one `JSON.stringify(...) + "\n"` written in a SINGLE `writeSync` call (never split across writes → the
|
|
@@ -105,7 +105,7 @@ function basenameOf(p) {
|
|
|
105
105
|
function dirnameOf(p) {
|
|
106
106
|
return dirname(p);
|
|
107
107
|
}
|
|
108
|
-
export function readJsonlRecords(path, onCorrupt) {
|
|
108
|
+
export function readJsonlRecords(path, onCorrupt, judge) {
|
|
109
109
|
let raw;
|
|
110
110
|
try {
|
|
111
111
|
raw = readFileSync(path, "utf8");
|
|
@@ -120,6 +120,11 @@ export function readJsonlRecords(path, onCorrupt) {
|
|
|
120
120
|
const parts = raw.split("\n");
|
|
121
121
|
const endedClean = parts[parts.length - 1] === "";
|
|
122
122
|
const out = [];
|
|
123
|
+
let dropped = 0;
|
|
124
|
+
const discloseSkip = (form, line, what) => {
|
|
125
|
+
dropped++;
|
|
126
|
+
onCorrupt?.({ kind: "record", path, reason: `${form}: ${what} at line ${line} (skipped)`, line, form });
|
|
127
|
+
};
|
|
123
128
|
for (let i = 0; i < parts.length; i++) {
|
|
124
129
|
if (i === parts.length - 1 && !endedClean)
|
|
125
130
|
break;
|
|
@@ -131,20 +136,26 @@ export function readJsonlRecords(path, onCorrupt) {
|
|
|
131
136
|
record = JSON.parse(line);
|
|
132
137
|
}
|
|
133
138
|
catch {
|
|
134
|
-
|
|
139
|
+
discloseSkip("torn-json", i + 1, "unparseable jsonl record");
|
|
135
140
|
continue;
|
|
136
141
|
}
|
|
137
142
|
if (record === null || typeof record !== "object" || Array.isArray(record)) {
|
|
138
|
-
|
|
143
|
+
discloseSkip("not-an-object", i + 1, `jsonl record is ${jsonTypeName(record)}, not an object`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (judge !== undefined && !judge.accepts(record)) {
|
|
147
|
+
discloseSkip("missing-required", i + 1, judge.what);
|
|
139
148
|
continue;
|
|
140
149
|
}
|
|
141
150
|
out.push(record);
|
|
142
151
|
}
|
|
152
|
+
if (dropped > 0) {
|
|
153
|
+
const total = out.length + dropped;
|
|
154
|
+
const reason = out.length === 0 ? `replay: none of ${total} records readable (all skipped)` : `replay: ${dropped} of ${total} records skipped, ${out.length} kept`;
|
|
155
|
+
onCorrupt?.({ kind: "replay", path, reason, kept: out.length, dropped, total });
|
|
156
|
+
}
|
|
143
157
|
return out;
|
|
144
158
|
}
|
|
145
|
-
function corruptRecordReason(form, lineNo, what) {
|
|
146
|
-
return `${form}: ${what} at line ${lineNo} (skipped; the rest of the replay is intact)`;
|
|
147
|
-
}
|
|
148
159
|
function jsonTypeName(value) {
|
|
149
160
|
if (value === null)
|
|
150
161
|
return "null";
|
|
@@ -5,6 +5,7 @@ import { type EvictPolicy } from "../../core/session-store.js";
|
|
|
5
5
|
import type { SessionStore } from "../../core/session.js";
|
|
6
6
|
import { type FileCheckpointStoreOptions } from "./checkpoint-store.js";
|
|
7
7
|
import { type FileFileHistoryStoreOptions } from "./file-history-store.js";
|
|
8
|
+
import type { CorruptReadInfo } from "./corrupt-read-sink.js";
|
|
8
9
|
import type { FileHistoryStore } from "../../core/file-history-store.js";
|
|
9
10
|
import type { StrategyStore } from "../../core/strategy-store.js";
|
|
10
11
|
import type { BackgroundAgentStore } from "../../core/background-agent-store.js";
|
|
@@ -105,17 +106,15 @@ export interface FileStorageBackendOptions {
|
|
|
105
106
|
* backend constructs can deliver, so ONE sink reaches all of them (a per-store payload type would make
|
|
106
107
|
* the backend's forwarding contravariance-illegal, which is what kept the seat single-store).
|
|
107
108
|
* `SessionPolicyCorruptReadInfo` is assignable to it; the two-field store seats are satisfied by it.
|
|
109
|
+
* `path` (absolute) and `reason` ride every level; `kind` tells a skipped JSONL record (`"record"`) and
|
|
110
|
+
* that replay's count (`"replay"`) from a disclosure about a file or directory as a whole (`"file"`).
|
|
108
111
|
*/
|
|
109
|
-
export
|
|
110
|
-
/** Absolute path of the offending file/directory. */
|
|
111
|
-
path: string;
|
|
112
|
-
/** What was observed and what the read degraded to. */
|
|
113
|
-
reason: string;
|
|
112
|
+
export type FileStorageCorruptReadInfo = CorruptReadInfo & {
|
|
114
113
|
/** Present only when the reading store keys by session (the session-policy face). */
|
|
115
114
|
sessionId?: string;
|
|
116
115
|
/** Present only when the read was keyed by a principal. */
|
|
117
116
|
principal?: string;
|
|
118
|
-
}
|
|
117
|
+
};
|
|
119
118
|
export declare class FileStorageBackend {
|
|
120
119
|
/** The resolved, canonical data root all four stores live under. */
|
|
121
120
|
readonly root: string;
|
|
@@ -89,7 +89,7 @@ export class FileStorageBackend {
|
|
|
89
89
|
root: strategiesRoot,
|
|
90
90
|
...(opts.onCorruptRead !== undefined
|
|
91
91
|
? {
|
|
92
|
-
onIncident: (i) => opts.onCorruptRead?.({ path: i.path ?? strategiesRoot, reason: `strategy ${i.op}: ${i.error}` }),
|
|
92
|
+
onIncident: (i) => opts.onCorruptRead?.({ kind: "file", path: i.path ?? strategiesRoot, reason: `strategy ${i.op}: ${i.error}` }),
|
|
93
93
|
}
|
|
94
94
|
: {}),
|
|
95
95
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type MailboxAppendMessage, type MailboxLease, type MailboxStore } from "../../core/mailbox-store.js";
|
|
1
|
+
import { type MailboxAckResult, type MailboxAppendMessage, type MailboxLease, type MailboxStore } from "../../core/mailbox-store.js";
|
|
2
|
+
import { type CorruptReadInfo } from "./corrupt-read-sink.js";
|
|
2
3
|
/** The waiter's refusal when a box lock stays held by a LIVE process past `lockWaitMs`. Carried as
|
|
3
4
|
* `.code` on a plain Error (consumers branch on the string); the message names the lock and the holder. */
|
|
4
5
|
export declare const MAILBOX_BOX_LOCK_TIMEOUT_CODE = "mailbox.box_lock_timeout";
|
|
@@ -45,10 +46,7 @@ export interface FileMailboxStoreOptions {
|
|
|
45
46
|
* INTERIOR record dropped by the JSONL replay is disclosed here instead of silently reading as
|
|
46
47
|
* "less mail". The torn tail stays silent by design — that is the append-log's documented
|
|
47
48
|
* crash-recovery shape, not corruption. Swallow-guarded; absent = the old silent tolerance. */
|
|
48
|
-
onCorruptRead?: (info:
|
|
49
|
-
path: string;
|
|
50
|
-
reason: string;
|
|
51
|
-
}) => void;
|
|
49
|
+
onCorruptRead?: (info: CorruptReadInfo) => void;
|
|
52
50
|
}
|
|
53
51
|
export declare class FileMailboxStore implements MailboxStore {
|
|
54
52
|
/** design/385 — the whole cross-process set (box lock + fresh re-read + descriptor reopen +
|
|
@@ -105,16 +103,13 @@ export declare class FileMailboxStore implements MailboxStore {
|
|
|
105
103
|
* and the cache is replayed from the file. A moved INODE also swap-closes the descriptor (ring 3):
|
|
106
104
|
* the next write reopens the current file instead of landing on the unlinked one.
|
|
107
105
|
*/
|
|
108
|
-
/** Fold ONE replayed event, disclosing a record that is not one of this box's ({@link applyEvent}
|
|
109
|
-
* answers `false`): dropped silently it reads exactly like a shorter mailbox. */
|
|
110
|
-
private applyReplayed;
|
|
111
106
|
private reconcile;
|
|
112
107
|
private load;
|
|
113
108
|
private commit;
|
|
114
109
|
private compact;
|
|
115
110
|
append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
|
|
116
111
|
claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
|
|
117
|
-
ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<
|
|
112
|
+
ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<MailboxAckResult>;
|
|
118
113
|
releaseLease(scope: string, handle: string, owner: string): Promise<void>;
|
|
119
114
|
peekCount(scope: string, handle: string): Promise<number>;
|
|
120
115
|
drop(scope: string, handle: string): Promise<void>;
|
|
@@ -373,23 +373,18 @@ export class FileMailboxStore {
|
|
|
373
373
|
return result;
|
|
374
374
|
});
|
|
375
375
|
}
|
|
376
|
-
applyReplayed(b, ev, path) {
|
|
377
|
-
if (applyEvent(b, ev))
|
|
378
|
-
return;
|
|
379
|
-
this.discloseCorrupt?.({ path, reason: "missing-required: a mailbox record is not one of this box's events — an unknown tag, or a known tag whose payload is missing (skipped; the rest of the replay is intact)" });
|
|
380
|
-
}
|
|
381
376
|
reconcile(b) {
|
|
382
377
|
const now = diskMarkOf(b.path);
|
|
383
378
|
if (sameDiskMark(now, b.mark))
|
|
384
379
|
return;
|
|
385
380
|
if (now === undefined || b.mark === undefined || now.ino !== b.mark.ino)
|
|
386
381
|
b.log.closeForSwap();
|
|
387
|
-
const events = readJsonlRecords(b.path, this.discloseCorrupt);
|
|
382
|
+
const events = readJsonlRecords(b.path, this.discloseCorrupt, MAILBOX_EVENT_JUDGE);
|
|
388
383
|
b.messages = [];
|
|
389
384
|
b.nextSeq = 1;
|
|
390
385
|
delete b.lease;
|
|
391
386
|
for (const ev of events)
|
|
392
|
-
|
|
387
|
+
applyEvent(b, ev);
|
|
393
388
|
b.events = Math.max(0, events.length - (b.messages.length + 1 + (b.lease !== undefined ? 1 : 0)));
|
|
394
389
|
b.mark = now;
|
|
395
390
|
}
|
|
@@ -406,10 +401,10 @@ export class FileMailboxStore {
|
|
|
406
401
|
return b;
|
|
407
402
|
}
|
|
408
403
|
ensureDir(join(this.dir, sanitizeScope(scope)));
|
|
409
|
-
const events = readJsonlRecords(path, this.discloseCorrupt);
|
|
404
|
+
const events = readJsonlRecords(path, this.discloseCorrupt, MAILBOX_EVENT_JUDGE);
|
|
410
405
|
const state = { messages: [], nextSeq: 1, log: new AppendLog(path), events: events.length, refs: 1, path, mark: undefined };
|
|
411
406
|
for (const ev of events)
|
|
412
|
-
|
|
407
|
+
applyEvent(state, ev);
|
|
413
408
|
state.mark = diskMarkOf(path);
|
|
414
409
|
sharedBoxes.set(key, state);
|
|
415
410
|
this.touched.set(key, state);
|
|
@@ -417,7 +412,8 @@ export class FileMailboxStore {
|
|
|
417
412
|
}
|
|
418
413
|
commit(b, path, ev) {
|
|
419
414
|
b.log.append(ev, this.fsyncEnabled);
|
|
420
|
-
|
|
415
|
+
if (MAILBOX_EVENT_JUDGE.accepts(ev))
|
|
416
|
+
applyEvent(b, ev);
|
|
421
417
|
b.events++;
|
|
422
418
|
if (b.events >= this.compactEvery) {
|
|
423
419
|
try {
|
|
@@ -475,8 +471,9 @@ export class FileMailboxStore {
|
|
|
475
471
|
return this.withBox(this.lockKey(scope, handle), this.boxPath(scope, handle), () => {
|
|
476
472
|
const b = this.load(scope, handle);
|
|
477
473
|
if (b.lease === undefined || b.lease.owner !== owner)
|
|
478
|
-
return;
|
|
474
|
+
return { acked: false };
|
|
479
475
|
this.commit(b, this.boxPath(scope, handle), { t: "ack", upToSeq });
|
|
476
|
+
return { acked: true };
|
|
480
477
|
});
|
|
481
478
|
}
|
|
482
479
|
async releaseLease(scope, handle, owner) {
|
|
@@ -496,8 +493,8 @@ export class FileMailboxStore {
|
|
|
496
493
|
return 0;
|
|
497
494
|
return withPathLock(this.lockKey(scope, handle), () => {
|
|
498
495
|
const b = { messages: [], nextSeq: 1 };
|
|
499
|
-
for (const ev of readJsonlRecords(path, this.discloseCorrupt))
|
|
500
|
-
|
|
496
|
+
for (const ev of readJsonlRecords(path, this.discloseCorrupt, MAILBOX_EVENT_JUDGE))
|
|
497
|
+
applyEvent(b, ev);
|
|
501
498
|
return b.messages.length;
|
|
502
499
|
});
|
|
503
500
|
}
|
|
@@ -556,8 +553,8 @@ export class FileMailboxStore {
|
|
|
556
553
|
if (sharedBoxes.has(key))
|
|
557
554
|
return false;
|
|
558
555
|
const b = { messages: [], nextSeq: 1 };
|
|
559
|
-
for (const ev of readJsonlRecords(path, this.discloseCorrupt))
|
|
560
|
-
|
|
556
|
+
for (const ev of readJsonlRecords(path, this.discloseCorrupt, MAILBOX_EVENT_JUDGE))
|
|
557
|
+
applyEvent(b, ev);
|
|
561
558
|
const newest = newestSentAt(b.messages);
|
|
562
559
|
if (newest === undefined || newest >= now - maxAgeMs)
|
|
563
560
|
return false;
|
|
@@ -584,35 +581,37 @@ export class FileMailboxStore {
|
|
|
584
581
|
}
|
|
585
582
|
}
|
|
586
583
|
const num = (v) => typeof v === "number" && Number.isFinite(v);
|
|
584
|
+
const MAILBOX_EVENT_JUDGE = {
|
|
585
|
+
accepts: (r) => {
|
|
586
|
+
const ev = r;
|
|
587
|
+
if (ev.t === "append")
|
|
588
|
+
return ev.m !== null && typeof ev.m === "object" && num(ev.m.seq);
|
|
589
|
+
if (ev.t === "lease")
|
|
590
|
+
return typeof ev.owner === "string" && num(ev.maxSeq) && num(ev.expiresAt);
|
|
591
|
+
if (ev.t === "ack")
|
|
592
|
+
return num(ev.upToSeq);
|
|
593
|
+
return ev.t === "release" && typeof ev.owner === "string";
|
|
594
|
+
},
|
|
595
|
+
what: "a mailbox record is not one of this box's events — an unknown tag, or a known tag whose payload is missing",
|
|
596
|
+
};
|
|
587
597
|
function applyEvent(b, ev) {
|
|
588
598
|
if (ev.t === "append") {
|
|
589
|
-
if (ev.m === null || typeof ev.m !== "object" || !num(ev.m.seq))
|
|
590
|
-
return false;
|
|
591
599
|
b.messages.push(ev.m);
|
|
592
600
|
if (ev.m.seq >= b.nextSeq)
|
|
593
601
|
b.nextSeq = ev.m.seq + 1;
|
|
594
|
-
return true;
|
|
595
602
|
}
|
|
596
|
-
if (ev.t === "lease") {
|
|
597
|
-
if (typeof ev.owner !== "string" || !num(ev.maxSeq) || !num(ev.expiresAt))
|
|
598
|
-
return false;
|
|
603
|
+
else if (ev.t === "lease") {
|
|
599
604
|
if (ev.maxSeq >= b.nextSeq)
|
|
600
605
|
b.nextSeq = ev.maxSeq + 1;
|
|
601
606
|
if (ev.owner !== "")
|
|
602
607
|
b.lease = { owner: ev.owner, expiresAt: ev.expiresAt, maxSeq: ev.maxSeq };
|
|
603
|
-
return true;
|
|
604
608
|
}
|
|
605
|
-
if (ev.t === "ack") {
|
|
606
|
-
if (!num(ev.upToSeq))
|
|
607
|
-
return false;
|
|
609
|
+
else if (ev.t === "ack") {
|
|
608
610
|
b.messages = b.messages.filter((m) => m.seq > ev.upToSeq);
|
|
609
611
|
if (b.lease !== undefined && b.lease.maxSeq <= ev.upToSeq)
|
|
610
612
|
b.lease = undefined;
|
|
611
|
-
return true;
|
|
612
613
|
}
|
|
613
|
-
if (
|
|
614
|
-
return false;
|
|
615
|
-
if (b.lease?.owner === ev.owner)
|
|
614
|
+
else if (b.lease?.owner === ev.owner) {
|
|
616
615
|
b.lease = undefined;
|
|
617
|
-
|
|
616
|
+
}
|
|
618
617
|
}
|
|
@@ -16,6 +16,8 @@ import { type PutRulesOptions, type SessionPermissionRules, type SessionPolicySt
|
|
|
16
16
|
/** Coordinates of one corrupt-treated-as-absent policy read (the {@link FileSessionPolicyStoreOptions.onCorruptRead}
|
|
17
17
|
* payload). Named rather than inline so the backend option that forwards it names the SAME shape. */
|
|
18
18
|
export interface SessionPolicyCorruptReadInfo {
|
|
19
|
+
/** Always a whole-file disclosure (the {@link import("./corrupt-read-sink.js").CorruptReadInfo} level). */
|
|
20
|
+
kind: "file";
|
|
19
21
|
/** The session whose read observed the corrupt row. On the enumeration face this is the session being
|
|
20
22
|
* ENUMERATED — a corrupt row's own `__sid` is by definition unreadable, so it cannot be attributed. */
|
|
21
23
|
sessionId: string;
|
|
@@ -77,7 +77,7 @@ export class FileSessionPolicyStore {
|
|
|
77
77
|
adoption !== undefined && "adopted" in adoption && adoption.adopted.from.kind === "local-owner" ? adoption.adopted.toPrincipal : undefined;
|
|
78
78
|
}
|
|
79
79
|
disclose(info) {
|
|
80
|
-
this.onCorruptRead?.(info);
|
|
80
|
+
this.onCorruptRead?.({ kind: "file", ...info });
|
|
81
81
|
}
|
|
82
82
|
discloseCorrupt(sessionId, principal, reason) {
|
|
83
83
|
this.disclose({ sessionId, ...(principal !== undefined ? { principal } : {}), path: this.pathFor(sessionId, principal), reason });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Session, SessionCreateOptions, SessionForkOptions, SessionMetadata, SessionPlacementRecord, SessionRepo, SessionTreeEntry } from "../../internal/harness.js";
|
|
2
2
|
import type { PlacedSessionRow } from "../../core/session.js";
|
|
3
|
+
import { type CorruptReadInfo } from "./corrupt-read-sink.js";
|
|
3
4
|
export interface FileSessionRepoOptions {
|
|
4
5
|
/**
|
|
5
6
|
* Disclosure sink for a durable read this repo treats as ABSENT (ruled 2026-08-03). Same name and
|
|
@@ -7,17 +8,14 @@ export interface FileSessionRepoOptions {
|
|
|
7
8
|
* "corrupt read as missing" are one failure grammar, so they get one seat name.
|
|
8
9
|
*
|
|
9
10
|
* Fires for: an unreadable sessions DIRECTORY (the listing degrades to "no sessions"), a session log
|
|
10
|
-
* that could not be replayed during a listing (skipped from the result), and
|
|
11
|
-
* record dropped on replay (turns
|
|
11
|
+
* that could not be replayed during a listing (skipped from the result), and each corrupt interior JSONL
|
|
12
|
+
* record dropped on replay plus that replay's tally (turns missing from an otherwise healthy session).
|
|
12
13
|
*
|
|
13
14
|
* Never fires on plain ENOENT / `not_found` — that is honest absence, and the torn TAIL stays silent
|
|
14
15
|
* too (dying mid-write is the append log's documented crash-recovery shape, not corruption).
|
|
15
16
|
* Swallow-guarded: a throwing sink cannot re-introduce the fault the fail-open avoids.
|
|
16
17
|
*/
|
|
17
|
-
onCorruptRead?: (info:
|
|
18
|
-
path: string;
|
|
19
|
-
reason: string;
|
|
20
|
-
}) => void;
|
|
18
|
+
onCorruptRead?: (info: CorruptReadInfo) => void;
|
|
21
19
|
}
|
|
22
20
|
export declare class FileSessionRepo implements SessionRepo {
|
|
23
21
|
private readonly dir;
|
|
@@ -31,7 +29,13 @@ export declare class FileSessionRepo implements SessionRepo {
|
|
|
31
29
|
/** The one delivery point for {@link FileSessionRepoOptions.onCorruptRead}. */
|
|
32
30
|
private disclose;
|
|
33
31
|
private pathFor;
|
|
34
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Read + torn-tail-recover a session file into (meta, entries). Three answers: no file → `not_found`; a
|
|
34
|
+
* file with no meta header → `corrupt_session`, even when every other record is intact (every write path
|
|
35
|
+
* here lays the header down first and the header alone carries `createdAt` / fork / placement, so the file
|
|
36
|
+
* is not a session this store wrote, or its header line is damaged); otherwise the session, each skipped
|
|
37
|
+
* record disclosed.
|
|
38
|
+
*/
|
|
35
39
|
private read;
|
|
36
40
|
private storage;
|
|
37
41
|
/**
|
|
@@ -6,6 +6,13 @@ import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
|
6
6
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
7
7
|
import { thrownText } from "../../core/thrown-value.js";
|
|
8
8
|
const SUFFIX = ".jsonl";
|
|
9
|
+
const SESSION_LINE_JUDGE = {
|
|
10
|
+
accepts: (r) => {
|
|
11
|
+
const line = r;
|
|
12
|
+
return line.kind === "meta" || (line.kind === "entry" && line.entry !== null && typeof line.entry === "object");
|
|
13
|
+
},
|
|
14
|
+
what: "a session line is neither a meta header nor an entry record",
|
|
15
|
+
};
|
|
9
16
|
const sharedSessionStorages = new Map();
|
|
10
17
|
const sessionStorageFinalizer = new FinalizationRegistry(({ canonical, log }) => {
|
|
11
18
|
log.close();
|
|
@@ -68,7 +75,7 @@ export class FileSessionRepo {
|
|
|
68
75
|
this.onCorruptRead = guardCorruptSink(opts?.onCorruptRead);
|
|
69
76
|
}
|
|
70
77
|
disclose(path, reason) {
|
|
71
|
-
this.onCorruptRead?.({ path, reason });
|
|
78
|
+
this.onCorruptRead?.({ kind: "file", path, reason });
|
|
72
79
|
}
|
|
73
80
|
pathFor(id) {
|
|
74
81
|
return join(this.dir, `${sanitizePathComponent(id)}${SUFFIX}`);
|
|
@@ -78,24 +85,24 @@ export class FileSessionRepo {
|
|
|
78
85
|
if (!existsSync(path)) {
|
|
79
86
|
throw new SessionError("not_found", `Session not found: ${id}`);
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
let tally;
|
|
89
|
+
const lines = readJsonlRecords(path, (info) => {
|
|
90
|
+
if (info.kind === "replay")
|
|
91
|
+
tally = info;
|
|
92
|
+
this.onCorruptRead?.(info);
|
|
93
|
+
}, SESSION_LINE_JUDGE);
|
|
94
|
+
let meta;
|
|
85
95
|
const entries = [];
|
|
86
96
|
for (const line of lines) {
|
|
87
|
-
if (line.kind === "meta")
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
this.disclose(path, "missing-required: a session line is neither a meta header nor an entry record (skipped; the rest of the replay is intact)");
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
entries.push(line.entry);
|
|
97
|
+
if (line.kind === "meta")
|
|
98
|
+
meta = line;
|
|
99
|
+
else
|
|
100
|
+
entries.push(line.entry);
|
|
101
|
+
}
|
|
102
|
+
if (meta === undefined) {
|
|
103
|
+
throw new SessionError("corrupt_session", `Session ${id} is unreadable: its log has no meta header (${tally?.dropped ?? 0} of ${tally?.total ?? lines.length} records skipped as corrupt)`);
|
|
98
104
|
}
|
|
105
|
+
const { createdAt, forkedFrom, placement } = meta;
|
|
99
106
|
return { createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}), ...(placement !== undefined ? { placement } : {}), entries };
|
|
100
107
|
}
|
|
101
108
|
storage(id, createdAt, entries, forkedFrom, placement) {
|
|
@@ -21,22 +21,11 @@ export class LedgerCore {
|
|
|
21
21
|
this.fence = acquireStoreDirLock(this.paths.dir, { label: this.model.label });
|
|
22
22
|
try {
|
|
23
23
|
const disclose = this.paths.onCorruptRead;
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
for (const x of xs) {
|
|
27
|
-
if (judge(x))
|
|
28
|
-
kept.push(x);
|
|
29
|
-
else
|
|
30
|
-
disclose?.({ path, reason: `missing-required: a ${what} carries none of this ${this.model.label}'s own members (skipped; the rest of the replay is intact)` });
|
|
31
|
-
}
|
|
32
|
-
return kept;
|
|
33
|
-
};
|
|
34
|
-
const snapPath = this.paths.snapshotPath;
|
|
35
|
-
const ledgerPath = this.paths.ledgerPath;
|
|
36
|
-
for (const row of mine(readJsonlRecords(snapPath, disclose), this.model.judgeRow, snapPath, "snapshot row")) {
|
|
24
|
+
const judge = (accepts, what) => ({ accepts, what: `a ${what} carries none of this ${this.model.label}'s own members` });
|
|
25
|
+
for (const row of readJsonlRecords(this.paths.snapshotPath, disclose, judge(this.model.judgeRow, "snapshot row"))) {
|
|
37
26
|
this.rows.set(this.model.keyOf(row), row);
|
|
38
27
|
}
|
|
39
|
-
const events =
|
|
28
|
+
const events = readJsonlRecords(this.paths.ledgerPath, disclose, judge(this.model.judgeEvent, "ledger event"));
|
|
40
29
|
this.events = events.length;
|
|
41
30
|
for (const ev of events)
|
|
42
31
|
this.model.apply(this.rows, ev);
|
|
@@ -37,6 +37,8 @@ export declare class FileToolResultStore implements ToolResultStore {
|
|
|
37
37
|
private readonly dir;
|
|
38
38
|
private readonly discloseCorrupt;
|
|
39
39
|
constructor(root: string, opts?: FileToolResultStoreOptions);
|
|
40
|
+
/** The one delivery point for {@link FileToolResultStoreOptions.onCorruptRead}. */
|
|
41
|
+
private disclose;
|
|
40
42
|
/** The directory a ref's files live in, and their shared stem. See {@link encodeRefFilename} for why
|
|
41
43
|
* a FOLDED name gets its own directory instead of sharing one with identity names. */
|
|
42
44
|
private locate;
|
|
@@ -14,6 +14,9 @@ export class FileToolResultStore {
|
|
|
14
14
|
this.discloseCorrupt = guardCorruptSink(opts?.onCorruptRead);
|
|
15
15
|
ensureDir(this.dir);
|
|
16
16
|
}
|
|
17
|
+
disclose(path, reason) {
|
|
18
|
+
this.discloseCorrupt?.({ kind: "file", path, reason });
|
|
19
|
+
}
|
|
17
20
|
locate(ref) {
|
|
18
21
|
assertSafeToolResultRef(ref);
|
|
19
22
|
const encoded = encodeRefFilename(ref);
|
|
@@ -81,7 +84,7 @@ export class FileToolResultStore {
|
|
|
81
84
|
return undefined;
|
|
82
85
|
if (opts.loud)
|
|
83
86
|
throw err;
|
|
84
|
-
this.
|
|
87
|
+
this.disclose(ownerPath, `unreadable owner sidecar (${err.code ?? "unknown"}) — read as unowned`);
|
|
85
88
|
return undefined;
|
|
86
89
|
}
|
|
87
90
|
const parsed = parseOwnerRecord(raw);
|
|
@@ -90,7 +93,7 @@ export class FileToolResultStore {
|
|
|
90
93
|
if (opts.loud) {
|
|
91
94
|
throw new Error(`tool-result store: owner record for ref ${JSON.stringify(ref)} is present but unreadable — refusing to write over it`);
|
|
92
95
|
}
|
|
93
|
-
this.
|
|
96
|
+
this.disclose(ownerPath, "not-an-object: owner sidecar is present but is not an owner record — read as unowned");
|
|
94
97
|
return undefined;
|
|
95
98
|
}
|
|
96
99
|
writeOwnerFile(ref, provenance) {
|
|
@@ -19,6 +19,10 @@ export const RESUME_CLAIM_SWEEP_INTERVAL_MS = 60_000;
|
|
|
19
19
|
export const RESUME_CLAIM_REAP_GRACE_MS = 5 * 60_000;
|
|
20
20
|
const sharedJournalDirs = new Map();
|
|
21
21
|
const MAX_OPEN_JOURNAL_LOGS = 64;
|
|
22
|
+
function isJournalLine(r) {
|
|
23
|
+
const rec = r;
|
|
24
|
+
return typeof rec.callKey === "string" && typeof rec.scope === "string" && Number.isFinite(rec.ordinal);
|
|
25
|
+
}
|
|
22
26
|
export class FileWorkflowJournalStore {
|
|
23
27
|
fsyncEnabled;
|
|
24
28
|
dir;
|
|
@@ -75,16 +79,17 @@ export class FileWorkflowJournalStore {
|
|
|
75
79
|
return undefined;
|
|
76
80
|
let scope;
|
|
77
81
|
const byOrdinal = new Map();
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
const judge = {
|
|
83
|
+
accepts: (r) => {
|
|
84
|
+
if (!isJournalLine(r))
|
|
85
|
+
return false;
|
|
86
|
+
scope ??= r.scope;
|
|
87
|
+
return r.scope === scope;
|
|
88
|
+
},
|
|
89
|
+
what: "a journal line carries no callKey/scope pair, or a scope other than its run's first line",
|
|
90
|
+
};
|
|
91
|
+
for (const rec of readJsonlRecords(path, this.discloseCorrupt, judge))
|
|
86
92
|
byOrdinal.set(rec.ordinal, entryOf(rec));
|
|
87
|
-
}
|
|
88
93
|
return scope === undefined ? undefined : { scope, byOrdinal };
|
|
89
94
|
}
|
|
90
95
|
async append(runId, scope, entry) {
|