@sema-agent/core 7.30.1 → 7.31.1
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 +284 -0
- package/dist/agents/cross-session-judge.d.ts +4 -12
- package/dist/agents/cross-session-judge.js +4 -11
- package/dist/agents/peer-session-drain.js +0 -5
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +1 -0
- package/dist/core/ask-origin.d.ts +6 -0
- package/dist/core/ask-origin.js +10 -0
- package/dist/core/checkpoint-store.d.ts +14 -0
- package/dist/core/checkpoint-store.js +1 -0
- package/dist/core/engine-notice.d.ts +4 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/memory-engine/data-plane.d.ts +3 -1
- package/dist/core/memory-engine/data-plane.js +1 -1
- package/dist/core/memory-engine/delegation-settlement.d.ts +44 -3
- package/dist/core/memory-engine/delegation-settlement.js +38 -3
- package/dist/core/memory-engine/engine.d.ts +133 -27
- package/dist/core/memory-engine/engine.js +452 -151
- package/dist/core/memory-engine/entry-scan.d.ts +23 -0
- package/dist/core/memory-engine/entry-scan.js +55 -0
- package/dist/core/memory-engine/file-backend.d.ts +31 -23
- package/dist/core/memory-engine/file-backend.js +174 -100
- package/dist/core/memory-engine/layer-observations.d.ts +109 -0
- package/dist/core/memory-engine/layer-observations.js +148 -0
- package/dist/core/memory-engine/layout.d.ts +40 -4
- package/dist/core/memory-engine/layout.js +43 -7
- package/dist/core/memory-engine/types.d.ts +26 -2
- package/dist/core/parent-spec-seat.d.ts +4 -1
- package/dist/core/parent-spec-seat.js +2 -0
- package/dist/core/park-selfcheck.js +1 -0
- package/dist/core/runner/announce-once-ledger.d.ts +26 -0
- package/dist/core/runner/announce-once-ledger.js +37 -0
- package/dist/core/runner/contracts.d.ts +22 -5
- package/dist/core/runner/denial-limit-arms.d.ts +2 -2
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +0 -4
- package/dist/core/runner/prepare-acquire-reconcile.js +1 -3
- package/dist/core/runner/prepare-file-history.d.ts +13 -15
- package/dist/core/runner/prepare-file-history.js +57 -43
- package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
- package/dist/core/runner/prepare-listings.d.ts +3 -1
- package/dist/core/runner/prepare-listings.js +2 -2
- package/dist/core/runner/prepare-memory.js +16 -5
- package/dist/core/runner/prepare-park-ask.js +27 -16
- package/dist/core/runner/prepare-prompt-inputs.d.ts +8 -1
- package/dist/core/runner/prepare-prompt-inputs.js +5 -3
- package/dist/core/runner/prepare-task.js +7 -5
- package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -0
- package/dist/core/runner/prepare-wiring-manifest.js +2 -1
- package/dist/core/runner/run-attachment-seats.js +1 -1
- package/dist/core/runner/run-leg.js +61 -52
- package/dist/core/runner/run-settle-and-teardown.js +4 -0
- package/dist/core/runner/run-turn-boundary.js +6 -6
- package/dist/core/runner/turn-attachments.d.ts +6 -6
- package/dist/core/runner-deps.d.ts +3 -1
- package/dist/core/secret-env.d.ts +12 -1
- package/dist/core/secret-env.js +27 -2
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +22 -0
- package/dist/core/task-result.d.ts +1 -1
- package/dist/core/task-spec.d.ts +17 -8
- package/dist/core/tool-spec.d.ts +1 -1
- package/dist/core/wiring-manifest.d.ts +10 -0
- package/dist/core/wiring-manifest.js +7 -1
- package/dist/engine/execution-env/node-execution-env.d.ts +6 -0
- package/dist/engine/execution-env/node-execution-env.js +15 -17
- package/dist/engine/harness/agent-harness.js +7 -5
- package/dist/engine/lsp/node-lsp-manager.d.ts +4 -0
- package/dist/engine/lsp/node-lsp-manager.js +10 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/fs/shell-read-plan.d.ts +2 -2
- package/dist/tools/fs/shell-read-plan.js +15 -10
- package/dist/tools/fs/shell-read-word.d.ts +5 -4
- package/dist/tools/fs/shell-read-word.js +3 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +7 -1
- package/wire-changes/core@7.31.0.json +705 -0
- package/wire-changes/core@7.31.1.json +189 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Files/dirs the entry scan never treats as entries: the derived index, dotfiles (`.hydrate`,
|
|
2
|
+
* transaction staging files), and anything not `.md`. */
|
|
3
|
+
export declare const MEMORY_INDEX_FILENAME = "MEMORY.md";
|
|
4
|
+
/** Default nesting depth cap under a scope dir (CC memory is flat; a small allowance for topic dirs). */
|
|
5
|
+
export declare const DEFAULT_MAX_ENTRY_DEPTH = 3;
|
|
6
|
+
/** One on-disk entry file the scan found. */
|
|
7
|
+
export interface ScannedEntryFile {
|
|
8
|
+
/** Absolute path. */
|
|
9
|
+
path: string;
|
|
10
|
+
/** Slug = path relative to the scope dir, without `.md` (may contain `/` for nested topics). */
|
|
11
|
+
slug: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Enumerate a scope dir's entry files (depth-capped, symlinks skipped, dotfiles/index/non-`.md`
|
|
15
|
+
* ignored). Shared by the backend and the engine's harvest scan so the two can never disagree on
|
|
16
|
+
* what counts as an entry file. A scope dir is that scope's home and nothing else — no other
|
|
17
|
+
* scope's directory can lie under it, so there is nothing to exclude.
|
|
18
|
+
*/
|
|
19
|
+
export declare function scanEntryFiles(dir: string, opts?: {
|
|
20
|
+
maxDepth?: number;
|
|
21
|
+
/** Harvest gate hook: called for every path the scan SKIPS, with why (backend scans ignore it). */
|
|
22
|
+
onSkip?: (path: string, kind: "symlink" | "depth" | "nonmd" | "dotfile" | "unreadable") => void;
|
|
23
|
+
}): ScannedEntryFile[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { lstatSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
export const MEMORY_INDEX_FILENAME = "MEMORY.md";
|
|
4
|
+
export const DEFAULT_MAX_ENTRY_DEPTH = 3;
|
|
5
|
+
export function scanEntryFiles(dir, opts = {}) {
|
|
6
|
+
const maxDepth = opts.maxDepth ?? DEFAULT_MAX_ENTRY_DEPTH;
|
|
7
|
+
const out = [];
|
|
8
|
+
const walk = (d, depth) => {
|
|
9
|
+
let names;
|
|
10
|
+
try {
|
|
11
|
+
names = readdirSync(d);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
if (d !== dir)
|
|
15
|
+
opts.onSkip?.(d, "unreadable");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
for (const name of names.sort()) {
|
|
19
|
+
if (name.startsWith(".") || name === MEMORY_INDEX_FILENAME) {
|
|
20
|
+
if (name.startsWith("."))
|
|
21
|
+
opts.onSkip?.(join(d, name), "dotfile");
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const p = join(d, name);
|
|
25
|
+
let st;
|
|
26
|
+
try {
|
|
27
|
+
st = lstatSync(p);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
opts.onSkip?.(p, "unreadable");
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (st.isSymbolicLink()) {
|
|
34
|
+
opts.onSkip?.(p, "symlink");
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (st.isDirectory()) {
|
|
38
|
+
if (depth + 1 <= maxDepth)
|
|
39
|
+
walk(p, depth + 1);
|
|
40
|
+
else
|
|
41
|
+
opts.onSkip?.(p, "depth");
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!st.isFile())
|
|
45
|
+
continue;
|
|
46
|
+
if (!name.endsWith(".md")) {
|
|
47
|
+
opts.onSkip?.(p, "nonmd");
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
out.push({ path: p, slug: relative(dir, p).replace(/\.md$/, "") });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
walk(dir, 0);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
@@ -1,29 +1,7 @@
|
|
|
1
1
|
import { scopeDirName, type ChallengeEvent, type MemoryPartitionIncidentSink } from "./layout.js";
|
|
2
2
|
import type { MemoryBundleImportPlan, MemoryImportReport } from "./export-bundle.js";
|
|
3
3
|
import type { HarvestRejection, MemoryBackend, MemoryEntry, MemoryEntryHeader, MemoryScopeEnumeration, NotePatch, PatchReport, ScoredMemoryEntry } from "./types.js";
|
|
4
|
-
|
|
5
|
-
* transaction staging files), and anything not `.md`. */
|
|
6
|
-
export declare const MEMORY_INDEX_FILENAME = "MEMORY.md";
|
|
7
|
-
/** Default nesting depth cap under a scope dir (CC memory is flat; a small allowance for topic dirs). */
|
|
8
|
-
export declare const DEFAULT_MAX_ENTRY_DEPTH = 3;
|
|
9
|
-
/** One on-disk entry file the scan found. */
|
|
10
|
-
export interface ScannedEntryFile {
|
|
11
|
-
/** Absolute path. */
|
|
12
|
-
path: string;
|
|
13
|
-
/** Slug = path relative to the scope dir, without `.md` (may contain `/` for nested topics). */
|
|
14
|
-
slug: string;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Enumerate a scope dir's entry files (depth-capped, symlinks skipped, dotfiles/index/non-`.md`
|
|
18
|
-
* ignored). Shared by the backend and the engine's harvest scan so the two can never disagree on
|
|
19
|
-
* what counts as an entry file. A scope dir is that scope's home and nothing else — no other
|
|
20
|
-
* scope's directory can lie under it, so there is nothing to exclude.
|
|
21
|
-
*/
|
|
22
|
-
export declare function scanEntryFiles(dir: string, opts?: {
|
|
23
|
-
maxDepth?: number;
|
|
24
|
-
/** Harvest gate hook: called for every path the scan SKIPS, with why (backend scans ignore it). */
|
|
25
|
-
onSkip?: (path: string, kind: "symlink" | "depth" | "nonmd" | "dotfile" | "unreadable") => void;
|
|
26
|
-
}): ScannedEntryFile[];
|
|
4
|
+
export { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles, type ScannedEntryFile } from "./entry-scan.js";
|
|
27
5
|
/** design/178 v2-b §3.3(b) — the explicit erasure selector (each form resolves to an explicit id
|
|
28
6
|
* list before anything is deleted; there is no whole-store selector). */
|
|
29
7
|
export type ErasureSelect = {
|
|
@@ -375,6 +353,8 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
375
353
|
* NEVER under {@link directoryRoot}, always on the config-root side even for an in-repo memory dir.
|
|
376
354
|
* The engine picks this up (like `directoryRoot`) so both sides share ONE control plane. */
|
|
377
355
|
readonly controlPlaneRoot: string;
|
|
356
|
+
/** {@link directoryRoot}'s physical form at construction — the root every act on a store path is judged against. */
|
|
357
|
+
private readonly physicalRoot;
|
|
378
358
|
/** Injected clock (opus 1.258 LOW: match the engine's `this.now` determinism discipline). */
|
|
379
359
|
private readonly now;
|
|
380
360
|
/** The construction-time incident sink (partition adoption, layout adoption on the standalone faces). */
|
|
@@ -701,6 +681,19 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
701
681
|
* §3.1 ⓪ (r5-①): "no lock" is ENOENT-only — a stat FAILURE folded into "nobody is committing"
|
|
702
682
|
* let a transient EIO run the unlocked recovery (and its staged sweep) over a live transaction. */
|
|
703
683
|
private txnInFlight;
|
|
684
|
+
/**
|
|
685
|
+
* The act gate for store paths ({@link storeTargetEscape}) over `ops`' writes and deletes — shadow
|
|
686
|
+
* ops name control-plane files, not store paths. Refuses with `memory.target_outside_store` at the
|
|
687
|
+
* first op that would land outside the store, naming the op and where it resolves. Runs over a
|
|
688
|
+
* whole batch before its commit marker (no journal is written; a caller that staged files removes
|
|
689
|
+
* them) and per op at the act itself; past the marker a refusal keeps the journal, which replays
|
|
690
|
+
* once the path resolves inside the store again.
|
|
691
|
+
*/
|
|
692
|
+
private refuseEscapingTargets;
|
|
693
|
+
/** At the head of every face (both recovery legs, the lock-free faces' layout seat, the adoption face):
|
|
694
|
+
* the store root still resolves to the directory this backend opened. A root re-pointed since would
|
|
695
|
+
* carry every read, adoption, sweep and restore — not only the journal's acts — into another directory. */
|
|
696
|
+
private assertRootIntact;
|
|
704
697
|
/** Remove orphaned `.stage-*` files (a crash BEFORE the journal rename = clean rollback: nothing
|
|
705
698
|
* was committed, the staged tmps are litter). Dotfiles are invisible to entry scans regardless. */
|
|
706
699
|
private sweepStagedFiles;
|
|
@@ -737,6 +730,14 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
737
730
|
* the probe classifies those inline and keeps going — a store with a lingering copy never
|
|
738
731
|
* contends for the writer lock on every read. */
|
|
739
732
|
private readScope;
|
|
733
|
+
/** #1076b — the observation-table door of the adopting read: bytes a session recorded appearing
|
|
734
|
+
* while it held this layer read-only are never adopted. Gate-failing bytes are not taken here —
|
|
735
|
+
* the inbound gate's containment keeps its door. A hit is announced, the committed copy stands in
|
|
736
|
+
* when this path is the entry's bound projection, and the disk is left as found (the account it
|
|
737
|
+
* is judged against is current — the scan sees to that). The notice is deduplicated against what
|
|
738
|
+
* is still queued, so every session that meets the file is told once. Returns whether the file
|
|
739
|
+
* was settled (the caller moves on). */
|
|
740
|
+
private settledAsObserved;
|
|
740
741
|
/**
|
|
741
742
|
* design-A RB-5 ([A3]) — the read-side whitewash gate, shared by the bound-here divergence lane
|
|
742
743
|
* and the move+edit lane so the two can never drift: an out-of-session disk change (git pull /
|
|
@@ -846,6 +847,13 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
846
847
|
* WRITES ARE REFUSED, loudly — a view handed to a write path is a defect, not a fallback.
|
|
847
848
|
*/
|
|
848
849
|
retrievalView(): MemoryBackend;
|
|
850
|
+
/** #1076b — the lock-free retrieval face serves no recorded divergence either (side-effect-free, so
|
|
851
|
+
* no notice): the committed copy stands in at the entry's bound projection, anything else is
|
|
852
|
+
* withheld. The table is read per call; the account only when it names one of these ids. */
|
|
853
|
+
private withoutObserved;
|
|
854
|
+
/** The committed rows read afresh from disk (no cache install), or undefined when the account
|
|
855
|
+
* cannot be read — a fault, never folded into "no rows". */
|
|
856
|
+
private committedRowsOrFault;
|
|
849
857
|
/** Dedup keys for divergence findings the restricted view already recorded: one materialize reads
|
|
850
858
|
* a scope several times (listHeaders, then getByIds per scope), and a checkpoint-cadence harvest
|
|
851
859
|
* re-audits the same still-pending state — without the key, one untouched divergence would mint
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, linkSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, openSync, fsyncSync, closeSync, utimesSync } from "node:fs";
|
|
1
|
+
import { existsSync, fstatSync, linkSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, openSync, fsyncSync, closeSync, utimesSync } from "node:fs";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { dirname, join, relative } from "node:path";
|
|
4
4
|
import { jaccardDistance, termSet } from "../memory-vector.js";
|
|
@@ -6,63 +6,13 @@ import { MAX_MEMORY_BYTES } from "../memory.js";
|
|
|
6
6
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
7
7
|
import { ambiguousOriginRepresentation, committedDistilledOf, committedOriginOf, computeEntryRev, distilledEquals, entryFromFile, isValidEntryId, originEquals, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
|
|
8
8
|
import { convertV1SidecarKeys } from "./delegation-settlement.js";
|
|
9
|
-
import {
|
|
9
|
+
import { layerObservationLookup, observedDivergenceNotice } from "./layer-observations.js";
|
|
10
|
+
import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./entry-scan.js";
|
|
11
|
+
import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, CHALLENGES_FILE, IMPORT_RECEIPTS_DIR, LINEAGE_FILE, SESSION_POLLUTION_DIR, appendChallengeEvents, atomicWriteFileSync, quarantineAndTombstone, captureAndClearLineageForEntries, adoptRootScopeLayout, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, importLatchTxnId, importLineageCommitted, importSessionPollution, isContainedIn, permissionRemedyOf, withPermissionRemedy, lineageContributionsOfSession, listSessionPollution, readChallengeEvents, readLineageRecord, registerScope, registeredScopes, releaseImportLatch, layoutNeedsAdoption, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, scopeRegistryShape, stageImportLatch, landingPathOf, storeTargetEscape, TARGET_OUTSIDE_STORE, writeAllSync, } from "./layout.js";
|
|
10
12
|
import { screenInboundEntries } from "./data-plane.js";
|
|
11
13
|
import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
|
|
12
14
|
import { thrownText } from "../thrown-value.js";
|
|
13
|
-
export
|
|
14
|
-
export const DEFAULT_MAX_ENTRY_DEPTH = 3;
|
|
15
|
-
export function scanEntryFiles(dir, opts = {}) {
|
|
16
|
-
const maxDepth = opts.maxDepth ?? DEFAULT_MAX_ENTRY_DEPTH;
|
|
17
|
-
const out = [];
|
|
18
|
-
const walk = (d, depth) => {
|
|
19
|
-
let names;
|
|
20
|
-
try {
|
|
21
|
-
names = readdirSync(d);
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
if (d !== dir)
|
|
25
|
-
opts.onSkip?.(d, "unreadable");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
for (const name of names.sort()) {
|
|
29
|
-
if (name.startsWith(".") || name === MEMORY_INDEX_FILENAME) {
|
|
30
|
-
if (name.startsWith("."))
|
|
31
|
-
opts.onSkip?.(join(d, name), "dotfile");
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
const p = join(d, name);
|
|
35
|
-
let st;
|
|
36
|
-
try {
|
|
37
|
-
st = lstatSync(p);
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
opts.onSkip?.(p, "unreadable");
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (st.isSymbolicLink()) {
|
|
44
|
-
opts.onSkip?.(p, "symlink");
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (st.isDirectory()) {
|
|
48
|
-
if (depth + 1 <= maxDepth)
|
|
49
|
-
walk(p, depth + 1);
|
|
50
|
-
else
|
|
51
|
-
opts.onSkip?.(p, "depth");
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (!st.isFile())
|
|
55
|
-
continue;
|
|
56
|
-
if (!name.endsWith(".md")) {
|
|
57
|
-
opts.onSkip?.(p, "nonmd");
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
out.push({ path: p, slug: relative(dir, p).replace(/\.md$/, "") });
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
walk(dir, 0);
|
|
64
|
-
return out;
|
|
65
|
-
}
|
|
15
|
+
export { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./entry-scan.js";
|
|
66
16
|
export function canonicalJsonStringify(value) {
|
|
67
17
|
if (Array.isArray(value))
|
|
68
18
|
return `[${value.map((v) => (v === undefined || typeof v === "function" || typeof v === "symbol" ? "null" : canonicalJsonStringify(v))).join(",")}]`;
|
|
@@ -257,6 +207,45 @@ function validateLedgerRowsV2(rowsRaw, path) {
|
|
|
257
207
|
}
|
|
258
208
|
return out;
|
|
259
209
|
}
|
|
210
|
+
function parseJournal(raw, jp) {
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(raw);
|
|
213
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.ops))
|
|
214
|
+
throw new Error("wrong shape");
|
|
215
|
+
return parsed;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
throw new ControlPlaneCorruptError(`transaction journal is unparseable: ${jp}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function journalLedgerSnapshot(journal, jp) {
|
|
222
|
+
const hasV2 = journal.ledger2 !== undefined;
|
|
223
|
+
const hasV1 = journal.ledger !== undefined;
|
|
224
|
+
if (hasV2 === hasV1) {
|
|
225
|
+
throw new ControlPlaneCorruptError(`transaction journal has ${hasV2 ? "both ledger snapshots" : "no ledger snapshot"} (wrong shape): ${jp}`);
|
|
226
|
+
}
|
|
227
|
+
if (hasV2) {
|
|
228
|
+
const env = journal.ledger2;
|
|
229
|
+
if (!env || typeof env !== "object" || Array.isArray(env) || env.v !== LEDGER_SCHEMA_VERSION) {
|
|
230
|
+
throw new ControlPlaneCorruptError(`transaction journal ledger2 snapshot has the wrong shape: ${jp}`);
|
|
231
|
+
}
|
|
232
|
+
const extras = {};
|
|
233
|
+
for (const [k, val] of Object.entries(env))
|
|
234
|
+
if (k !== "v" && k !== "rows")
|
|
235
|
+
extras[k] = val;
|
|
236
|
+
return { form: "v2", rows: validateLedgerRowsV2(env.rows, jp), extras };
|
|
237
|
+
}
|
|
238
|
+
const map = journal.ledger;
|
|
239
|
+
if (!map || typeof map !== "object" || Array.isArray(map))
|
|
240
|
+
throw new ControlPlaneCorruptError(`transaction journal ledger snapshot has the wrong shape: ${jp}`);
|
|
241
|
+
const rows = Object.create(null);
|
|
242
|
+
for (const [id, rev] of Object.entries(map)) {
|
|
243
|
+
if (typeof rev !== "string")
|
|
244
|
+
throw new ControlPlaneCorruptError(`transaction journal v1 ledger snapshot entry ${JSON.stringify(id)} is not a string: ${jp}`);
|
|
245
|
+
rows[id] = { rev };
|
|
246
|
+
}
|
|
247
|
+
return { form: "v1", rows };
|
|
248
|
+
}
|
|
260
249
|
function parseLedgerText(raw, path) {
|
|
261
250
|
let parsed;
|
|
262
251
|
try {
|
|
@@ -533,6 +522,7 @@ function probeProjection(absPath, id) {
|
|
|
533
522
|
export class FileMemoryEngineBackend {
|
|
534
523
|
directoryRoot;
|
|
535
524
|
controlPlaneRoot;
|
|
525
|
+
physicalRoot;
|
|
536
526
|
now;
|
|
537
527
|
onIncident;
|
|
538
528
|
ledger;
|
|
@@ -556,6 +546,7 @@ export class FileMemoryEngineBackend {
|
|
|
556
546
|
adoptCanonicalKeyedControlDir(resolveMemoryEngineRoot(), dir, opts.onIncident);
|
|
557
547
|
this.controlPlaneRoot = opts.controlDir ?? deriveControlPlaneDir(resolveMemoryEngineRoot(), dir);
|
|
558
548
|
ensureDirExists(dir);
|
|
549
|
+
this.physicalRoot = landingPathOf(dir);
|
|
559
550
|
ensureDirExists(this.controlPlaneRoot);
|
|
560
551
|
this.recoverJournal({ unlocked: true });
|
|
561
552
|
try {
|
|
@@ -588,6 +579,7 @@ export class FileMemoryEngineBackend {
|
|
|
588
579
|
});
|
|
589
580
|
}
|
|
590
581
|
ensureLayout(ownerCandidate) {
|
|
582
|
+
this.assertRootIntact();
|
|
591
583
|
if (!layoutNeedsAdoption(this.directoryRoot, this.controlPlaneRoot))
|
|
592
584
|
return;
|
|
593
585
|
const lock = this.tryAcquireTxnLockSync();
|
|
@@ -602,6 +594,7 @@ export class FileMemoryEngineBackend {
|
|
|
602
594
|
}
|
|
603
595
|
}
|
|
604
596
|
async adoptRootScopeLayout(opts = {}) {
|
|
597
|
+
this.assertRootIntact();
|
|
605
598
|
if (!layoutNeedsAdoption(this.directoryRoot, this.controlPlaneRoot))
|
|
606
599
|
return;
|
|
607
600
|
const lock = await this.acquireTxnLock();
|
|
@@ -1222,6 +1215,7 @@ export class FileMemoryEngineBackend {
|
|
|
1222
1215
|
}
|
|
1223
1216
|
}
|
|
1224
1217
|
recoverJournal(fence) {
|
|
1218
|
+
this.assertRootIntact();
|
|
1225
1219
|
if (fence.token === undefined) {
|
|
1226
1220
|
if (this.txnInFlight())
|
|
1227
1221
|
return;
|
|
@@ -1240,44 +1234,9 @@ export class FileMemoryEngineBackend {
|
|
|
1240
1234
|
const jp = join(this.controlPlaneRoot, JOURNAL_FILE);
|
|
1241
1235
|
const raw = readControlFileOrAbsent(jp, "transaction journal");
|
|
1242
1236
|
if (raw !== undefined) {
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.ops))
|
|
1247
|
-
throw new Error("wrong shape");
|
|
1248
|
-
journal = parsed;
|
|
1249
|
-
}
|
|
1250
|
-
catch {
|
|
1251
|
-
throw new ControlPlaneCorruptError(`transaction journal is unparseable: ${jp}`);
|
|
1252
|
-
}
|
|
1253
|
-
const hasV2 = journal.ledger2 !== undefined;
|
|
1254
|
-
const hasV1 = journal.ledger !== undefined;
|
|
1255
|
-
if (hasV2 === hasV1) {
|
|
1256
|
-
throw new ControlPlaneCorruptError(`transaction journal has ${hasV2 ? "both ledger snapshots" : "no ledger snapshot"} (wrong shape): ${jp}`);
|
|
1257
|
-
}
|
|
1258
|
-
let snapshotRows;
|
|
1259
|
-
let snapshotExtras;
|
|
1260
|
-
if (hasV2) {
|
|
1261
|
-
const env = journal.ledger2;
|
|
1262
|
-
if (!env || typeof env !== "object" || Array.isArray(env) || env.v !== LEDGER_SCHEMA_VERSION) {
|
|
1263
|
-
throw new ControlPlaneCorruptError(`transaction journal ledger2 snapshot has the wrong shape: ${jp}`);
|
|
1264
|
-
}
|
|
1265
|
-
snapshotRows = validateLedgerRowsV2(env.rows, jp);
|
|
1266
|
-
snapshotExtras = {};
|
|
1267
|
-
for (const [k, val] of Object.entries(env))
|
|
1268
|
-
if (k !== "v" && k !== "rows")
|
|
1269
|
-
snapshotExtras[k] = val;
|
|
1270
|
-
}
|
|
1271
|
-
else {
|
|
1272
|
-
const map = journal.ledger;
|
|
1273
|
-
if (!map || typeof map !== "object" || Array.isArray(map))
|
|
1274
|
-
throw new ControlPlaneCorruptError(`transaction journal ledger snapshot has the wrong shape: ${jp}`);
|
|
1275
|
-
snapshotRows = Object.create(null);
|
|
1276
|
-
for (const [id, rev] of Object.entries(map)) {
|
|
1277
|
-
if (typeof rev !== "string")
|
|
1278
|
-
throw new ControlPlaneCorruptError(`transaction journal v1 ledger snapshot entry ${JSON.stringify(id)} is not a string: ${jp}`);
|
|
1279
|
-
snapshotRows[id] = { rev };
|
|
1280
|
-
}
|
|
1237
|
+
const journal = parseJournal(raw, jp);
|
|
1238
|
+
const { rows: snapshotRows, extras: snapshotExtras, form: snapshotForm } = journalLedgerSnapshot(journal, jp);
|
|
1239
|
+
if (snapshotForm === "v1") {
|
|
1281
1240
|
try {
|
|
1282
1241
|
const diskRaw = readControlFileOrAbsent(join(this.controlPlaneRoot, LEDGER_FILE), "committed-rev ledger");
|
|
1283
1242
|
const diskParsed = diskRaw !== undefined ? JSON.parse(diskRaw) : undefined;
|
|
@@ -1314,7 +1273,7 @@ export class FileMemoryEngineBackend {
|
|
|
1314
1273
|
}
|
|
1315
1274
|
journalEvs.set(e.ev, canonical);
|
|
1316
1275
|
}
|
|
1317
|
-
if (
|
|
1276
|
+
if (snapshotForm === "v1")
|
|
1318
1277
|
throw new ControlPlaneCorruptError(`transaction journal carries transfers with a v1 snapshot (impossible form): ${jp}`);
|
|
1319
1278
|
if (journalEvs.size > 0) {
|
|
1320
1279
|
for (const row of this.readTransferChain(fenceToken)) {
|
|
@@ -1445,7 +1404,10 @@ export class FileMemoryEngineBackend {
|
|
|
1445
1404
|
}
|
|
1446
1405
|
}
|
|
1447
1406
|
this.assertTxnLockOwnership(fenceToken);
|
|
1407
|
+
const replayAt = `the journal replay (journal kept at ${jp})`;
|
|
1408
|
+
this.refuseEscapingTargets(journal.ops, replayAt);
|
|
1448
1409
|
for (const op of journal.ops) {
|
|
1410
|
+
this.refuseEscapingTargets([op], replayAt);
|
|
1449
1411
|
try {
|
|
1450
1412
|
if (op.kind === "write" && op.content !== undefined) {
|
|
1451
1413
|
atomicWriteFileSync(op.target, op.content);
|
|
@@ -1464,12 +1426,12 @@ export class FileMemoryEngineBackend {
|
|
|
1464
1426
|
}
|
|
1465
1427
|
catch (err) {
|
|
1466
1428
|
const code = err.code;
|
|
1467
|
-
throw new ControlPlaneCorruptError(`transaction journal replay failed on op kind=${op.kind} target=${JSON.stringify(op.target)} (${code ?? (thrownText(err))}) — the journal is kept at ${jp}; an I/O failure retries at the next construction, a persistent fault needs the explicit control-plane rebuild path`);
|
|
1429
|
+
throw new ControlPlaneCorruptError(`transaction journal replay failed on op kind=${op.kind} target=${JSON.stringify(op.target)} (${code ?? (thrownText(err))}) — the journal is kept at ${jp}; ${permissionRemedyOf(err, this.directoryRoot) ?? "an I/O failure retries at the next construction, a persistent fault needs the explicit control-plane rebuild path"}`);
|
|
1468
1430
|
}
|
|
1469
1431
|
}
|
|
1470
1432
|
this.recoveryCommitProbe?.();
|
|
1471
1433
|
this.assertTxnLockOwnership(fenceToken);
|
|
1472
|
-
if (
|
|
1434
|
+
if (snapshotForm === "v2") {
|
|
1473
1435
|
this.ledger = snapshotRows;
|
|
1474
1436
|
this.envelopeExtras = snapshotExtras;
|
|
1475
1437
|
this.persistedSchemaVersion = "v2";
|
|
@@ -1503,6 +1465,24 @@ export class FileMemoryEngineBackend {
|
|
|
1503
1465
|
throw new ControlPlaneCorruptError(`txn lock state could not be probed (${code ?? "io error"}) at ${this.txnLockDir()} — a probe failure is not "no writer" (fail-closed)`);
|
|
1504
1466
|
}
|
|
1505
1467
|
}
|
|
1468
|
+
refuseEscapingTargets(ops, at) {
|
|
1469
|
+
for (const op of ops) {
|
|
1470
|
+
if (op.kind !== "write" && op.kind !== "delete")
|
|
1471
|
+
continue;
|
|
1472
|
+
for (const path of op.staged === undefined ? [op.target] : [op.target, op.staged]) {
|
|
1473
|
+
const escape = storeTargetEscape(this.physicalRoot, path);
|
|
1474
|
+
if (escape === undefined)
|
|
1475
|
+
continue;
|
|
1476
|
+
throw erasureCodedError(`${TARGET_OUTSIDE_STORE}: ${at} refused a ${op.kind} of ${op.target}${path === op.target ? "" : ` (its staged file ${path})`} — ${escape}; not performed`, TARGET_OUTSIDE_STORE);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
assertRootIntact() {
|
|
1481
|
+
const now = landingPathOf(this.directoryRoot);
|
|
1482
|
+
if (now === this.physicalRoot)
|
|
1483
|
+
return;
|
|
1484
|
+
throw erasureCodedError(`${TARGET_OUTSIDE_STORE}: the store root ${this.directoryRoot} now resolves to ${now}, not ${this.physicalRoot} where this backend opened it — refused; open a new backend on the directory you mean`, TARGET_OUTSIDE_STORE);
|
|
1485
|
+
}
|
|
1506
1486
|
sweepStagedFiles() {
|
|
1507
1487
|
const walk = (d, depth) => {
|
|
1508
1488
|
let names;
|
|
@@ -1589,6 +1569,9 @@ export class FileMemoryEngineBackend {
|
|
|
1589
1569
|
const moveEvents = [];
|
|
1590
1570
|
const moveAnnouncements = [];
|
|
1591
1571
|
const skippedUnreadable = [];
|
|
1572
|
+
let table;
|
|
1573
|
+
const observed = () => (table ??= layerObservationLookup(this.controlPlaneRoot));
|
|
1574
|
+
let freshAccount;
|
|
1592
1575
|
for (const f of files) {
|
|
1593
1576
|
let text;
|
|
1594
1577
|
try {
|
|
@@ -1608,6 +1591,12 @@ export class FileMemoryEngineBackend {
|
|
|
1608
1591
|
const committed = rowsRev(rows, entry.id);
|
|
1609
1592
|
const binding = rowsBinding(rows, entry.id);
|
|
1610
1593
|
const relPath = relative(this.directoryRoot, f.path);
|
|
1594
|
+
if (probeOnly === true && observed().covers(entry.id)) {
|
|
1595
|
+
const fresh = (freshAccount ??= this.committedRowsOrFault() ?? null);
|
|
1596
|
+
const freshBinding = fresh !== null ? rowsBinding(fresh, entry.id) : undefined;
|
|
1597
|
+
if (fresh === null || rowsRev(fresh, entry.id) !== committed || freshBinding?.scope !== binding?.scope || freshBinding?.slug !== binding?.slug)
|
|
1598
|
+
return undefined;
|
|
1599
|
+
}
|
|
1611
1600
|
if (committed !== undefined && binding !== undefined && !(binding.scope === scope && binding.slug === f.slug)) {
|
|
1612
1601
|
const probe = probeProjection(this.bindingAbsPath(binding), entry.id);
|
|
1613
1602
|
if (probe === "present") {
|
|
@@ -1618,6 +1607,8 @@ export class FileMemoryEngineBackend {
|
|
|
1618
1607
|
this.announceAdoptionNotice(`bindprobe|${relPath}`, `committed entry ${entry.id} appears at ${JSON.stringify(inlineUntrusted(relPath))} but is bound to ${JSON.stringify(inlineUntrusted(this.bindingRelPath(binding)))}, and the bound projection could not be probed conclusively (a read failure, or a foreign file at that address — not ENOENT) — nothing is served or adopted this pass`);
|
|
1619
1608
|
continue;
|
|
1620
1609
|
}
|
|
1610
|
+
if (this.settledAsObserved(observed(), entries, entry, relPath, f.slug, scope, committed, binding, text, "quarantined-only"))
|
|
1611
|
+
continue;
|
|
1621
1612
|
if (probeOnly)
|
|
1622
1613
|
return undefined;
|
|
1623
1614
|
if (lockToken !== undefined)
|
|
@@ -1630,6 +1621,8 @@ export class FileMemoryEngineBackend {
|
|
|
1630
1621
|
continue;
|
|
1631
1622
|
}
|
|
1632
1623
|
if (committed !== entry.rev) {
|
|
1624
|
+
if (committed !== undefined && this.settledAsObserved(observed(), entries, entry, relPath, f.slug, scope, committed, binding, text, "restored"))
|
|
1625
|
+
continue;
|
|
1633
1626
|
if (probeOnly)
|
|
1634
1627
|
return undefined;
|
|
1635
1628
|
if (lockToken !== undefined)
|
|
@@ -1664,6 +1657,8 @@ export class FileMemoryEngineBackend {
|
|
|
1664
1657
|
entry = entryFromFile(shadowText, entry.id, f.slug, scope);
|
|
1665
1658
|
}
|
|
1666
1659
|
else {
|
|
1660
|
+
if (committed === undefined && this.settledAsObserved(observed(), entries, entry, relPath, f.slug, scope, committed, binding, text, "restored"))
|
|
1661
|
+
continue;
|
|
1667
1662
|
if (committed !== undefined)
|
|
1668
1663
|
adoptedExternal.push(relPath);
|
|
1669
1664
|
bindRowTracked(rows, entry.id, { rev: entry.rev, scope, slug: f.slug, at: this.now() }, moveEvents, "adopted-move");
|
|
@@ -1740,6 +1735,25 @@ export class FileMemoryEngineBackend {
|
|
|
1740
1735
|
this.batchScan.set(scope, entries);
|
|
1741
1736
|
return entries;
|
|
1742
1737
|
}
|
|
1738
|
+
settledAsObserved(table, out, entry, relPath, slug, scope, committed, binding, text, containment) {
|
|
1739
|
+
if (!table.covers(entry.id) || this.inboundGate(relPath, text) !== undefined)
|
|
1740
|
+
return false;
|
|
1741
|
+
if (committed !== undefined && committed !== entry.rev && this.whitewashInboundFinding(relPath, entry, containment) !== undefined)
|
|
1742
|
+
return false;
|
|
1743
|
+
const hit = table.find(entry.id, committed, entry.rev);
|
|
1744
|
+
if (hit === undefined)
|
|
1745
|
+
return false;
|
|
1746
|
+
const standIn = committed !== undefined && binding !== undefined && binding.scope === scope && binding.slug === slug ? this.readCommittedShadow(entry.id) : undefined;
|
|
1747
|
+
const notice = observedDivergenceNotice(relPath, standIn !== undefined, hit.undecided === true);
|
|
1748
|
+
try {
|
|
1749
|
+
enqueueMemoryAnnouncement(this.controlPlaneRoot, { kind: "external", at: this.now(), items: [notice] }, { supersedesPrefix: notice });
|
|
1750
|
+
}
|
|
1751
|
+
catch {
|
|
1752
|
+
}
|
|
1753
|
+
if (standIn !== undefined)
|
|
1754
|
+
out.push(entryFromFile(standIn, entry.id, slug, scope));
|
|
1755
|
+
return true;
|
|
1756
|
+
}
|
|
1743
1757
|
whitewashInboundFinding(relPath, entry, containment) {
|
|
1744
1758
|
const committedShadow = this.readCommittedShadow(entry.id);
|
|
1745
1759
|
const committedFm = committedShadow !== undefined ? parseEntryFile(committedShadow).frontmatter : undefined;
|
|
@@ -1862,14 +1876,46 @@ export class FileMemoryEngineBackend {
|
|
|
1862
1876
|
throw new Error(`memory retrieval view is read-only — ${op} must go through the backend itself`);
|
|
1863
1877
|
};
|
|
1864
1878
|
return {
|
|
1865
|
-
listHeaders: async (scopes) => (this.ensureLayout(), this.listHeadersFrom(scopes, (s) => this.readScope(s, false))),
|
|
1866
|
-
getByIds: async (ids) => (this.ensureLayout(), this.getByIdsFrom(ids, (s) => this.readScope(s, false))),
|
|
1867
|
-
search: async (query, scopes, opts) => (this.ensureLayout(), this.searchFrom(query, scopes, opts, (s) => this.readScope(s, false))),
|
|
1879
|
+
listHeaders: async (scopes) => (this.ensureLayout(), this.listHeadersFrom(scopes, (s) => this.withoutObserved(this.readScope(s, false), s))),
|
|
1880
|
+
getByIds: async (ids) => (this.ensureLayout(), this.getByIdsFrom(ids, (s) => this.withoutObserved(this.readScope(s, false), s))),
|
|
1881
|
+
search: async (query, scopes, opts) => (this.ensureLayout(), this.searchFrom(query, scopes, opts, (s) => this.withoutObserved(this.readScope(s, false), s))),
|
|
1868
1882
|
applyPatches: async () => refuse("applyPatches"),
|
|
1869
1883
|
getConsolidationCursor: async (scope) => this.getConsolidationCursor(scope),
|
|
1870
1884
|
setConsolidationCursor: async () => refuse("setConsolidationCursor"),
|
|
1871
1885
|
};
|
|
1872
1886
|
}
|
|
1887
|
+
withoutObserved(entries, scope) {
|
|
1888
|
+
const table = layerObservationLookup(this.controlPlaneRoot);
|
|
1889
|
+
if (!entries.some((e) => table.covers(e.id)))
|
|
1890
|
+
return entries;
|
|
1891
|
+
const account = this.committedRowsOrFault();
|
|
1892
|
+
if (account === undefined)
|
|
1893
|
+
return entries.filter((e) => !table.covers(e.id));
|
|
1894
|
+
const out = [];
|
|
1895
|
+
for (const e of entries) {
|
|
1896
|
+
const committed = rowsRev(account, e.id);
|
|
1897
|
+
const binding = rowsBinding(account, e.id);
|
|
1898
|
+
const boundHere = binding !== undefined && binding.scope === scope && binding.slug === e.slug;
|
|
1899
|
+
if ((committed === e.rev && boundHere) || table.find(e.id, committed, e.rev) === undefined) {
|
|
1900
|
+
out.push(e);
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
const standIn = committed !== undefined && boundHere ? this.readCommittedShadow(e.id) : undefined;
|
|
1904
|
+
if (standIn !== undefined)
|
|
1905
|
+
out.push(entryFromFile(standIn, e.id, e.slug, scope));
|
|
1906
|
+
}
|
|
1907
|
+
return out;
|
|
1908
|
+
}
|
|
1909
|
+
committedRowsOrFault() {
|
|
1910
|
+
const path = join(this.controlPlaneRoot, LEDGER_FILE);
|
|
1911
|
+
try {
|
|
1912
|
+
const raw = readControlFileOrAbsent(path, "committed-rev ledger");
|
|
1913
|
+
return raw === undefined ? Object.create(null) : parseLedgerText(raw, path).rows;
|
|
1914
|
+
}
|
|
1915
|
+
catch {
|
|
1916
|
+
return undefined;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1873
1919
|
restrictedFindingKeys = new Set();
|
|
1874
1920
|
recordRestrictedFinding(key, finding) {
|
|
1875
1921
|
if (this.restrictedFindingKeys.has(key))
|
|
@@ -2187,6 +2233,9 @@ export class FileMemoryEngineBackend {
|
|
|
2187
2233
|
try {
|
|
2188
2234
|
return await this.applyPatchesLocked(patches, lock.token);
|
|
2189
2235
|
}
|
|
2236
|
+
catch (err) {
|
|
2237
|
+
throw withPermissionRemedy(err, this.directoryRoot);
|
|
2238
|
+
}
|
|
2190
2239
|
finally {
|
|
2191
2240
|
lock.release();
|
|
2192
2241
|
}
|
|
@@ -2343,16 +2392,37 @@ export class FileMemoryEngineBackend {
|
|
|
2343
2392
|
return report;
|
|
2344
2393
|
}
|
|
2345
2394
|
const txn = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2395
|
+
const stagedFiles = [];
|
|
2396
|
+
const refuseUnstaging = (batch, at) => {
|
|
2397
|
+
try {
|
|
2398
|
+
this.refuseEscapingTargets(batch, at);
|
|
2399
|
+
}
|
|
2400
|
+
catch (err) {
|
|
2401
|
+
for (const f of stagedFiles) {
|
|
2402
|
+
try {
|
|
2403
|
+
const st = lstatSync(f.path);
|
|
2404
|
+
if (st.dev === f.dev && st.ino === f.ino)
|
|
2405
|
+
rmSync(f.path, { force: true });
|
|
2406
|
+
}
|
|
2407
|
+
catch {
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
throw err;
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2346
2413
|
let n = 0;
|
|
2347
2414
|
for (const op of ops) {
|
|
2348
2415
|
if (op.kind !== "write" || op.content === undefined)
|
|
2349
2416
|
continue;
|
|
2417
|
+
refuseUnstaging([op], "applyPatches (staging)");
|
|
2350
2418
|
const staged = join(dirname(op.target), `.stage-${txn}-${n++}`);
|
|
2351
2419
|
mkdirSync(dirname(op.target), { recursive: true });
|
|
2352
2420
|
const fd = openSync(staged, "w", 0o600);
|
|
2353
2421
|
try {
|
|
2354
2422
|
writeAllSync(fd, op.content);
|
|
2355
2423
|
fsyncSync(fd);
|
|
2424
|
+
const st = fstatSync(fd);
|
|
2425
|
+
stagedFiles.push({ path: staged, dev: st.dev, ino: st.ino });
|
|
2356
2426
|
}
|
|
2357
2427
|
finally {
|
|
2358
2428
|
closeSync(fd);
|
|
@@ -2370,8 +2440,10 @@ export class FileMemoryEngineBackend {
|
|
|
2370
2440
|
if (coherentTransfers.length > 0)
|
|
2371
2441
|
this.precheckTransfersAppendable();
|
|
2372
2442
|
const journal = { txn, ops, ledger2: ledgerEnvelope(rows, this.envelopeExtras), ...(coherentTransfers.length > 0 ? { transfers: coherentTransfers } : {}) };
|
|
2443
|
+
refuseUnstaging(ops, "applyPatches (commit point)");
|
|
2373
2444
|
atomicWriteFileSync(join(this.controlPlaneRoot, JOURNAL_FILE), JSON.stringify(journal));
|
|
2374
2445
|
for (const op of ops) {
|
|
2446
|
+
this.refuseEscapingTargets([op], "applyPatches (after the commit point; the journal is kept)");
|
|
2375
2447
|
if (op.kind === "write" && op.staged) {
|
|
2376
2448
|
renameSync(op.staged, op.target);
|
|
2377
2449
|
}
|
|
@@ -2796,8 +2868,10 @@ export class FileMemoryEngineBackend {
|
|
|
2796
2868
|
this.assertTxnLockOwnership(lock.token);
|
|
2797
2869
|
this.precheckTransfersAppendable();
|
|
2798
2870
|
const journal = { txn, ops, ledger2: ledgerEnvelope(rows, this.envelopeExtras), transfers };
|
|
2871
|
+
this.refuseEscapingTargets(ops, "eraseWithEvidence (commit point)");
|
|
2799
2872
|
atomicWriteFileSync(join(this.controlPlaneRoot, JOURNAL_FILE), JSON.stringify(journal));
|
|
2800
2873
|
for (const op of ops) {
|
|
2874
|
+
this.refuseEscapingTargets([op], "eraseWithEvidence (after the commit point; the journal is kept)");
|
|
2801
2875
|
if (op.kind === "delete") {
|
|
2802
2876
|
rmSync(op.target, { force: true });
|
|
2803
2877
|
}
|
|
@@ -2865,7 +2939,7 @@ export class FileMemoryEngineBackend {
|
|
|
2865
2939
|
}
|
|
2866
2940
|
catch (err) {
|
|
2867
2941
|
this.ledger = undefined;
|
|
2868
|
-
throw err;
|
|
2942
|
+
throw withPermissionRemedy(err, this.directoryRoot);
|
|
2869
2943
|
}
|
|
2870
2944
|
finally {
|
|
2871
2945
|
lock.release();
|