loopforge 3.0.0 → 3.6.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/README.md +126 -143
- package/dist/audit.d.ts +50 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +131 -0
- package/dist/audit.js.map +1 -0
- package/dist/canonical-state.d.ts +98 -23
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +257 -181
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +40 -33
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +17 -0
- package/dist/cognitive-facts.d.ts.map +1 -0
- package/dist/cognitive-facts.js +99 -0
- package/dist/cognitive-facts.js.map +1 -0
- package/dist/cognitive-governance.d.ts +25 -33
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +38 -138
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +59 -0
- package/dist/committed-round.d.ts.map +1 -0
- package/dist/committed-round.js +222 -0
- package/dist/committed-round.js.map +1 -0
- package/dist/enforcement-gate.d.ts +78 -0
- package/dist/enforcement-gate.d.ts.map +1 -0
- package/dist/enforcement-gate.js +1109 -0
- package/dist/enforcement-gate.js.map +1 -0
- package/dist/engine.d.ts +111 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +887 -0
- package/dist/engine.js.map +1 -0
- package/dist/evidence-claims.d.ts +50 -0
- package/dist/evidence-claims.d.ts.map +1 -0
- package/dist/evidence-claims.js +116 -0
- package/dist/evidence-claims.js.map +1 -0
- package/dist/evidence-provider.d.ts +50 -23
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +167 -50
- package/dist/evidence-provider.js.map +1 -1
- package/dist/generate-schema.js +1 -84
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +19 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -15
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +58 -3
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +841 -304
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +2 -2
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +2 -2
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts +6 -0
- package/dist/loop-projection.d.ts.map +1 -0
- package/dist/loop-projection.js +26 -0
- package/dist/loop-projection.js.map +1 -0
- package/dist/loop-store.d.ts +115 -24
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +365 -218
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +214 -0
- package/dist/mcp/round-lifecycle.d.ts.map +1 -0
- package/dist/mcp/round-lifecycle.js +935 -0
- package/dist/mcp/round-lifecycle.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +29 -43
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +64 -101
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +433 -1110
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +20 -11
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +587 -413
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +12 -24
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +78 -184
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +61 -50
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +48 -146
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +37 -6
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +487 -278
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -8
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +8 -26
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +398 -236
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +147 -17
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +5 -13
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +32 -69
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +64 -0
- package/dist/round-contract.d.ts.map +1 -0
- package/dist/round-contract.js +80 -0
- package/dist/round-contract.js.map +1 -0
- package/dist/round-coordinator.d.ts +23 -25
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +103 -92
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +13 -13
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +25 -14
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +14 -18
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +37 -47
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +48 -0
- package/dist/self-eval.d.ts.map +1 -0
- package/dist/self-eval.js +248 -0
- package/dist/self-eval.js.map +1 -0
- package/dist/storage.d.ts +9 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +18 -6
- package/dist/storage.js.map +1 -1
- package/dist/token-utils.d.ts +15 -3
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +24 -5
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +149 -0
- package/dist/verification-gate.d.ts.map +1 -0
- package/dist/verification-gate.js +1343 -0
- package/dist/verification-gate.js.map +1 -0
- package/dist/workspace.d.ts +29 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +61 -0
- package/dist/workspace.js.map +1 -0
- package/loop_policy.json +37 -16
- package/package.json +5 -10
- package/skills/perception/SKILL.md +184 -202
- package/dist/canonical-event-writer.d.ts +0 -20
- package/dist/canonical-event-writer.d.ts.map +0 -1
- package/dist/canonical-event-writer.js +0 -131
- package/dist/canonical-event-writer.js.map +0 -1
- package/dist/compiler-subgoals.d.ts +0 -7
- package/dist/compiler-subgoals.d.ts.map +0 -1
- package/dist/compiler-subgoals.js +0 -93
- package/dist/compiler-subgoals.js.map +0 -1
- package/dist/derive-id.d.ts +0 -12
- package/dist/derive-id.d.ts.map +0 -1
- package/dist/derive-id.js +0 -31
- package/dist/derive-id.js.map +0 -1
- package/dist/mcp/session-projection.d.ts +0 -5
- package/dist/mcp/session-projection.d.ts.map +0 -1
- package/dist/mcp/session-projection.js +0 -18
- package/dist/mcp/session-projection.js.map +0 -1
- package/dist/mcp/session-queue.d.ts +0 -3
- package/dist/mcp/session-queue.d.ts.map +0 -1
- package/dist/mcp/session-queue.js +0 -18
- package/dist/mcp/session-queue.js.map +0 -1
- package/dist/mcp/session-types.d.ts +0 -72
- package/dist/mcp/session-types.d.ts.map +0 -1
- package/dist/mcp/session-types.js +0 -2
- package/dist/mcp/session-types.js.map +0 -1
- package/dist/round-commit-store.d.ts +0 -12
- package/dist/round-commit-store.d.ts.map +0 -1
- package/dist/round-commit-store.js +0 -13
- package/dist/round-commit-store.js.map +0 -1
- package/dist/round-gate.d.ts +0 -31
- package/dist/round-gate.d.ts.map +0 -1
- package/dist/round-gate.js +0 -259
- package/dist/round-gate.js.map +0 -1
- package/dist/round-recovery.d.ts +0 -17
- package/dist/round-recovery.d.ts.map +0 -1
- package/dist/round-recovery.js +0 -117
- package/dist/round-recovery.js.map +0 -1
- package/dist/round-report.d.ts +0 -16
- package/dist/round-report.d.ts.map +0 -1
- package/dist/round-report.js +0 -401
- package/dist/round-report.js.map +0 -1
- package/dist/runtime-services.d.ts +0 -59
- package/dist/runtime-services.d.ts.map +0 -1
- package/dist/runtime-services.js +0 -460
- package/dist/runtime-services.js.map +0 -1
- package/dist/vault-format.d.ts +0 -4
- package/dist/vault-format.d.ts.map +0 -1
- package/dist/vault-format.js +0 -4
- package/dist/vault-format.js.map +0 -1
- package/dist/vault-types.d.ts +0 -24
- package/dist/vault-types.d.ts.map +0 -1
- package/dist/vault-types.js +0 -2
- package/dist/vault-types.js.map +0 -1
- package/loopforge-policy.json +0 -340
package/dist/loop-store.js
CHANGED
|
@@ -1,39 +1,128 @@
|
|
|
1
|
-
/** Typed per-loop persistence.
|
|
1
|
+
/** Typed per-loop persistence.
|
|
2
|
+
*
|
|
3
|
+
* Layout:
|
|
4
|
+
* .loopforge/loops/<sha256(loopId)>/session.json
|
|
5
|
+
* .loopforge/loops/<sha256(loopId)>/rounds/<round>.json
|
|
6
|
+
*
|
|
7
|
+
* Markdown state files are derived views. These JSON documents are the only
|
|
8
|
+
* durable transaction truth.
|
|
9
|
+
*/
|
|
2
10
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
4
12
|
import { dirname, join, resolve } from "node:path";
|
|
5
13
|
import { parseRoundTransactionSnapshot } from "./round-transaction.js";
|
|
6
14
|
import { validateLoopId } from "./policy.js";
|
|
7
15
|
import { isRecord } from "./token-utils.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
16
|
+
export const LOOP_STORE_SCHEMA_VERSION = 1;
|
|
17
|
+
const SEVERITY_MAP = {
|
|
18
|
+
sequence_gap: "recoverable",
|
|
19
|
+
sequence_duplicate: "recoverable",
|
|
20
|
+
invalid_json: "corrupted",
|
|
21
|
+
invalid_format: "corrupted",
|
|
22
|
+
sequence_cross_loop: "corrupted",
|
|
23
|
+
sequence_invalid: "corrupted",
|
|
24
|
+
};
|
|
25
|
+
/** v2.12: Typed corruption error carrying a severity class. Callers may
|
|
26
|
+
* recover from `recoverable` errors (record and continue) but must surface
|
|
27
|
+
* `corrupted` errors — the runtime never silently repairs storage. */
|
|
17
28
|
export class StorageCorruptionError extends Error {
|
|
18
|
-
kind;
|
|
19
|
-
path;
|
|
20
|
-
loopId;
|
|
21
|
-
sequence;
|
|
22
29
|
code = "storage_corruption";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this.path = path;
|
|
28
|
-
this.loopId = loopId;
|
|
29
|
-
this.sequence = sequence;
|
|
30
|
+
kind;
|
|
31
|
+
severity;
|
|
32
|
+
constructor(kind, message) {
|
|
33
|
+
super(message);
|
|
30
34
|
this.name = "StorageCorruptionError";
|
|
35
|
+
this.kind = kind;
|
|
36
|
+
this.severity = SEVERITY_MAP[kind];
|
|
37
|
+
}
|
|
38
|
+
get recoverable() {
|
|
39
|
+
return this.severity === "recoverable";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Zero-dependency synchronous sleep (Atomics.wait on a shared slot). */
|
|
43
|
+
function sleepSync(ms) {
|
|
44
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
45
|
+
}
|
|
46
|
+
/** Filter a loop's flat entry view with the legacy VaultBackend query
|
|
47
|
+
* options. Derived read-only view over the single durable truth (typed
|
|
48
|
+
* session/round documents) — never a separate write path. Feedback
|
|
49
|
+
* entries are excluded by default, mirroring the historical
|
|
50
|
+
* queryEntries({ feedbackOnly }) semantics. */
|
|
51
|
+
export function queryLoopEntries(store, loopId, opts) {
|
|
52
|
+
return store.listEntries(loopId, { sinceRound: opts?.sinceRound }).filter((entry) => {
|
|
53
|
+
const taskId = String(entry.task_id ?? "");
|
|
54
|
+
if (opts?.feedbackOnly && !taskId.endsWith(":feedback"))
|
|
55
|
+
return false;
|
|
56
|
+
if (!opts?.feedbackOnly && taskId.endsWith(":feedback"))
|
|
57
|
+
return false;
|
|
58
|
+
if (opts?.prefix) {
|
|
59
|
+
if (!taskId.startsWith(opts.prefix))
|
|
60
|
+
return false;
|
|
61
|
+
// Guard against ambiguous prefix matches: "loop:x:r1" must not
|
|
62
|
+
// match "loop:x:r10" or "loop:x:r11". Only check when the prefix
|
|
63
|
+
// itself ends with a digit (indicating a specific round number).
|
|
64
|
+
// Prefixes ending in non-digits (e.g. "loop:x:r") match any round
|
|
65
|
+
// and should NOT be filtered.
|
|
66
|
+
const lastChar = opts.prefix[opts.prefix.length - 1];
|
|
67
|
+
if (lastChar !== undefined && /^\d$/.test(lastChar)) {
|
|
68
|
+
const after = taskId[opts.prefix.length];
|
|
69
|
+
if (after !== undefined && /^\d$/.test(after))
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** v2.12: Ordered round sequence for a loop. Throws StorageCorruptionError
|
|
77
|
+
* on gaps (recoverable) or mixed-format corruption; returns [] for loops
|
|
78
|
+
* with no rounds. Consumed by audit (sequenceComplete) and resume. */
|
|
79
|
+
export function eventSequence(store, loopId) {
|
|
80
|
+
checkRoundSequence(store, loopId);
|
|
81
|
+
return store.listRoundSequences(loopId).map((doc) => doc.round);
|
|
82
|
+
}
|
|
83
|
+
/** v2.12: Validate that a loop's round documents form a contiguous sequence
|
|
84
|
+
* from 1 to max. Legacy loops (no sequence stamps at all) are exempt.
|
|
85
|
+
* Mixed stamping is allowed only monotonically: rounds below the first
|
|
86
|
+
* stamped round are treated as legacy; once stamping begins it must not
|
|
87
|
+
* stop. Throws StorageCorruptionError on violation. */
|
|
88
|
+
export function checkRoundSequence(store, loopId) {
|
|
89
|
+
const docs = store.listRoundSequences(loopId);
|
|
90
|
+
const stamped = docs.filter((doc) => doc.sequence !== undefined);
|
|
91
|
+
if (stamped.length === 0)
|
|
92
|
+
return { complete: true, legacy: true };
|
|
93
|
+
const firstStamped = Math.min(...stamped.map((doc) => doc.round));
|
|
94
|
+
// Any round at or after the first stamped round must itself be stamped.
|
|
95
|
+
for (const doc of docs) {
|
|
96
|
+
if (doc.round >= firstStamped && doc.sequence === undefined) {
|
|
97
|
+
throw new StorageCorruptionError("sequence_invalid", `Loop ${loopId}: round ${doc.round} is missing its sequence stamp ` +
|
|
98
|
+
`while round ${firstStamped} is stamped (mixed format)`);
|
|
99
|
+
}
|
|
31
100
|
}
|
|
101
|
+
const stampedRounds = stamped.map((doc) => doc.round);
|
|
102
|
+
const max = Math.max(...stampedRounds);
|
|
103
|
+
const present = new Set(docs.filter((doc) => doc.round >= firstStamped).map((doc) => doc.round));
|
|
104
|
+
for (let round = firstStamped; round <= max; round++) {
|
|
105
|
+
if (!present.has(round)) {
|
|
106
|
+
throw new StorageCorruptionError("sequence_gap", `Loop ${loopId}: round sequence gap at ${round} (max ${max})`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// v2.14: rounds below the first stamped round are exempt ONLY for loops
|
|
110
|
+
// without a runtime session document — migration imports may legitimately
|
|
111
|
+
// start at any round, but migration never writes session documents.
|
|
112
|
+
// A stamped loop WITH a session always began at round 1 (write-time
|
|
113
|
+
// continuity), so missing rounds 1..N-1 mean deletion, not import.
|
|
114
|
+
if (firstStamped > 1 && store.readSession(loopId)) {
|
|
115
|
+
throw new StorageCorruptionError("sequence_gap", `Loop ${loopId}: rounds 1..${firstStamped - 1} are missing ` +
|
|
116
|
+
`(first stamped round is ${firstStamped})`);
|
|
117
|
+
}
|
|
118
|
+
return { complete: true, legacy: false };
|
|
32
119
|
}
|
|
33
120
|
function loopIdFromEntry(entry) {
|
|
34
121
|
if (typeof entry.loop_id === "string" && entry.loop_id)
|
|
35
122
|
return entry.loop_id;
|
|
36
123
|
const taskId = String(entry.task_id ?? "");
|
|
124
|
+
if (!taskId.startsWith("loop:"))
|
|
125
|
+
return null;
|
|
37
126
|
const session = taskId.match(/^loop:(.+):session$/);
|
|
38
127
|
if (session)
|
|
39
128
|
return session[1];
|
|
@@ -41,19 +130,22 @@ function loopIdFromEntry(entry) {
|
|
|
41
130
|
return round?.[1] ?? null;
|
|
42
131
|
}
|
|
43
132
|
function roundFromEntry(entry) {
|
|
44
|
-
const
|
|
45
|
-
if (typeof round === "number" && Number.isInteger(round))
|
|
46
|
-
return round;
|
|
133
|
+
const data = entry.loop_lineage;
|
|
134
|
+
if (data && typeof data.round === "number" && Number.isInteger(data.round)) {
|
|
135
|
+
return data.round;
|
|
136
|
+
}
|
|
47
137
|
const match = String(entry.task_id ?? "").match(/:r(\d+)(?::|$)/);
|
|
48
138
|
return match ? Number(match[1]) : null;
|
|
49
139
|
}
|
|
50
|
-
function
|
|
51
|
-
return
|
|
140
|
+
function promptFromSnapshot(snapshot) {
|
|
141
|
+
return snapshot?.promptArtifact;
|
|
52
142
|
}
|
|
53
143
|
export class FileLoopStore {
|
|
54
144
|
root;
|
|
55
145
|
lockDepth = 0;
|
|
56
|
-
constructor(root = ".loopforge") {
|
|
146
|
+
constructor(root = ".loopforge") {
|
|
147
|
+
this.root = resolve(root);
|
|
148
|
+
}
|
|
57
149
|
withLock(fn) {
|
|
58
150
|
if (this.lockDepth > 0) {
|
|
59
151
|
this.lockDepth++;
|
|
@@ -65,64 +157,57 @@ export class FileLoopStore {
|
|
|
65
157
|
}
|
|
66
158
|
}
|
|
67
159
|
mkdirSync(this.root, { recursive: true });
|
|
68
|
-
const
|
|
69
|
-
const
|
|
160
|
+
const lockPath = join(this.root, ".store.lock");
|
|
161
|
+
const ownerPath = join(lockPath, "owner.json");
|
|
70
162
|
const token = randomUUID();
|
|
71
163
|
const deadline = Date.now() + 1000;
|
|
72
|
-
let spinCount = 0;
|
|
73
164
|
for (;;) {
|
|
74
165
|
try {
|
|
75
|
-
mkdirSync(
|
|
76
|
-
writeFileSync(
|
|
166
|
+
mkdirSync(lockPath);
|
|
167
|
+
writeFileSync(ownerPath, JSON.stringify({ token, pid: process.pid, createdAt: Date.now() }));
|
|
77
168
|
break;
|
|
78
169
|
}
|
|
79
170
|
catch {
|
|
80
171
|
let stale = false;
|
|
172
|
+
let age = 0;
|
|
81
173
|
try {
|
|
82
|
-
|
|
83
|
-
const age = Date.now() - statSync(lock).mtimeMs;
|
|
84
|
-
if (age > 5000) {
|
|
85
|
-
// Unreadable owner (orphaned lock) or a self-owned lock (crashed
|
|
86
|
-
// during release) is reclaimable; otherwise probe the pid. The
|
|
87
|
-
// 5s age gate keeps the mkdir→owner.json race window out of the
|
|
88
|
-
// reclaim path.
|
|
89
|
-
if (typeof info.pid !== "number" || info.pid === process.pid)
|
|
90
|
-
stale = true;
|
|
91
|
-
else
|
|
92
|
-
try {
|
|
93
|
-
process.kill(info.pid, 0);
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
const code = error.code;
|
|
97
|
-
stale = code === "ESRCH" || (code === "EPERM" && age > 10000);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
174
|
+
age = Date.now() - statSync(lockPath).mtimeMs;
|
|
100
175
|
}
|
|
101
176
|
catch {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
177
|
+
age = 0;
|
|
178
|
+
}
|
|
179
|
+
let owner = null;
|
|
180
|
+
try {
|
|
181
|
+
owner = JSON.parse(readFileSync(ownerPath, "utf8"));
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// v2.14: a crash between mkdir(lock) and write(owner.json) leaves
|
|
185
|
+
// a lock directory without an owner. That window is microseconds,
|
|
186
|
+
// so after a short grace the lock is provably stale — previously
|
|
187
|
+
// this state caused a permanent lock until manual deletion.
|
|
188
|
+
stale = age > 500;
|
|
189
|
+
}
|
|
190
|
+
if (!stale && owner && age > 5000 && typeof owner.pid === "number") {
|
|
106
191
|
try {
|
|
107
|
-
|
|
192
|
+
process.kill(owner.pid, 0);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
// On Windows, EPERM may be returned for dead cross-user
|
|
196
|
+
// processes. Treat as stale when the lock is old regardless.
|
|
197
|
+
const code = error.code;
|
|
198
|
+
stale = code === "ESRCH"
|
|
199
|
+
|| (code === "EPERM" && age > 10_000); // Windows safety: EPERM + old lock → stale
|
|
108
200
|
}
|
|
109
|
-
catch { /* lock vanished; next spin retries */ }
|
|
110
|
-
if (age > 5000)
|
|
111
|
-
stale = true;
|
|
112
201
|
}
|
|
113
202
|
if (stale) {
|
|
114
203
|
try {
|
|
115
|
-
rmSync(
|
|
204
|
+
rmSync(lockPath, { recursive: true });
|
|
116
205
|
}
|
|
117
206
|
catch { /* race */ }
|
|
118
207
|
continue;
|
|
119
208
|
}
|
|
120
209
|
if (Date.now() >= deadline)
|
|
121
210
|
throw new Error("LoopStore lock timeout (1000ms)");
|
|
122
|
-
// Exponential backoff (2→4→8→16→32ms) instead of a hot spin; the
|
|
123
|
-
// deadline above still bounds the total wait.
|
|
124
|
-
sleepSync(Math.min(2 << Math.min(spinCount, 4), 32));
|
|
125
|
-
spinCount++;
|
|
126
211
|
}
|
|
127
212
|
}
|
|
128
213
|
this.lockDepth = 1;
|
|
@@ -132,205 +217,263 @@ export class FileLoopStore {
|
|
|
132
217
|
finally {
|
|
133
218
|
this.lockDepth = 0;
|
|
134
219
|
try {
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
rmSync(
|
|
220
|
+
const owner = JSON.parse(readFileSync(ownerPath, "utf8"));
|
|
221
|
+
if (owner.token === token)
|
|
222
|
+
rmSync(lockPath, { recursive: true });
|
|
138
223
|
}
|
|
139
|
-
catch { /* never
|
|
224
|
+
catch { /* never delete an unowned lock */ }
|
|
140
225
|
}
|
|
141
226
|
}
|
|
142
227
|
listLoopIds() {
|
|
143
|
-
const
|
|
144
|
-
if (!existsSync(
|
|
228
|
+
const loops = join(this.root, "loops");
|
|
229
|
+
if (!existsSync(loops))
|
|
145
230
|
return [];
|
|
146
|
-
const
|
|
147
|
-
for (const name of readdirSync(
|
|
148
|
-
const
|
|
149
|
-
const metadata = this.readJson(
|
|
150
|
-
if (metadata ===
|
|
231
|
+
const result = [];
|
|
232
|
+
for (const name of readdirSync(loops)) {
|
|
233
|
+
const dir = join(loops, name);
|
|
234
|
+
const metadata = this.readJson(join(dir, "metadata.json"));
|
|
235
|
+
if (isRecord(metadata) && typeof metadata.loopId === "string") {
|
|
236
|
+
result.push(metadata.loopId);
|
|
151
237
|
continue;
|
|
152
|
-
if (!isRecord(metadata) || metadata.format !== LOOP_STORE_FORMAT ||
|
|
153
|
-
typeof metadata.loopId !== "string" || !metadata.loopId) {
|
|
154
|
-
throw new StorageCorruptionError("invalid_format", metadataPath);
|
|
155
238
|
}
|
|
156
|
-
|
|
239
|
+
// v2.14: orphaned loop dirs (sessions written before metadata
|
|
240
|
+
// stamping) recover their loopId from the session document itself.
|
|
241
|
+
const session = this.readJson(join(dir, "session.json"));
|
|
242
|
+
if (isRecord(session) && typeof session.loopId === "string") {
|
|
243
|
+
result.push(session.loopId);
|
|
244
|
+
}
|
|
157
245
|
}
|
|
158
|
-
return
|
|
246
|
+
return result.sort();
|
|
159
247
|
}
|
|
160
248
|
readSession(loopId) {
|
|
161
249
|
validateLoopId(loopId);
|
|
162
|
-
const
|
|
163
|
-
|
|
250
|
+
const value = this.readJson(join(this.loopDir(loopId), "session.json"));
|
|
251
|
+
// readJson returns null only for a missing file; an existing document
|
|
252
|
+
// with an unexpected shape is structural corruption, never silently
|
|
253
|
+
// treated as "missing" (which would let a later write overwrite it).
|
|
164
254
|
if (value === null)
|
|
165
255
|
return null;
|
|
166
|
-
if (!isRecord(value) || value.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (!Number.isInteger(value.entry.event_sequence) || value.entry.event_sequence < 1) {
|
|
170
|
-
throw new StorageCorruptionError("sequence_invalid", path, loopId, value.entry.event_sequence);
|
|
256
|
+
if (!isRecord(value) || value.schemaVersion !== LOOP_STORE_SCHEMA_VERSION ||
|
|
257
|
+
value.loopId !== loopId || !isRecord(value.entry)) {
|
|
258
|
+
throw new StorageCorruptionError("invalid_format", `Loop ${loopId}: session.json has an unexpected document shape`);
|
|
171
259
|
}
|
|
172
260
|
return value;
|
|
173
261
|
}
|
|
174
262
|
writeSession(loopId, document) {
|
|
175
263
|
validateLoopId(loopId);
|
|
176
|
-
const
|
|
177
|
-
|
|
264
|
+
const dir = this.loopDir(loopId);
|
|
265
|
+
// v2.14: stamp metadata alongside the session document — a session with
|
|
266
|
+
// no committed rounds yet (created, then crashed before round 1) must
|
|
267
|
+
// still be discoverable via listLoopIds / auto-resume.
|
|
268
|
+
this.atomicWrite(join(dir, "metadata.json"), {
|
|
269
|
+
schemaVersion: LOOP_STORE_SCHEMA_VERSION,
|
|
270
|
+
loopId,
|
|
271
|
+
});
|
|
272
|
+
this.atomicWrite(join(dir, "session.json"), document);
|
|
178
273
|
}
|
|
179
274
|
readRound(loopId, round) {
|
|
180
275
|
validateLoopId(loopId);
|
|
181
276
|
if (!Number.isInteger(round) || round < 1)
|
|
182
277
|
return null;
|
|
183
|
-
const
|
|
184
|
-
|
|
278
|
+
const value = this.readJson(join(this.loopDir(loopId), "rounds", `${round}.json`));
|
|
279
|
+
// readJson returns null only for a missing file; an existing document
|
|
280
|
+
// with an unexpected shape is structural corruption — surfacing it
|
|
281
|
+
// (rather than returning null) prevents silent overwrite by writeEntry.
|
|
185
282
|
if (value === null)
|
|
186
283
|
return null;
|
|
187
|
-
if (!isRecord(value) || value.
|
|
188
|
-
|
|
284
|
+
if (!isRecord(value) || value.schemaVersion !== LOOP_STORE_SCHEMA_VERSION ||
|
|
285
|
+
value.loopId !== loopId || value.round !== round) {
|
|
286
|
+
throw new StorageCorruptionError("invalid_format", `Loop ${loopId}: round ${round} document has an unexpected shape`);
|
|
189
287
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
288
|
+
// v2.12: a stamped document whose stamp disagrees with its filename is
|
|
289
|
+
// structural corruption, never silently repaired.
|
|
290
|
+
if (typeof value.sequence === "number" && value.sequence !== round) {
|
|
291
|
+
throw new StorageCorruptionError("sequence_invalid", `Loop ${loopId}: round ${round} document carries sequence ${value.sequence}`);
|
|
193
292
|
}
|
|
194
|
-
const
|
|
195
|
-
.filter(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
293
|
+
const events = Array.isArray(value.events)
|
|
294
|
+
? value.events.filter(isRecord)
|
|
295
|
+
: [];
|
|
296
|
+
const transaction = parseRoundTransactionSnapshot(value.transaction);
|
|
297
|
+
return {
|
|
298
|
+
schemaVersion: LOOP_STORE_SCHEMA_VERSION,
|
|
299
|
+
loopId,
|
|
300
|
+
round,
|
|
301
|
+
sequence: typeof value.sequence === "number" ? value.sequence : undefined,
|
|
302
|
+
updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : "",
|
|
303
|
+
lineage: isRecord(value.lineage) ? value.lineage : undefined,
|
|
304
|
+
feedback: isRecord(value.feedback) ? value.feedback : undefined,
|
|
305
|
+
transaction: transaction ?? undefined,
|
|
306
|
+
promptArtifact: transaction?.promptArtifact,
|
|
307
|
+
events,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
listRoundSequences(loopId) {
|
|
311
|
+
validateLoopId(loopId);
|
|
312
|
+
const roundsDir = join(this.loopDir(loopId), "rounds");
|
|
313
|
+
if (!existsSync(roundsDir))
|
|
314
|
+
return [];
|
|
315
|
+
const result = [];
|
|
316
|
+
for (const file of readdirSync(roundsDir).filter((name) => /^\d+\.json$/.test(name))) {
|
|
317
|
+
const round = Number(file.slice(0, -5));
|
|
318
|
+
const value = this.readJson(join(roundsDir, file));
|
|
319
|
+
if (!isRecord(value) || value.loopId !== loopId || value.round !== round)
|
|
320
|
+
continue;
|
|
321
|
+
result.push({
|
|
322
|
+
round,
|
|
323
|
+
sequence: typeof value.sequence === "number" ? value.sequence : undefined,
|
|
324
|
+
});
|
|
204
325
|
}
|
|
205
|
-
return
|
|
326
|
+
return result.sort((a, b) => a.round - b.round);
|
|
206
327
|
}
|
|
207
|
-
listEntries(loopId) {
|
|
328
|
+
listEntries(loopId, opts) {
|
|
329
|
+
const ids = loopId ? [loopId] : this.listLoopIds();
|
|
208
330
|
const result = [];
|
|
209
|
-
for (const id of
|
|
331
|
+
for (const id of ids) {
|
|
210
332
|
const session = this.readSession(id);
|
|
211
333
|
if (session)
|
|
212
334
|
result.push(session.entry);
|
|
213
|
-
const
|
|
214
|
-
if (!existsSync(
|
|
335
|
+
const roundsDir = join(this.loopDir(id), "rounds");
|
|
336
|
+
if (!existsSync(roundsDir))
|
|
215
337
|
continue;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
338
|
+
// Numeric sort — readdirSync order is filesystem-dependent and must
|
|
339
|
+
// not leak into the flat entry view (rounds 1, 10, 2 … would be).
|
|
340
|
+
for (const file of readdirSync(roundsDir)
|
|
341
|
+
.filter((name) => /^\d+\.json$/.test(name))
|
|
342
|
+
.sort((a, b) => Number(a.slice(0, -5)) - Number(b.slice(0, -5)))) {
|
|
343
|
+
const round = Number(file.slice(0, -5));
|
|
344
|
+
// v3.0.1: incremental reads skip older round documents entirely —
|
|
345
|
+
// this is what bounds per-compile I/O in long loops.
|
|
346
|
+
if (opts?.sinceRound !== undefined && round < opts.sinceRound)
|
|
219
347
|
continue;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
348
|
+
const doc = this.readRound(id, round);
|
|
349
|
+
if (!doc)
|
|
350
|
+
continue;
|
|
351
|
+
if (doc.lineage) {
|
|
352
|
+
result.push({
|
|
353
|
+
...doc.lineage,
|
|
354
|
+
full_prompt: doc.promptArtifact?.renderedPrompt ?? doc.lineage.full_prompt,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (doc.feedback)
|
|
358
|
+
result.push(doc.feedback);
|
|
359
|
+
result.push(...doc.events);
|
|
225
360
|
}
|
|
226
361
|
}
|
|
227
362
|
return result;
|
|
228
363
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
364
|
+
appendEntry(entry) {
|
|
365
|
+
this.withLock(() => this.writeEntry(entry));
|
|
366
|
+
}
|
|
367
|
+
appendEntries(entries) {
|
|
368
|
+
return this.withLock(() => {
|
|
369
|
+
for (const entry of entries)
|
|
370
|
+
this.writeEntry(entry);
|
|
371
|
+
return entries.length;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
migrateLegacyVault(path = ".promptcraft/prompt_vault.json") {
|
|
375
|
+
const source = resolve(path);
|
|
376
|
+
const marker = join(this.root, "migrations", "promptcraft-v1.json");
|
|
377
|
+
if (existsSync(marker)) {
|
|
378
|
+
return { source, imported: 0, skipped: 0, alreadyMigrated: true };
|
|
379
|
+
}
|
|
380
|
+
let imported = 0;
|
|
381
|
+
let skipped = 0;
|
|
382
|
+
const raw = this.readJson(source);
|
|
383
|
+
const entries = isRecord(raw) && Array.isArray(raw.entries)
|
|
384
|
+
? raw.entries.filter(isRecord)
|
|
385
|
+
: [];
|
|
386
|
+
this.withLock(() => {
|
|
387
|
+
const existing = new Set(this.listEntries().map((entry) => String(entry.task_id ?? "")));
|
|
388
|
+
for (const entry of entries) {
|
|
389
|
+
if (!loopIdFromEntry(entry) || existing.has(String(entry.task_id ?? ""))) {
|
|
390
|
+
skipped++;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
this.writeEntry(entry, { allowGap: true });
|
|
394
|
+
imported++;
|
|
252
395
|
}
|
|
253
|
-
|
|
396
|
+
this.atomicWrite(marker, {
|
|
397
|
+
schemaVersion: LOOP_STORE_SCHEMA_VERSION,
|
|
398
|
+
source,
|
|
399
|
+
imported,
|
|
400
|
+
skipped,
|
|
401
|
+
migratedAt: new Date().toISOString(),
|
|
402
|
+
});
|
|
254
403
|
});
|
|
255
|
-
|
|
256
|
-
return entries;
|
|
404
|
+
return { source, imported, skipped, alreadyMigrated: false };
|
|
257
405
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
this.writeEntry(entry); return entries.length; }); }
|
|
261
|
-
replaceEntries(entries) { this.withLock(() => { for (const entry of entries)
|
|
262
|
-
this.writeEntry(entry); }); }
|
|
263
|
-
writeEntry(input) {
|
|
264
|
-
const loopId = loopIdFromEntry(input);
|
|
406
|
+
writeEntry(entry, opts) {
|
|
407
|
+
const loopId = loopIdFromEntry(entry);
|
|
265
408
|
if (!loopId)
|
|
266
409
|
throw new Error("LoopStore only accepts loop-scoped entries");
|
|
267
410
|
validateLoopId(loopId);
|
|
268
|
-
if (input.loop_id !== undefined && input.loop_id !== loopId) {
|
|
269
|
-
throw new StorageCorruptionError("sequence_cross_loop", this.loopDir(loopId), loopId);
|
|
270
|
-
}
|
|
271
|
-
if (input.format !== undefined && input.format !== LOOP_EVENT_FORMAT)
|
|
272
|
-
throw new Error("LoopStore rejected an event with an unknown format");
|
|
273
|
-
const entry = this.withEventSequence(loopId, { format: LOOP_EVENT_FORMAT, ...input });
|
|
274
411
|
const now = new Date().toISOString();
|
|
275
412
|
const dir = this.loopDir(loopId);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (!existsSync(metadataPath))
|
|
281
|
-
this.atomicWrite(metadataPath, { format: LOOP_STORE_FORMAT, loopId });
|
|
413
|
+
this.atomicWrite(join(dir, "metadata.json"), {
|
|
414
|
+
schemaVersion: LOOP_STORE_SCHEMA_VERSION,
|
|
415
|
+
loopId,
|
|
416
|
+
});
|
|
282
417
|
if (entry.task_type === "session_state" || entry.task_id === `loop:${loopId}:session`) {
|
|
283
|
-
this.atomicWrite(join(dir, "session.json"), {
|
|
418
|
+
this.atomicWrite(join(dir, "session.json"), {
|
|
419
|
+
schemaVersion: LOOP_STORE_SCHEMA_VERSION,
|
|
420
|
+
loopId,
|
|
421
|
+
updatedAt: now,
|
|
422
|
+
entry,
|
|
423
|
+
});
|
|
284
424
|
return;
|
|
285
425
|
}
|
|
286
426
|
const round = roundFromEntry(entry);
|
|
287
427
|
if (!round)
|
|
288
428
|
throw new Error(`Loop entry has no round: ${entry.task_id ?? "unknown"}`);
|
|
289
|
-
|
|
429
|
+
// v2.12: monotonic write-time check — a stamped loop may not skip its
|
|
430
|
+
// predecessor. Legacy imports (allowGap) bypass this; the load-time
|
|
431
|
+
// scan in checkRoundSequence remains the backstop.
|
|
432
|
+
if (!opts?.allowGap && round > 1) {
|
|
433
|
+
const previous = this.readRound(loopId, round - 1);
|
|
434
|
+
if (!previous && this.listRoundSequences(loopId).some((doc) => doc.sequence !== undefined)) {
|
|
435
|
+
throw new StorageCorruptionError("sequence_gap", `Loop ${loopId}: cannot write round ${round}; round ${round - 1} is missing`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const current = this.readRound(loopId, round) ?? {
|
|
439
|
+
schemaVersion: LOOP_STORE_SCHEMA_VERSION,
|
|
440
|
+
loopId,
|
|
441
|
+
round,
|
|
442
|
+
sequence: round,
|
|
443
|
+
updatedAt: now,
|
|
444
|
+
events: [],
|
|
445
|
+
};
|
|
446
|
+
if (current.sequence === undefined)
|
|
447
|
+
current.sequence = round;
|
|
290
448
|
const taskId = String(entry.task_id ?? "");
|
|
291
|
-
if (taskId.endsWith(":
|
|
292
|
-
current.
|
|
293
|
-
else if (entry.task_type === "loop_lineage" || taskId === `loop:${loopId}:r${round}`)
|
|
449
|
+
if (taskId.endsWith(":feedback"))
|
|
450
|
+
current.feedback = entry;
|
|
451
|
+
else if (entry.task_type === "loop_lineage" || taskId === `loop:${loopId}:r${round}`) {
|
|
294
452
|
current.lineage = entry;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
current.events = [
|
|
456
|
+
...current.events.filter((event) => event.task_id !== entry.task_id),
|
|
457
|
+
entry,
|
|
458
|
+
];
|
|
459
|
+
}
|
|
460
|
+
const transactionRaw = current.feedback?.loop_lineage?.round_transaction ??
|
|
461
|
+
entry.loop_lineage?.round_transaction;
|
|
462
|
+
const transaction = parseRoundTransactionSnapshot(isRecord(transactionRaw) ? transactionRaw.snapshot : undefined);
|
|
463
|
+
if (transaction) {
|
|
464
|
+
current.transaction = transaction;
|
|
465
|
+
current.promptArtifact = promptFromSnapshot(transaction);
|
|
302
466
|
}
|
|
303
467
|
current.updatedAt = now;
|
|
304
468
|
this.atomicWrite(join(dir, "rounds", `${round}.json`), current);
|
|
305
469
|
}
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
this.
|
|
309
|
-
const prior = existing.find((candidate) => candidate.task_id === entry.task_id);
|
|
310
|
-
if (prior && Number.isInteger(prior.event_sequence)) {
|
|
311
|
-
if (entry.event_sequence === undefined)
|
|
312
|
-
return { ...entry, event_sequence: prior.event_sequence };
|
|
313
|
-
if (entry.event_sequence === prior.event_sequence)
|
|
314
|
-
return entry;
|
|
315
|
-
throw new StorageCorruptionError("sequence_invalid", this.loopDir(loopId), loopId, entry.event_sequence, "An existing event cannot change its event_sequence");
|
|
316
|
-
}
|
|
317
|
-
const used = new Set(existing.map((candidate) => candidate.event_sequence).filter((value) => typeof value === "number" && Number.isInteger(value) && value > 0));
|
|
318
|
-
const next = used.size ? Math.max(...used) + 1 : 1;
|
|
319
|
-
if (entry.event_sequence !== undefined) {
|
|
320
|
-
if (!Number.isInteger(entry.event_sequence) || entry.event_sequence < 1) {
|
|
321
|
-
throw new StorageCorruptionError("sequence_invalid", this.loopDir(loopId), loopId, entry.event_sequence);
|
|
322
|
-
}
|
|
323
|
-
if (used.has(entry.event_sequence)) {
|
|
324
|
-
throw new StorageCorruptionError("sequence_duplicate", this.loopDir(loopId), loopId, entry.event_sequence);
|
|
325
|
-
}
|
|
326
|
-
if (entry.event_sequence !== next) {
|
|
327
|
-
throw new StorageCorruptionError("sequence_gap", this.loopDir(loopId), loopId, entry.event_sequence);
|
|
328
|
-
}
|
|
329
|
-
return entry;
|
|
330
|
-
}
|
|
331
|
-
return { ...entry, event_sequence: next };
|
|
470
|
+
loopDir(loopId) {
|
|
471
|
+
const hash = createHash("sha256").update(loopId).digest("hex");
|
|
472
|
+
return join(this.root, "loops", hash);
|
|
332
473
|
}
|
|
333
|
-
|
|
474
|
+
/** Read a JSON document, distinguishing missing from corrupt.
|
|
475
|
+
* ENOENT → null (missing); parse failure → StorageCorruptionError
|
|
476
|
+
* (corrupted) — the runtime never silently repairs storage. */
|
|
334
477
|
readJson(path) {
|
|
335
478
|
try {
|
|
336
479
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -338,26 +481,30 @@ export class FileLoopStore {
|
|
|
338
481
|
catch (error) {
|
|
339
482
|
if (error.code === "ENOENT")
|
|
340
483
|
return null;
|
|
341
|
-
throw new StorageCorruptionError("invalid_json",
|
|
484
|
+
throw new StorageCorruptionError("invalid_json", `Corrupt JSON at ${path}: ${error.message}`);
|
|
342
485
|
}
|
|
343
486
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
487
|
+
atomicWrite(path, value) {
|
|
488
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
489
|
+
const writeOnce = () => {
|
|
490
|
+
const temporary = `${path}.tmp.${randomUUID().slice(0, 8)}`;
|
|
491
|
+
writeFileSync(temporary, JSON.stringify(value, null, 2), "utf8");
|
|
492
|
+
renameSync(temporary, path);
|
|
493
|
+
};
|
|
494
|
+
// v2.12: transient I/O failures (Windows rename EPERM/EACCES) are
|
|
495
|
+
// retried once after a short backoff; a second failure bubbles.
|
|
496
|
+
try {
|
|
497
|
+
writeOnce();
|
|
498
|
+
}
|
|
499
|
+
catch (first) {
|
|
500
|
+
sleepSync(500);
|
|
501
|
+
try {
|
|
502
|
+
writeOnce();
|
|
354
503
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
throw new StorageCorruptionError("sequence_gap", this.loopDir(loopId), loopId, sequence);
|
|
504
|
+
catch (second) {
|
|
505
|
+
throw second;
|
|
358
506
|
}
|
|
359
507
|
}
|
|
360
508
|
}
|
|
361
|
-
atomicWrite(path, value) { mkdirSync(dirname(path), { recursive: true }); const temporary = `${path}.tmp.${randomUUID().slice(0, 8)}`; writeFileSync(temporary, JSON.stringify(value, null, 2), "utf8"); renameSync(temporary, path); }
|
|
362
509
|
}
|
|
363
510
|
//# sourceMappingURL=loop-store.js.map
|