@sema-agent/core 7.26.3 → 7.28.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 +59 -0
- package/dist/agents/builtin-agents.d.ts +6 -15
- package/dist/agents/cascade.d.ts +29 -33
- package/dist/agents/observer.d.ts +18 -20
- package/dist/agents/subagent.d.ts +58 -70
- package/dist/agents/tool-filter.d.ts +10 -12
- package/dist/agents/verify.d.ts +44 -49
- package/dist/brain/input-too-long.d.ts +10 -12
- package/dist/brain/media-degrade.d.ts +4 -5
- package/dist/brain/reasoning.d.ts +10 -12
- package/dist/brain/repetition.d.ts +7 -9
- package/dist/brain/request-params.d.ts +20 -25
- package/dist/brain/route-adjudicator.d.ts +4 -4
- package/dist/brain/route-conformance.d.ts +2 -2
- package/dist/brain/status-sink.d.ts +12 -14
- package/dist/brain/terminal-cause.d.ts +12 -15
- package/dist/brain/timeout.d.ts +17 -19
- package/dist/core/auto-mode-directive.d.ts +6 -4
- package/dist/core/auto-mode-directive.js +1 -5
- package/dist/core/engine-notice.d.ts +13 -6
- package/dist/core/memory-engine/consolidation.d.ts +11 -15
- package/dist/core/memory-engine/engine.d.ts +45 -54
- package/dist/core/memory-engine/tools.d.ts +17 -23
- package/dist/core/permission-rule-model.js +5 -3
- package/dist/core/runner/contracts.d.ts +31 -7
- package/dist/core/runner/denial-limit-arms.d.ts +17 -15
- package/dist/core/runner/denial-limit-arms.js +4 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +1 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +1 -2
- package/dist/core/runner/prepare-policy-chain.d.ts +1 -2
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/run-leg.js +5 -1
- package/dist/core/tool-policy.d.ts +23 -2
- package/dist/core/tool-policy.js +106 -103
- package/dist/index.d.ts +6 -1
- package/dist/shell/lex.d.ts +279 -0
- package/dist/shell/lex.js +1253 -0
- package/dist/stores/file/background-agent-store.d.ts +4 -0
- package/dist/stores/file/background-agent-store.js +10 -0
- package/dist/stores/file/checkpoint-store.d.ts +3 -0
- package/dist/stores/file/checkpoint-store.js +18 -0
- package/dist/stores/file/corrupt-read-sink.d.ts +20 -0
- package/dist/stores/file/corrupt-read-sink.js +15 -0
- package/dist/stores/file/file-history-store.d.ts +1 -2
- package/dist/stores/file/file-history-store.js +3 -8
- package/dist/stores/file/fs-atomic.d.ts +8 -3
- package/dist/stores/file/fs-atomic.js +19 -2
- package/dist/stores/file/index.js +4 -4
- package/dist/stores/file/mailbox-store.d.ts +3 -0
- package/dist/stores/file/mailbox-store.js +24 -18
- package/dist/stores/file/permission-rule-store.js +18 -1
- package/dist/stores/file/session-policy-store.d.ts +17 -1
- package/dist/stores/file/session-policy-store.js +3 -12
- package/dist/stores/file/session-store.d.ts +1 -5
- package/dist/stores/file/session-store.js +9 -14
- package/dist/stores/file/shared-ledger.d.ts +18 -0
- package/dist/stores/file/shared-ledger.js +16 -2
- package/dist/stores/file/tool-result-store.d.ts +9 -1
- package/dist/stores/file/tool-result-store.js +17 -2
- package/dist/stores/file/workflow-journal-store.d.ts +10 -1
- package/dist/stores/file/workflow-journal-store.js +8 -12
- package/dist/stores/file/workflow-run-store.d.ts +3 -0
- package/dist/stores/file/workflow-run-store.js +10 -0
- package/dist/tools/artifact/local-stub.js +16 -1
- package/dist/tools/fs/bash-program-position.d.ts +2 -4
- package/dist/tools/fs/bash-program-position.js +2 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -2
- package/dist/tools/fs/bash-readonly-classifier.js +1 -1
- package/dist/tools/fs/shell-read-check.js +1 -1
- package/dist/tools/fs/shell-read-establish.js +11 -1
- package/dist/tools/fs/shell-read-plan.d.ts +7 -2
- package/dist/tools/fs/shell-read-plan.js +48 -10
- package/package.json +2 -2
- package/test/export-surface.snapshot.json +3 -1
- package/wire-changes/core@7.27.0.json +152 -0
- package/wire-changes/core@7.28.0.json +594 -0
- package/dist/tools/fs/bash-lexer.d.ts +0 -152
- package/dist/tools/fs/bash-lexer.js +0 -627
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type BackgroundAgentReapOptions, type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentStore, type BackgroundAgentUpdateIfExpect } from "../../core/background-agent-store.js";
|
|
2
|
+
import { type CorruptReadInfo } from "./corrupt-read-sink.js";
|
|
2
3
|
export interface FileBackgroundAgentStoreOptions {
|
|
4
|
+
/** This store's half of {@link import("./shared-ledger.js").LedgerPaths.onCorruptRead} — the same seat,
|
|
5
|
+
* over the agent row model. */
|
|
6
|
+
onCorruptRead?: (info: CorruptReadInfo) => void;
|
|
3
7
|
/** When false, a ledger append is NOT fsync'd. Default true. */
|
|
4
8
|
fsync?: boolean;
|
|
5
9
|
/** Compact the ledger into a snapshot once it exceeds this many events (then truncate). Default 1000. */
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, assertBackgroundAgentReapOptions, queryBackgroundAgents, } from "../../core/background-agent-store.js";
|
|
3
3
|
import { SharedLedgerTable } from "./shared-ledger.js";
|
|
4
|
+
import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
4
5
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
6
|
+
const isAgentRow = (x) => x !== null && typeof x === "object" && typeof x.handle === "string" && typeof x.scope === "string";
|
|
5
7
|
const agentLedgers = new SharedLedgerTable({
|
|
6
8
|
label: "background-agent ledger",
|
|
7
9
|
keyOf: (r) => FileBackgroundAgentStore.key(r.handle, r.scope),
|
|
@@ -12,6 +14,13 @@ const agentLedgers = new SharedLedgerTable({
|
|
|
12
14
|
}
|
|
13
15
|
rows.set(ev.k, ev.r);
|
|
14
16
|
},
|
|
17
|
+
judgeRow: isAgentRow,
|
|
18
|
+
judgeEvent: (x) => {
|
|
19
|
+
const ev = x;
|
|
20
|
+
if (typeof ev.k !== "string")
|
|
21
|
+
return false;
|
|
22
|
+
return ev.t === "delete" ? true : isAgentRow(ev.r);
|
|
23
|
+
},
|
|
15
24
|
});
|
|
16
25
|
export class FileBackgroundAgentStore {
|
|
17
26
|
fsyncEnabled;
|
|
@@ -29,6 +38,7 @@ export class FileBackgroundAgentStore {
|
|
|
29
38
|
this.ledger = agentLedgers.acquire({
|
|
30
39
|
dir,
|
|
31
40
|
tmpDir: join(root, "tmp"),
|
|
41
|
+
...(opts.onCorruptRead !== undefined ? { onCorruptRead: guardCorruptSink(opts.onCorruptRead) } : {}),
|
|
32
42
|
ledgerPath: join(dir, "agents.jsonl"),
|
|
33
43
|
snapshotPath: join(dir, "agents.snapshot.jsonl"),
|
|
34
44
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { type ExecutionOutcomeRecordWord } from "../../core/checkpoint-execution-record.js";
|
|
2
2
|
import { type Checkpoint, type PendingSteerInput, type CheckpointFaultMode, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ReopenReason, type ResolveExpectation, type ResumeOutcome } from "../../core/checkpoint-store.js";
|
|
3
3
|
import type { GateOutcome } from "../../core/gate-outcome.js";
|
|
4
|
+
import { type CorruptReadInfo } from "./corrupt-read-sink.js";
|
|
4
5
|
export interface FileCheckpointStoreOptions {
|
|
6
|
+
/** This store's half of {@link import("./shared-ledger.js").LedgerPaths.onCorruptRead}. */
|
|
7
|
+
onCorruptRead?: (info: CorruptReadInfo) => void;
|
|
5
8
|
/** When false, an `appendLine` for a state transition is NOT fsync'd. The checkpoint COMMIT POINT always
|
|
6
9
|
* fsyncs regardless (its crash-safety depends on it); this governs the `put` and `setPendingSteer`
|
|
7
10
|
* (steer-append) commits only — `resolve`/`reopen`/`expire` fsync unconditionally. Default true. */
|
|
@@ -3,6 +3,7 @@ import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
|
3
3
|
import { checkpointExecutionRecorded, executionRecordDisposition, ExecutionOutcomeConflictError, } from "../../core/checkpoint-execution-record.js";
|
|
4
4
|
import { appendPendingSteer, CheckpointError, checkpointOccMatches, checkpointRowMatches, summarizeCheckpoint, validatePendingSteer, winnerFromOutcome, } from "../../core/checkpoint-store.js";
|
|
5
5
|
import { SharedLedgerTable } from "./shared-ledger.js";
|
|
6
|
+
import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
6
7
|
const CHECKPOINT_LEDGER_EVENT_REGISTRY = {
|
|
7
8
|
put: true,
|
|
8
9
|
resolve: true,
|
|
@@ -57,10 +58,26 @@ function applyCheckpointEvent(cps, ev) {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
const isCheckpointRow = (x) => x !== null &&
|
|
62
|
+
typeof x === "object" &&
|
|
63
|
+
typeof x.token === "string" &&
|
|
64
|
+
typeof x.scope === "string" &&
|
|
65
|
+
typeof x.status === "string";
|
|
60
66
|
const checkpointLedgers = new SharedLedgerTable({
|
|
61
67
|
label: "checkpoint ledger",
|
|
62
68
|
keyOf: (cp) => cp.token,
|
|
63
69
|
apply: applyCheckpointEvent,
|
|
70
|
+
judgeRow: isCheckpointRow,
|
|
71
|
+
judgeEvent: (x) => {
|
|
72
|
+
const ev = x;
|
|
73
|
+
if (typeof ev.t !== "string")
|
|
74
|
+
return false;
|
|
75
|
+
if (!KNOWN_CHECKPOINT_LEDGER_EVENTS.has(ev.t))
|
|
76
|
+
return true;
|
|
77
|
+
if (typeof ev.token !== "string")
|
|
78
|
+
return false;
|
|
79
|
+
return ev.t === "put" ? isCheckpointRow(ev.cp) : true;
|
|
80
|
+
},
|
|
64
81
|
});
|
|
65
82
|
export class FileCheckpointStore {
|
|
66
83
|
durability = "durable";
|
|
@@ -83,6 +100,7 @@ export class FileCheckpointStore {
|
|
|
83
100
|
this.ledger = checkpointLedgers.acquire({
|
|
84
101
|
dir,
|
|
85
102
|
tmpDir: join(root, "tmp"),
|
|
103
|
+
...(opts.onCorruptRead !== undefined ? { onCorruptRead: guardCorruptSink(opts.onCorruptRead) } : {}),
|
|
86
104
|
ledgerPath: join(dir, "checkpoints.jsonl"),
|
|
87
105
|
snapshotPath: join(dir, "checkpoints.snapshot.jsonl"),
|
|
88
106
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The file backend's ONE disclosure-seat guard: `@contract store.read_back_shape_judged`'s second legal
|
|
3
|
+
* posture — an absence that DISCLOSES — rides a host's `onCorruptRead`, and the guard around it was
|
|
4
|
+
* hand-written four times. Three copies contained an async sink's rejection; the mailbox's caught only a
|
|
5
|
+
* synchronous throw, so an `async` sink there became an unhandled rejection.
|
|
6
|
+
*/
|
|
7
|
+
/** Coordinates of one durable read a store answered as absent because the bytes were corrupt. */
|
|
8
|
+
export interface CorruptReadInfo {
|
|
9
|
+
path: string;
|
|
10
|
+
reason: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Wrap a host's disclosure seat: neither a synchronous throw nor an async rejection can re-introduce the
|
|
14
|
+
* fault the fail-open avoids. `undefined` in ⇒ `undefined` out. Generic in the payload because the
|
|
15
|
+
* session-policy store discloses session coordinates rather than a path.
|
|
16
|
+
*/
|
|
17
|
+
/** The sink is invoked UNBOUND (`this` is undefined): pass a bound function or an arrow, never a bare
|
|
18
|
+
* method reference — a method reading `this` throws, and the throw is contained here, so the disclosure
|
|
19
|
+
* is silently lost rather than surfaced. */
|
|
20
|
+
export declare function guardCorruptSink<I>(sink: ((info: I) => void) | undefined): ((info: I) => void) | undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function guardCorruptSink(sink) {
|
|
2
|
+
if (sink === undefined)
|
|
3
|
+
return undefined;
|
|
4
|
+
return (info) => {
|
|
5
|
+
try {
|
|
6
|
+
const returned = sink(info);
|
|
7
|
+
if (typeof returned?.then === "function") {
|
|
8
|
+
void Promise.resolve(returned).then(undefined, () => {
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -233,8 +233,7 @@ export declare class FileFileHistoryStore implements FileHistoryStore {
|
|
|
233
233
|
* GC that could not run leaves a graph larger than its bound, the safer of the two directions).
|
|
234
234
|
*/
|
|
235
235
|
private applyBoundaryRetention;
|
|
236
|
-
/** The single delivery point for the disclosure sink
|
|
237
|
-
* remember, and so a host's throwing sink cannot break the never-throw seam. */
|
|
236
|
+
/** The single delivery point for the disclosure sink. */
|
|
238
237
|
private disclose;
|
|
239
238
|
private scopeDir;
|
|
240
239
|
private trackedPath;
|
|
@@ -5,6 +5,7 @@ import { thrownText } from "../../core/thrown-value.js";
|
|
|
5
5
|
import { applyFileRestoreViaEnv, boundaryPublishVerdict, worldStillMatchesMint, captureFileBackupViaEnv, modeProvenDifferent, previewFileDelta, describeTrackKey, fileHistoryExportDigest, resolveTrackKey, trackKeyOf, validateFileHistoryExport, } from "../../core/file-history-store.js";
|
|
6
6
|
import { fileHistoryBoundariesToKeep, resolveFileHistoryRetention } from "../../core/file-history-retention.js";
|
|
7
7
|
import { atomicWriteFile, ensureDir, sanitizePathComponent, sanitizeScope, writeThenLink } from "./fs-atomic.js";
|
|
8
|
+
import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
8
9
|
import { isAbsoluteForFamily } from "../../tools/fs/safety.js";
|
|
9
10
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
10
11
|
function trackedDigest(rows) {
|
|
@@ -156,7 +157,7 @@ export class FileFileHistoryStore {
|
|
|
156
157
|
this.scopesDir = join(this.base, "scopes");
|
|
157
158
|
ensureDir(this.blobsDir);
|
|
158
159
|
ensureDir(this.scopesDir);
|
|
159
|
-
this.onCorruptRead = opts?.onCorruptRead;
|
|
160
|
+
this.onCorruptRead = guardCorruptSink(opts?.onCorruptRead);
|
|
160
161
|
}
|
|
161
162
|
applyBoundaryRetention(scope, justCommitted) {
|
|
162
163
|
if (this.boundaryKeep === null)
|
|
@@ -194,13 +195,7 @@ export class FileFileHistoryStore {
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
disclose(path, reason) {
|
|
197
|
-
|
|
198
|
-
const r = this.onCorruptRead?.({ path, reason });
|
|
199
|
-
if (typeof r?.then === "function")
|
|
200
|
-
r.then(undefined, () => { });
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
}
|
|
198
|
+
this.onCorruptRead?.({ path, reason });
|
|
204
199
|
}
|
|
205
200
|
scopeDir(scope) {
|
|
206
201
|
return join(this.scopesDir, sanitizeScope(scope));
|
|
@@ -40,11 +40,16 @@ export declare function atomicWriteFile(tmpDir: string, target: string, bytes: s
|
|
|
40
40
|
* parity), so a single corrupt interior line never poisons the whole replay.
|
|
41
41
|
* A missing file → `[]`. This is why JSONL beats one big JSON blob: a torn tail can never corrupt the store.
|
|
42
42
|
*
|
|
43
|
+
* SHAPE (`@contract store.read_back_shape_judged`): parseability is not the judgement. Every store on this
|
|
44
|
+
* 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, skipped, the replay intact — because it is the same fact:
|
|
46
|
+
* these bytes are not what this store wrote. Without it `null` made a `line.kind` test throw.
|
|
47
|
+
*
|
|
43
48
|
* `onCorrupt` (optional, review-510 disclosure-seat family): fired once per skipped INTERIOR record so a
|
|
44
49
|
* caller-side seat can observe real corruption — a silently-skipped record is otherwise indistinguishable
|
|
45
|
-
* from "less data"
|
|
46
|
-
*
|
|
47
|
-
* must not throw swallow-guards on ITS side
|
|
50
|
+
* from "less data"; its `reason` opens with the {@link CorruptBodyForm} word, so a sink classifies without
|
|
51
|
+
* reading prose. The torn TAIL stays out by design: dying mid-write is the crash shape this format exists
|
|
52
|
+
* to absorb. The callback is invoked as given — a seat that must not throw swallow-guards on ITS side.
|
|
48
53
|
*/
|
|
49
54
|
export declare function readJsonlRecords<T>(path: string, onCorrupt?: (info: {
|
|
50
55
|
path: string;
|
|
@@ -126,15 +126,32 @@ export function readJsonlRecords(path, onCorrupt) {
|
|
|
126
126
|
const line = parts[i];
|
|
127
127
|
if (line === "")
|
|
128
128
|
continue;
|
|
129
|
+
let record;
|
|
129
130
|
try {
|
|
130
|
-
|
|
131
|
+
record = JSON.parse(line);
|
|
131
132
|
}
|
|
132
133
|
catch {
|
|
133
|
-
onCorrupt?.({ path, reason:
|
|
134
|
+
onCorrupt?.({ path, reason: corruptRecordReason("torn-json", i + 1, "unparseable jsonl record") });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (record === null || typeof record !== "object" || Array.isArray(record)) {
|
|
138
|
+
onCorrupt?.({ path, reason: corruptRecordReason("not-an-object", i + 1, `jsonl record is ${jsonTypeName(record)}, not an object`) });
|
|
139
|
+
continue;
|
|
134
140
|
}
|
|
141
|
+
out.push(record);
|
|
135
142
|
}
|
|
136
143
|
return out;
|
|
137
144
|
}
|
|
145
|
+
function corruptRecordReason(form, lineNo, what) {
|
|
146
|
+
return `${form}: ${what} at line ${lineNo} (skipped; the rest of the replay is intact)`;
|
|
147
|
+
}
|
|
148
|
+
function jsonTypeName(value) {
|
|
149
|
+
if (value === null)
|
|
150
|
+
return "null";
|
|
151
|
+
if (Array.isArray(value))
|
|
152
|
+
return "an array";
|
|
153
|
+
return `a ${typeof value}`;
|
|
154
|
+
}
|
|
138
155
|
export function canonicalStoreKey(p) {
|
|
139
156
|
return physicalPathOfExisting(p).toLowerCase();
|
|
140
157
|
}
|
|
@@ -65,19 +65,19 @@ export class FileStorageBackend {
|
|
|
65
65
|
this.fileSessions = repo;
|
|
66
66
|
this.ttl = new TtlSessionStore({ repo, evict: opts.evict ?? "forget", durability: "durable", placements: { subagent: { durability: "durable" } } });
|
|
67
67
|
this.sessionStore = this.ttl;
|
|
68
|
-
this.fileCheckpoints = new FileCheckpointStore(this.root, opts.checkpoint);
|
|
68
|
+
this.fileCheckpoints = new FileCheckpointStore(this.root, { ...opts.checkpoint, ...corruptRead });
|
|
69
69
|
built.push(() => this.fileCheckpoints.close());
|
|
70
70
|
this.checkpointStore = this.fileCheckpoints;
|
|
71
71
|
this.fileMemory = new FileMemoryStore(this.root, opts.embedder ? { embedder: opts.embedder } : {});
|
|
72
72
|
built.push(() => this.fileMemory.close());
|
|
73
73
|
this.memoryStore = guardedMemoryStore(this.fileMemory, opts.utilityGate);
|
|
74
|
-
this.toolResultStore = new FileToolResultStore(this.root);
|
|
74
|
+
this.toolResultStore = new FileToolResultStore(this.root, corruptRead);
|
|
75
75
|
this.sessionPolicyStore = new FileSessionPolicyStore(this.root, corruptRead);
|
|
76
|
-
this.fileWorkflowJournal = new FileWorkflowJournalStore(this.root);
|
|
76
|
+
this.fileWorkflowJournal = new FileWorkflowJournalStore(this.root, true, corruptRead);
|
|
77
77
|
built.push(() => this.fileWorkflowJournal.dispose());
|
|
78
78
|
this.workflowJournalStore = this.fileWorkflowJournal;
|
|
79
79
|
this.usageWindowStore = new FileUsageWindowStore(this.root);
|
|
80
|
-
this.fileAgentRows = new FileBackgroundAgentStore(this.root);
|
|
80
|
+
this.fileAgentRows = new FileBackgroundAgentStore(this.root, corruptRead ?? {});
|
|
81
81
|
built.push(() => this.fileAgentRows.close());
|
|
82
82
|
this.backgroundAgentStore = this.fileAgentRows;
|
|
83
83
|
this.fileMailbox = new FileMailboxStore(this.root, corruptRead ?? {});
|
|
@@ -105,6 +105,9 @@ export declare class FileMailboxStore implements MailboxStore {
|
|
|
105
105
|
* and the cache is replayed from the file. A moved INODE also swap-closes the descriptor (ring 3):
|
|
106
106
|
* the next write reopens the current file instead of landing on the unlinked one.
|
|
107
107
|
*/
|
|
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;
|
|
108
111
|
private reconcile;
|
|
109
112
|
private load;
|
|
110
113
|
private commit;
|
|
@@ -4,6 +4,7 @@ import { assertRetentionPolicy } from "../../core/retention-policy.js";
|
|
|
4
4
|
import { existsSync, linkSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { detachMailboxMessage, newestSentAt, readMailboxPeerMeta, } from "../../core/mailbox-store.js";
|
|
6
6
|
import { AppendLog, atomicWriteFile, canonicalStoreKey, ensureDir, readJsonlRecords, sanitizeScope, sanitizePathComponent } from "./fs-atomic.js";
|
|
7
|
+
import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
7
8
|
import { physicalPathOf } from "../../core/physical-path.js";
|
|
8
9
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
9
10
|
function diskMarkOf(path) {
|
|
@@ -339,17 +340,7 @@ export class FileMailboxStore {
|
|
|
339
340
|
throw new Error(`FileMailboxStore: lockWaitMs must be a finite number > 0, got ${JSON.stringify(opts.lockWaitMs)}`);
|
|
340
341
|
}
|
|
341
342
|
this.lockWaitMs = lockWaitMs;
|
|
342
|
-
|
|
343
|
-
this.discloseCorrupt =
|
|
344
|
-
sink === undefined
|
|
345
|
-
? undefined
|
|
346
|
-
: (info) => {
|
|
347
|
-
try {
|
|
348
|
-
sink(info);
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
351
|
-
}
|
|
352
|
-
};
|
|
343
|
+
this.discloseCorrupt = guardCorruptSink(opts.onCorruptRead);
|
|
353
344
|
const lexicalDir = resolve(join(root, "mailboxes"));
|
|
354
345
|
const lexicalTmp = resolve(join(root, "tmp"));
|
|
355
346
|
ensureDir(lexicalDir);
|
|
@@ -382,6 +373,11 @@ export class FileMailboxStore {
|
|
|
382
373
|
return result;
|
|
383
374
|
});
|
|
384
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
|
+
}
|
|
385
381
|
reconcile(b) {
|
|
386
382
|
const now = diskMarkOf(b.path);
|
|
387
383
|
if (sameDiskMark(now, b.mark))
|
|
@@ -393,7 +389,7 @@ export class FileMailboxStore {
|
|
|
393
389
|
b.nextSeq = 1;
|
|
394
390
|
delete b.lease;
|
|
395
391
|
for (const ev of events)
|
|
396
|
-
|
|
392
|
+
this.applyReplayed(b, ev, b.path);
|
|
397
393
|
b.events = Math.max(0, events.length - (b.messages.length + 1 + (b.lease !== undefined ? 1 : 0)));
|
|
398
394
|
b.mark = now;
|
|
399
395
|
}
|
|
@@ -413,7 +409,7 @@ export class FileMailboxStore {
|
|
|
413
409
|
const events = readJsonlRecords(path, this.discloseCorrupt);
|
|
414
410
|
const state = { messages: [], nextSeq: 1, log: new AppendLog(path), events: events.length, refs: 1, path, mark: undefined };
|
|
415
411
|
for (const ev of events)
|
|
416
|
-
|
|
412
|
+
this.applyReplayed(state, ev, path);
|
|
417
413
|
state.mark = diskMarkOf(path);
|
|
418
414
|
sharedBoxes.set(key, state);
|
|
419
415
|
this.touched.set(key, state);
|
|
@@ -501,7 +497,7 @@ export class FileMailboxStore {
|
|
|
501
497
|
return withPathLock(this.lockKey(scope, handle), () => {
|
|
502
498
|
const b = { messages: [], nextSeq: 1 };
|
|
503
499
|
for (const ev of readJsonlRecords(path, this.discloseCorrupt))
|
|
504
|
-
|
|
500
|
+
this.applyReplayed(b, ev, path);
|
|
505
501
|
return b.messages.length;
|
|
506
502
|
});
|
|
507
503
|
}
|
|
@@ -561,7 +557,7 @@ export class FileMailboxStore {
|
|
|
561
557
|
return false;
|
|
562
558
|
const b = { messages: [], nextSeq: 1 };
|
|
563
559
|
for (const ev of readJsonlRecords(path, this.discloseCorrupt))
|
|
564
|
-
|
|
560
|
+
this.applyReplayed(b, ev, path);
|
|
565
561
|
const newest = newestSentAt(b.messages);
|
|
566
562
|
if (newest === undefined || newest >= now - maxAgeMs)
|
|
567
563
|
return false;
|
|
@@ -587,26 +583,36 @@ export class FileMailboxStore {
|
|
|
587
583
|
this.touched.clear();
|
|
588
584
|
}
|
|
589
585
|
}
|
|
586
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v);
|
|
590
587
|
function applyEvent(b, ev) {
|
|
591
588
|
if (ev.t === "append") {
|
|
589
|
+
if (ev.m === null || typeof ev.m !== "object" || !num(ev.m.seq))
|
|
590
|
+
return false;
|
|
592
591
|
b.messages.push(ev.m);
|
|
593
592
|
if (ev.m.seq >= b.nextSeq)
|
|
594
593
|
b.nextSeq = ev.m.seq + 1;
|
|
595
|
-
return;
|
|
594
|
+
return true;
|
|
596
595
|
}
|
|
597
596
|
if (ev.t === "lease") {
|
|
597
|
+
if (typeof ev.owner !== "string" || !num(ev.maxSeq) || !num(ev.expiresAt))
|
|
598
|
+
return false;
|
|
598
599
|
if (ev.maxSeq >= b.nextSeq)
|
|
599
600
|
b.nextSeq = ev.maxSeq + 1;
|
|
600
601
|
if (ev.owner !== "")
|
|
601
602
|
b.lease = { owner: ev.owner, expiresAt: ev.expiresAt, maxSeq: ev.maxSeq };
|
|
602
|
-
return;
|
|
603
|
+
return true;
|
|
603
604
|
}
|
|
604
605
|
if (ev.t === "ack") {
|
|
606
|
+
if (!num(ev.upToSeq))
|
|
607
|
+
return false;
|
|
605
608
|
b.messages = b.messages.filter((m) => m.seq > ev.upToSeq);
|
|
606
609
|
if (b.lease !== undefined && b.lease.maxSeq <= ev.upToSeq)
|
|
607
610
|
b.lease = undefined;
|
|
608
|
-
return;
|
|
611
|
+
return true;
|
|
609
612
|
}
|
|
613
|
+
if (ev.t !== "release" || typeof ev.owner !== "string")
|
|
614
|
+
return false;
|
|
610
615
|
if (b.lease?.owner === ev.owner)
|
|
611
616
|
b.lease = undefined;
|
|
617
|
+
return true;
|
|
612
618
|
}
|
|
@@ -48,8 +48,25 @@ export function readRuleAdoptionFile(dir) {
|
|
|
48
48
|
}
|
|
49
49
|
return parsed;
|
|
50
50
|
}
|
|
51
|
+
function isRuleOwnerShape(v) {
|
|
52
|
+
if (v === null || typeof v !== "object")
|
|
53
|
+
return false;
|
|
54
|
+
const owner = v;
|
|
55
|
+
return owner.kind === "local-owner" || (owner.kind === "principal" && typeof owner.principal === "string");
|
|
56
|
+
}
|
|
51
57
|
function isRuleAdoptionFileShape(v) {
|
|
52
|
-
|
|
58
|
+
if (v === null || typeof v !== "object" || v.schemaVersion !== 1)
|
|
59
|
+
return false;
|
|
60
|
+
const hasMarker = "marker" in v;
|
|
61
|
+
const hasAdopted = "adopted" in v;
|
|
62
|
+
if (hasMarker === hasAdopted)
|
|
63
|
+
return false;
|
|
64
|
+
const rec = (hasMarker ? v.marker : v.adopted);
|
|
65
|
+
if (rec === null || typeof rec !== "object")
|
|
66
|
+
return false;
|
|
67
|
+
if (!isRuleOwnerShape(rec.from) || typeof rec.toPrincipal !== "string" || rec.toPrincipal === "")
|
|
68
|
+
return false;
|
|
69
|
+
return hasMarker ? rec.phase === 2 || rec.phase === 3 || rec.phase === 4 || rec.phase === 5 : typeof rec.atMs === "number" && Number.isFinite(rec.atMs);
|
|
53
70
|
}
|
|
54
71
|
export function writeRuleAdoptionFile(dir, content) {
|
|
55
72
|
assertSafeDir(dir);
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { type PutRulesOptions, type SessionPermissionRules, type SessionPolicyStore, type SessionRulesRecord, type StoredSessionRules } from "../../core/session-policy-store.js";
|
|
2
|
+
/**
|
|
3
|
+
* design/99 §E6 — file-backed {@link SessionPolicyStore} for the local (TOC) backend: one JSON row file per
|
|
4
|
+
* `(sessionId, principal)`, plus — once that key has ever been stamped — a second, tiny file beside it holding
|
|
5
|
+
* the number the erase contract on {@link SessionPolicyStore.deleteBySession} says must outlive the row. Both
|
|
6
|
+
* names share a stem, and the mark's suffix is what every directory sweep here tests first
|
|
7
|
+
* ({@link MARK_SUFFIX}). Semantics otherwise **byte-for-byte identical to `InMemorySessionPolicyStore`** (the
|
|
8
|
+
* cross-backend equivalence contract). CAS-rev OCC + tighten-only reuse the SAME pure helpers
|
|
9
|
+
* (`loosenReasons`/`normalizeRules`/`stripRev`) as core — no re-implemented rule logic that could drift.
|
|
10
|
+
*
|
|
11
|
+
* Atomicity: the file backend is single-process (the `FileStorageBackend` boot lock guarantees ONE writer per
|
|
12
|
+
* data dir), and `getRules`/`putRules` read-check-write SYNCHRONOUSLY (no await between the rev read and the
|
|
13
|
+
* atomic write), so the read-modify-write is atomic in the one event loop — exactly the InMemory store's premise.
|
|
14
|
+
* Cross-process CORRECT concurrency is the Pg/TiDB backend's job (a CAS WHERE clause), by design.
|
|
15
|
+
*/
|
|
16
|
+
/** Coordinates of one corrupt-treated-as-absent policy read (the {@link FileSessionPolicyStoreOptions.onCorruptRead}
|
|
17
|
+
* payload). Named rather than inline so the backend option that forwards it names the SAME shape. */
|
|
2
18
|
export interface SessionPolicyCorruptReadInfo {
|
|
3
19
|
/** The session whose read observed the corrupt row. On the enumeration face this is the session being
|
|
4
20
|
* ENUMERATED — a corrupt row's own `__sid` is by definition unreadable, so it cannot be attributed. */
|
|
@@ -84,7 +100,7 @@ export declare class FileSessionPolicyStore implements SessionPolicyStore {
|
|
|
84
100
|
* alike (a window-era anonymous write must not mint a fresh orphan row). */
|
|
85
101
|
private readonly anonymousAlias;
|
|
86
102
|
constructor(root: string, opts?: FileSessionPolicyStoreOptions);
|
|
87
|
-
/** The one delivery point for {@link onCorruptRead}.
|
|
103
|
+
/** The one delivery point for {@link onCorruptRead}. */
|
|
88
104
|
private disclose;
|
|
89
105
|
/** RB-482 top item (baggage class-4 ruling): corrupt-read fail-open is a DOCUMENTED compromise (a throwing
|
|
90
106
|
* getRules fails the whole task; a throwing putRules self-locks the repair write) — but it must not
|
|
@@ -2,13 +2,8 @@ import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loosenReasons, normalizeRules, stripRev, SessionPolicyError, } from "../../core/session-policy-store.js";
|
|
4
4
|
import { atomicWriteFile, ensureDir, sanitizeScope } from "./fs-atomic.js";
|
|
5
|
+
import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
5
6
|
import { assertAdoptionBootGate, readRootAdoptionFile } from "./adoption/marker.js";
|
|
6
|
-
function containSinkThenable(r) {
|
|
7
|
-
if (typeof r?.then === "function") {
|
|
8
|
-
r.then(undefined, () => {
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
7
|
export const SESSION_POLICY_MARK_SUFFIX = ".hwm.json";
|
|
13
8
|
const MARK_SUFFIX = SESSION_POLICY_MARK_SUFFIX;
|
|
14
9
|
export function readSessionPolicyMark(path) {
|
|
@@ -76,17 +71,13 @@ export class FileSessionPolicyStore {
|
|
|
76
71
|
assertAdoptionBootGate(root, "FileSessionPolicyStore");
|
|
77
72
|
this.dir = join(root, "session-policy");
|
|
78
73
|
ensureDir(this.dir);
|
|
79
|
-
this.onCorruptRead = opts?.onCorruptRead;
|
|
74
|
+
this.onCorruptRead = guardCorruptSink(opts?.onCorruptRead);
|
|
80
75
|
const adoption = readRootAdoptionFile(root);
|
|
81
76
|
this.anonymousAlias =
|
|
82
77
|
adoption !== undefined && "adopted" in adoption && adoption.adopted.from.kind === "local-owner" ? adoption.adopted.toPrincipal : undefined;
|
|
83
78
|
}
|
|
84
79
|
disclose(info) {
|
|
85
|
-
|
|
86
|
-
containSinkThenable(this.onCorruptRead?.(info));
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
}
|
|
80
|
+
this.onCorruptRead?.(info);
|
|
90
81
|
}
|
|
91
82
|
discloseCorrupt(sessionId, principal, reason) {
|
|
92
83
|
this.disclose({ sessionId, ...(principal !== undefined ? { principal } : {}), path: this.pathFor(sessionId, principal), reason });
|
|
@@ -28,11 +28,7 @@ export declare class FileSessionRepo implements SessionRepo {
|
|
|
28
28
|
* check on the way out possible without pinning a storage the table itself only holds weakly. */
|
|
29
29
|
private readonly joined;
|
|
30
30
|
constructor(root: string, opts?: FileSessionRepoOptions);
|
|
31
|
-
/** The one delivery point for {@link FileSessionRepoOptions.onCorruptRead}
|
|
32
|
-
* no call site has to remember. The sink seat is void-typed but a host may hand it an async
|
|
33
|
-
* function — an async sink's rejection is observed off its returned thenable (same containment
|
|
34
|
-
* as the strategy store's incident sink), so neither a sync throw nor an async rejection can
|
|
35
|
-
* re-introduce the failure mode the fail-open avoids. */
|
|
31
|
+
/** The one delivery point for {@link FileSessionRepoOptions.onCorruptRead}. */
|
|
36
32
|
private disclose;
|
|
37
33
|
private pathFor;
|
|
38
34
|
/** Read + torn-tail-recover a session file into (meta, entries); a missing file → not_found. */
|
|
@@ -2,14 +2,9 @@ import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { BaseSessionStorage, StoredSession, SessionError, getEntriesToFork, uuidv7, validateEntriesForImport, } from "../../internal/harness.js";
|
|
4
4
|
import { canonicalStoreKey, AppendLog, atomicWriteFile, ensureDir, readJsonlRecords, sanitizePathComponent } from "./fs-atomic.js";
|
|
5
|
+
import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
5
6
|
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
6
7
|
import { thrownText } from "../../core/thrown-value.js";
|
|
7
|
-
function containSinkThenable(r) {
|
|
8
|
-
if (typeof r?.then === "function") {
|
|
9
|
-
r.then(undefined, () => {
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
8
|
const SUFFIX = ".jsonl";
|
|
14
9
|
const sharedSessionStorages = new Map();
|
|
15
10
|
const sessionStorageFinalizer = new FinalizationRegistry(({ canonical, log }) => {
|
|
@@ -70,14 +65,10 @@ export class FileSessionRepo {
|
|
|
70
65
|
this.tmpDir = join(root, "tmp");
|
|
71
66
|
ensureDir(this.dir);
|
|
72
67
|
ensureDir(this.tmpDir);
|
|
73
|
-
this.onCorruptRead = opts?.onCorruptRead;
|
|
68
|
+
this.onCorruptRead = guardCorruptSink(opts?.onCorruptRead);
|
|
74
69
|
}
|
|
75
70
|
disclose(path, reason) {
|
|
76
|
-
|
|
77
|
-
containSinkThenable(this.onCorruptRead?.({ path, reason }));
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
}
|
|
71
|
+
this.onCorruptRead?.({ path, reason });
|
|
81
72
|
}
|
|
82
73
|
pathFor(id) {
|
|
83
74
|
return join(this.dir, `${sanitizePathComponent(id)}${SUFFIX}`);
|
|
@@ -97,9 +88,13 @@ export class FileSessionRepo {
|
|
|
97
88
|
createdAt = line.createdAt;
|
|
98
89
|
forkedFrom = line.forkedFrom;
|
|
99
90
|
placement = line.placement;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (line.kind !== "entry" || line.entry === null || typeof line.entry !== "object") {
|
|
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;
|
|
100
96
|
}
|
|
101
|
-
|
|
102
|
-
entries.push(line.entry);
|
|
97
|
+
entries.push(line.entry);
|
|
103
98
|
}
|
|
104
99
|
return { createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}), ...(placement !== undefined ? { placement } : {}), entries };
|
|
105
100
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CorruptReadInfo } from "./corrupt-read-sink.js";
|
|
1
2
|
/**
|
|
2
3
|
* design/157 A9 — the SHARED substrate under the three snapshot+ledger file stores
|
|
3
4
|
* ({@link import("./background-agent-store.js").FileBackgroundAgentStore},
|
|
@@ -61,6 +62,11 @@
|
|
|
61
62
|
/** The paths one ledger authority owns. `dir` is also what the authority is keyed on. */
|
|
62
63
|
export interface LedgerPaths {
|
|
63
64
|
readonly dir: string;
|
|
65
|
+
/** Disclosure seat for a replay row this authority DROPPED as corrupt — without it the loss is
|
|
66
|
+
* indistinguishable from "fewer rows". Already swallow-guarded by the store that wired it. Only the
|
|
67
|
+
* FIRST instance over a directory replays, so only its seat can fire: a joiner reads the authority
|
|
68
|
+
* that instance built, which is the same fact its refcount records. */
|
|
69
|
+
readonly onCorruptRead?: (info: CorruptReadInfo) => void;
|
|
64
70
|
/** Scratch dir for {@link atomicWriteFile} (same filesystem as the targets → rename is atomic). */
|
|
65
71
|
readonly tmpDir: string;
|
|
66
72
|
readonly ledgerPath: string;
|
|
@@ -73,6 +79,18 @@ export interface LedgerModel<Row, Ev> {
|
|
|
73
79
|
label: string;
|
|
74
80
|
keyOf(row: Row): string;
|
|
75
81
|
apply(rows: Map<string, Row>, ev: Ev): void;
|
|
82
|
+
/**
|
|
83
|
+
* `@contract store.read_back_shape_judged` floor: is this parsed body one of MY rows / MY events at
|
|
84
|
+
* all? Judged on the DISCRIMINANT only — the key a row is filed under, the tag an event is folded by —
|
|
85
|
+
* because what to do with a discriminant this binary does not KNOW is the model's own business (the
|
|
86
|
+
* checkpoint ledger refuses the whole replay for a kind from a newer worker, deliberately). A body
|
|
87
|
+
* that carries no discriminant carries no such choice: it is damage, and `bootstrap` discloses and
|
|
88
|
+
* skips it. Required rather than optional, so a fourth ledger store cannot inherit the silence by
|
|
89
|
+
* omission — before this floor existed, `{"note":"x"}` in an agent ledger filed a row under the key
|
|
90
|
+
* `undefined` and the next enumeration read members off it.
|
|
91
|
+
*/
|
|
92
|
+
judgeRow(x: unknown): boolean;
|
|
93
|
+
judgeEvent(x: unknown): boolean;
|
|
76
94
|
}
|
|
77
95
|
/**
|
|
78
96
|
* One directory's authority: the replayed row map, the per-key async mutex, the append log and the
|
|
@@ -20,9 +20,23 @@ export class LedgerCore {
|
|
|
20
20
|
bootstrap() {
|
|
21
21
|
this.fence = acquireStoreDirLock(this.paths.dir, { label: this.model.label });
|
|
22
22
|
try {
|
|
23
|
-
|
|
23
|
+
const disclose = this.paths.onCorruptRead;
|
|
24
|
+
const mine = (xs, judge, path, what) => {
|
|
25
|
+
const kept = [];
|
|
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
37
|
this.rows.set(this.model.keyOf(row), row);
|
|
25
|
-
|
|
38
|
+
}
|
|
39
|
+
const events = mine(readJsonlRecords(ledgerPath, disclose), this.model.judgeEvent, ledgerPath, "ledger event");
|
|
26
40
|
this.events = events.length;
|
|
27
41
|
for (const ev of events)
|
|
28
42
|
this.model.apply(this.rows, ev);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ToolResultDeletionReport, ToolResultProvenance, ToolResultSlice, ToolResultStore } from "../../core/tool-result-store.js";
|
|
2
|
+
import { type CorruptReadInfo } from "./corrupt-read-sink.js";
|
|
2
3
|
/**
|
|
3
4
|
* design/80 §2.5 — file {@link ToolResultStore}: write-once-idempotent, ONE file per ref.
|
|
4
5
|
*
|
|
@@ -26,9 +27,16 @@ import type { ToolResultDeletionReport, ToolResultProvenance, ToolResultSlice, T
|
|
|
26
27
|
* is present but unreadable stops the write loudly, where treating it as "no owner" would report
|
|
27
28
|
* success and leave the entry unclaimable forever.
|
|
28
29
|
*/
|
|
30
|
+
export interface FileToolResultStoreOptions {
|
|
31
|
+
/** Disclosure seat for an owner sidecar that is PRESENT but unreadable: {@link
|
|
32
|
+
* FileToolResultStore.ownerOf} answers `undefined` — the same word it uses for an entry nobody owns,
|
|
33
|
+
* so a host that authorizes on it reads damage as "unowned". Swallow-guarded. */
|
|
34
|
+
onCorruptRead?: (info: CorruptReadInfo) => void;
|
|
35
|
+
}
|
|
29
36
|
export declare class FileToolResultStore implements ToolResultStore {
|
|
30
37
|
private readonly dir;
|
|
31
|
-
|
|
38
|
+
private readonly discloseCorrupt;
|
|
39
|
+
constructor(root: string, opts?: FileToolResultStoreOptions);
|
|
32
40
|
/** The directory a ref's files live in, and their shared stem. See {@link encodeRefFilename} for why
|
|
33
41
|
* a FOLDED name gets its own directory instead of sharing one with identity names. */
|
|
34
42
|
private locate;
|