@felan-ai/ext-memory 0.8.0 → 0.9.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 +41 -30
- package/dist/data.d.ts +3 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +2 -0
- package/dist/data.js.map +1 -0
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +14 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/extension.d.ts +5 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +37 -0
- package/dist/extension.js.map +1 -0
- package/dist/hydration.d.ts +0 -3
- package/dist/hydration.d.ts.map +1 -1
- package/dist/hydration.js +0 -78
- package/dist/hydration.js.map +1 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -4
- package/dist/index.js.map +1 -1
- package/dist/local/context.d.ts +10 -0
- package/dist/local/context.d.ts.map +1 -0
- package/dist/local/context.js +10 -0
- package/dist/local/context.js.map +1 -0
- package/dist/local/control.d.ts +13 -0
- package/dist/local/control.d.ts.map +1 -0
- package/dist/local/control.js +183 -0
- package/dist/local/control.js.map +1 -0
- package/dist/local/coordinator.d.ts +93 -0
- package/dist/local/coordinator.d.ts.map +1 -0
- package/dist/local/coordinator.js +859 -0
- package/dist/local/coordinator.js.map +1 -0
- package/dist/local/dreamer.d.ts +53 -0
- package/dist/local/dreamer.d.ts.map +1 -0
- package/dist/local/dreamer.js +490 -0
- package/dist/local/dreamer.js.map +1 -0
- package/dist/local/history.d.ts +33 -0
- package/dist/local/history.d.ts.map +1 -0
- package/dist/local/history.js +300 -0
- package/dist/local/history.js.map +1 -0
- package/dist/local/hydration.d.ts +6 -0
- package/dist/local/hydration.d.ts.map +1 -0
- package/dist/local/hydration.js +84 -0
- package/dist/local/hydration.js.map +1 -0
- package/dist/local/lease.d.ts +14 -0
- package/dist/local/lease.d.ts.map +1 -0
- package/dist/local/lease.js +184 -0
- package/dist/local/lease.js.map +1 -0
- package/dist/local/lock.d.ts +28 -0
- package/dist/local/lock.d.ts.map +1 -0
- package/dist/local/lock.js +431 -0
- package/dist/local/lock.js.map +1 -0
- package/dist/local/project.d.ts +8 -0
- package/dist/local/project.d.ts.map +1 -0
- package/dist/local/project.js +33 -0
- package/dist/local/project.js.map +1 -0
- package/dist/local/run-retention.d.ts +13 -0
- package/dist/local/run-retention.d.ts.map +1 -0
- package/dist/local/run-retention.js +272 -0
- package/dist/local/run-retention.js.map +1 -0
- package/dist/local/run.d.ts +84 -0
- package/dist/local/run.d.ts.map +1 -0
- package/dist/local/run.js +361 -0
- package/dist/local/run.js.map +1 -0
- package/dist/local/store.d.ts +117 -0
- package/dist/local/store.d.ts.map +1 -0
- package/dist/local/store.js +735 -0
- package/dist/local/store.js.map +1 -0
- package/dist/local/ui.d.ts +9 -0
- package/dist/local/ui.d.ts.map +1 -0
- package/dist/local/ui.js +166 -0
- package/dist/local/ui.js.map +1 -0
- package/dist/session-history.d.ts +18 -0
- package/dist/session-history.d.ts.map +1 -0
- package/dist/session-history.js +91 -0
- package/dist/session-history.js.map +1 -0
- package/package.json +9 -5
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
import { removeIfPresent } from './context.js';
|
|
2
|
+
import { withLocalFileLock } from './lock.js';
|
|
3
|
+
import { acquireLocalMemoryLease } from './lease.js';
|
|
4
|
+
import { localMemoryProjectDirectory } from './project.js';
|
|
5
|
+
import { hydrateMemoryDirectory, readMemoryDirectory } from './hydration.js';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { createEmptyMemoryArtifact, createMemoryProjectionSnapshot, createMemorySnapshot, memoryArtifactFingerprint } from '../index.js';
|
|
9
|
+
export const MEMORY_ATTEMPT_TIMEOUT_MS = 60 * 60 * 1_000;
|
|
10
|
+
export const MEMORY_RETRY_DELAYS_MS = [60_000, 300_000];
|
|
11
|
+
export const STATE_VERSION = 1;
|
|
12
|
+
export class LocalMemoryControlError extends Error {
|
|
13
|
+
constructor(path) {
|
|
14
|
+
super(`Local memory control is malformed or unsupported; processing is blocked: ${path}`);
|
|
15
|
+
this.name = 'LocalMemoryControlError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const MAX_CONTROL_BYTES = 64 * 1_024;
|
|
19
|
+
export class LocalMemoryStore {
|
|
20
|
+
environment;
|
|
21
|
+
agentDir;
|
|
22
|
+
project;
|
|
23
|
+
projectDirectory;
|
|
24
|
+
currentDirectory;
|
|
25
|
+
stagingDirectory;
|
|
26
|
+
statePath;
|
|
27
|
+
controlPath;
|
|
28
|
+
projectionName = '.memory';
|
|
29
|
+
#memoryPath;
|
|
30
|
+
#retryDelaysMs;
|
|
31
|
+
constructor(environment, agentDir, project, options = {}) {
|
|
32
|
+
this.environment = environment;
|
|
33
|
+
this.agentDir = agentDir;
|
|
34
|
+
this.project = project;
|
|
35
|
+
this.projectDirectory = localMemoryProjectDirectory(agentDir, project);
|
|
36
|
+
this.currentDirectory = join(this.projectDirectory, 'current');
|
|
37
|
+
this.stagingDirectory = join(this.projectDirectory, 'staging');
|
|
38
|
+
this.statePath = join(this.projectDirectory, 'state.json');
|
|
39
|
+
this.controlPath = join(this.projectDirectory, 'control.json');
|
|
40
|
+
this.#memoryPath = options.memoryPath ?? '.memory';
|
|
41
|
+
this.#retryDelaysMs = options.retryDelaysMs ?? MEMORY_RETRY_DELAYS_MS;
|
|
42
|
+
if (this.#retryDelaysMs.length !== 2 || this.#retryDelaysMs.some((delay) => !Number.isFinite(delay) || delay < 0)) {
|
|
43
|
+
throw new Error('Memory retry delays must be two non-negative finite durations');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async initialize(now = Date.now()) {
|
|
47
|
+
await this.environment.runtime.files.ensurePrivateDirectory(this.projectDirectory, { recursive: true });
|
|
48
|
+
await this.environment.runtime.files.ensurePrivateDirectory(this.stagingDirectory, { recursive: true });
|
|
49
|
+
const recoveryLease = await acquireLocalMemoryLease(this.environment, this.projectDirectory);
|
|
50
|
+
try {
|
|
51
|
+
await withStateLock(this.environment, this, async () => {
|
|
52
|
+
if (!(await readControl(this.environment, this.controlPath)))
|
|
53
|
+
await writeControl(this.environment, this.controlPath, emptyControl(now));
|
|
54
|
+
if (recoveryLease)
|
|
55
|
+
await recoverTransaction(this.environment, this, recoveryLease, now);
|
|
56
|
+
const current = await safeLstat(this.environment, this.currentDirectory);
|
|
57
|
+
if (current?.kind === 'symlink')
|
|
58
|
+
throw new Error('Canonical memory current directory cannot be a symlink');
|
|
59
|
+
if (!current) {
|
|
60
|
+
await verifyInitializationLease(recoveryLease);
|
|
61
|
+
await hydrateMemoryDirectory(this.environment, createEmptyMemoryArtifact(this.#memoryPath), this.currentDirectory);
|
|
62
|
+
}
|
|
63
|
+
const artifact = await readMemoryDirectory(this.environment, this.currentDirectory, { memoryPath: this.#memoryPath, mode: 'read' });
|
|
64
|
+
const fingerprint = memoryArtifactFingerprint(artifact);
|
|
65
|
+
const existing = await readState(this.environment, this.statePath);
|
|
66
|
+
if (!existing || existing.memoryFingerprint !== fingerprint) {
|
|
67
|
+
await verifyInitializationLease(recoveryLease);
|
|
68
|
+
await writeState(this.environment, this.statePath, {
|
|
69
|
+
version: STATE_VERSION,
|
|
70
|
+
memoryFingerprint: fingerprint,
|
|
71
|
+
pending: existing?.pending ?? {},
|
|
72
|
+
processed: existing?.processed ?? {},
|
|
73
|
+
updatedAt: new Date(now).toISOString(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await recoveryLease?.release();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async readCurrent() {
|
|
83
|
+
return withStateLock(this.environment, this, async () => {
|
|
84
|
+
const artifact = await readMemoryDirectory(this.environment, this.currentDirectory, { memoryPath: this.#memoryPath, mode: 'read' });
|
|
85
|
+
return createMemorySnapshot(artifact, this.#memoryPath, { mode: 'read' });
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async projectTo(sessionStorageRoot, snapshot) {
|
|
89
|
+
const current = snapshot ?? await this.readCurrent();
|
|
90
|
+
const target = join(sessionStorageRoot, this.projectionName);
|
|
91
|
+
const projection = createMemoryProjectionSnapshot(current, target);
|
|
92
|
+
await hydrateMemoryDirectory(this.environment, projection, target, { replace: true, memoryPath: target, mode: 'read' });
|
|
93
|
+
return projection;
|
|
94
|
+
}
|
|
95
|
+
async recordCheckpoint(checkpoint) {
|
|
96
|
+
return withStateLock(this.environment, this, async () => {
|
|
97
|
+
const state = await this.readState();
|
|
98
|
+
const previousPending = state.pending[checkpoint.sessionId];
|
|
99
|
+
const previousProcessed = state.processed[checkpoint.sessionId];
|
|
100
|
+
if (sameCursor(previousPending?.checkpoint, checkpoint) || sameCursor(previousProcessed?.checkpoint, checkpoint)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const recordedAt = new Date().toISOString();
|
|
104
|
+
const pending = {
|
|
105
|
+
...state.pending,
|
|
106
|
+
[checkpoint.sessionId]: {
|
|
107
|
+
checkpoint,
|
|
108
|
+
recordedAt,
|
|
109
|
+
acceptedUpdates: previousPending === undefined ? 1 : acceptedCheckpointUpdates(previousPending) + 1,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
await writeState(this.environment, this.statePath, { ...state, pending, updatedAt: recordedAt });
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async status() {
|
|
117
|
+
return this.readState();
|
|
118
|
+
}
|
|
119
|
+
async pendingAcceptedUpdates(include = () => true) {
|
|
120
|
+
const state = await this.readState();
|
|
121
|
+
return acceptedCheckpointUpdatesFor(Object.values(state.pending).filter(({ checkpoint }) => include(checkpoint)));
|
|
122
|
+
}
|
|
123
|
+
async readControl() {
|
|
124
|
+
return await readControl(this.environment, this.controlPath) ?? emptyControl(Date.now());
|
|
125
|
+
}
|
|
126
|
+
async beginAttempt(lease, input, now = Date.now()) {
|
|
127
|
+
return withStateLock(this.environment, this, async () => {
|
|
128
|
+
await verifyLease(lease);
|
|
129
|
+
const control = await this.readControl();
|
|
130
|
+
if (control.autoDisabled || control.activeAttempt
|
|
131
|
+
|| (!input.bypassRetryAt && control.nextRetryAt && Date.parse(control.nextRetryAt) > now)
|
|
132
|
+
|| input.checkpoints.length === 0)
|
|
133
|
+
return undefined;
|
|
134
|
+
const attempt = {
|
|
135
|
+
runId: input.runId,
|
|
136
|
+
generation: control.generation,
|
|
137
|
+
leaseToken: lease.token,
|
|
138
|
+
startedAt: new Date(now).toISOString(),
|
|
139
|
+
deadlineAt: input.deadlineAt ?? new Date(now + MEMORY_ATTEMPT_TIMEOUT_MS).toISOString(),
|
|
140
|
+
baseFingerprint: input.baseFingerprint,
|
|
141
|
+
checkpoints: structuredClone(input.checkpoints),
|
|
142
|
+
};
|
|
143
|
+
if (!isMemoryAttempt(attempt))
|
|
144
|
+
throw new Error('Invalid memory attempt');
|
|
145
|
+
const state = await this.readState();
|
|
146
|
+
if (state.memoryFingerprint !== input.baseFingerprint
|
|
147
|
+
|| await fingerprintAt(this.environment, this.currentDirectory, this.memoryPath) !== input.baseFingerprint) {
|
|
148
|
+
throw new Error('Memory changed before the attempt started');
|
|
149
|
+
}
|
|
150
|
+
if (input.checkpoints.some((entry) => !sameStoredCheckpoint(state.pending[entry.checkpoint.sessionId], entry))) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
await verifyLease(lease);
|
|
154
|
+
await writeControl(this.environment, this.controlPath, { ...control, activeAttempt: attempt, updatedAt: attempt.startedAt });
|
|
155
|
+
return attempt;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async finishAttempt(lease, attempt, outcome, now = Date.now()) {
|
|
159
|
+
return withStateLock(this.environment, this, async () => {
|
|
160
|
+
if (outcome.status !== 'cancelled') {
|
|
161
|
+
await verifyLease(lease);
|
|
162
|
+
await recoverTransaction(this.environment, this, lease, now);
|
|
163
|
+
}
|
|
164
|
+
const control = await withPublicationEvidence(this.environment, this, await this.readControl());
|
|
165
|
+
if (!sameAttempt(control.activeAttempt, attempt))
|
|
166
|
+
return control;
|
|
167
|
+
if (outcome.status !== 'cancelled' && control.activeAttempt.leaseToken !== lease.token) {
|
|
168
|
+
throw new Error('Memory attempt belongs to a different writer lease');
|
|
169
|
+
}
|
|
170
|
+
if (outcome.status === 'success' && !control.activeAttempt.published) {
|
|
171
|
+
throw new Error('Memory attempt has not published successfully');
|
|
172
|
+
}
|
|
173
|
+
const next = finishControl(control, outcome, now, this.#retryDelaysMs);
|
|
174
|
+
if (outcome.status !== 'cancelled')
|
|
175
|
+
await verifyLease(lease);
|
|
176
|
+
await writeControl(this.environment, this.controlPath, next);
|
|
177
|
+
return next;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async resetProjectControl(now = Date.now(), isLocallyEligible = () => true) {
|
|
181
|
+
return withStateLock(this.environment, this, async () => {
|
|
182
|
+
const control = await withPublicationEvidence(this.environment, this, await this.readControl());
|
|
183
|
+
const finished = finishControl(control, { status: 'cancelled' }, now, this.#retryDelaysMs);
|
|
184
|
+
const lastDisabledRunId = control.lastDisabledRunId ?? control.autoDisabled?.runId;
|
|
185
|
+
const next = {
|
|
186
|
+
...emptyControl(now),
|
|
187
|
+
generation: control.generation + 1,
|
|
188
|
+
...(finished.lastOutcome ? { lastOutcome: finished.lastOutcome } : {}),
|
|
189
|
+
...(lastDisabledRunId ? { lastDisabledRunId } : {}),
|
|
190
|
+
};
|
|
191
|
+
if (!isLocallyEligible())
|
|
192
|
+
throw new Error('Memory retry was cancelled');
|
|
193
|
+
await writeControl(this.environment, this.controlPath, next);
|
|
194
|
+
if (!isLocallyEligible()) {
|
|
195
|
+
await writeControl(this.environment, this.controlPath, control);
|
|
196
|
+
throw new Error('Memory retry was cancelled');
|
|
197
|
+
}
|
|
198
|
+
return next;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async cancelActiveAttempt(now = Date.now()) {
|
|
202
|
+
return withStateLock(this.environment, this, async () => {
|
|
203
|
+
const control = await withPublicationEvidence(this.environment, this, await this.readControl());
|
|
204
|
+
if (!control.activeAttempt)
|
|
205
|
+
return control;
|
|
206
|
+
const next = finishControl(control, { status: 'cancelled' }, now, this.#retryDelaysMs);
|
|
207
|
+
await writeControl(this.environment, this.controlPath, next);
|
|
208
|
+
return next;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async reconcileAbandonedAttempt(lease, now = Date.now()) {
|
|
212
|
+
return withStateLock(this.environment, this, async () => {
|
|
213
|
+
await verifyLease(lease);
|
|
214
|
+
await recoverTransaction(this.environment, this, lease, now);
|
|
215
|
+
const control = await withPublicationEvidence(this.environment, this, await this.readControl());
|
|
216
|
+
const active = control.activeAttempt;
|
|
217
|
+
if (!active || (active.leaseToken === lease.token && !active.published && Date.parse(active.deadlineAt) > now)) {
|
|
218
|
+
return control;
|
|
219
|
+
}
|
|
220
|
+
const next = finishControl(control, { status: 'cancelled', reason: 'Memory writer was interrupted' }, now, this.#retryDelaysMs);
|
|
221
|
+
await verifyLease(lease);
|
|
222
|
+
await writeControl(this.environment, this.controlPath, next);
|
|
223
|
+
return next;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async processingSnapshot(maxSessions = 8, include = () => true) {
|
|
227
|
+
return withStateLock(this.environment, this, async () => {
|
|
228
|
+
const state = await this.readState();
|
|
229
|
+
const artifact = await readMemoryDirectory(this.environment, this.currentDirectory, { memoryPath: this.#memoryPath, mode: 'read' });
|
|
230
|
+
const fingerprint = memoryArtifactFingerprint(artifact);
|
|
231
|
+
const checkpoints = Object.values(state.pending)
|
|
232
|
+
.sort((left, right) => left.recordedAt.localeCompare(right.recordedAt))
|
|
233
|
+
.filter(({ checkpoint }) => include(checkpoint))
|
|
234
|
+
.slice(0, maxSessions);
|
|
235
|
+
return { artifact, fingerprint, checkpoints, state };
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
async createStagingDirectory() {
|
|
239
|
+
await this.environment.runtime.files.ensurePrivateDirectory(this.stagingDirectory, { recursive: true });
|
|
240
|
+
const path = join(this.stagingDirectory, `run-${randomUUID()}`);
|
|
241
|
+
await this.environment.runtime.files.ensurePrivateDirectory(path, { recursive: true });
|
|
242
|
+
return path;
|
|
243
|
+
}
|
|
244
|
+
async commit(lease, baseFingerprint, artifact, processed, attempt) {
|
|
245
|
+
if (!(await lease.verify()))
|
|
246
|
+
throw new Error('Memory writer lease was lost before commit');
|
|
247
|
+
const fingerprint = memoryArtifactFingerprint(artifact);
|
|
248
|
+
const staging = await this.createStagingDirectory();
|
|
249
|
+
const stagedMemory = join(staging, 'memory');
|
|
250
|
+
const previousMemory = join(staging, 'previous');
|
|
251
|
+
const journalPath = join(staging, 'commit.json');
|
|
252
|
+
await hydrateMemoryDirectory(this.environment, artifact, stagedMemory, { memoryPath: this.#memoryPath });
|
|
253
|
+
const journal = {
|
|
254
|
+
version: 2,
|
|
255
|
+
phase: 'prepared',
|
|
256
|
+
baseFingerprint,
|
|
257
|
+
fingerprint,
|
|
258
|
+
checkpoints: structuredClone(processed),
|
|
259
|
+
...(attempt ? { attempt: { ...attempt, leaseToken: lease.token } } : {}),
|
|
260
|
+
};
|
|
261
|
+
await withStateLock(this.environment, this, async () => {
|
|
262
|
+
const control = await this.readControl();
|
|
263
|
+
assertPublicationAllowed(control, attempt, lease.token);
|
|
264
|
+
if (attempt && (control.activeAttempt.baseFingerprint !== baseFingerprint
|
|
265
|
+
|| !sameCheckpoints(control.activeAttempt.checkpoints, processed))) {
|
|
266
|
+
throw new Error('Memory publication does not match the active attempt input');
|
|
267
|
+
}
|
|
268
|
+
const current = await readMemoryDirectory(this.environment, this.currentDirectory, { memoryPath: this.#memoryPath, mode: 'read' });
|
|
269
|
+
if (memoryArtifactFingerprint(current) !== baseFingerprint) {
|
|
270
|
+
throw new Error('Memory changed while the dream was running');
|
|
271
|
+
}
|
|
272
|
+
const state = await this.readState();
|
|
273
|
+
if (state.memoryFingerprint !== baseFingerprint)
|
|
274
|
+
throw new Error('Memory state changed while the dream was running');
|
|
275
|
+
await verifyLease(lease);
|
|
276
|
+
await writeJson(this.environment, journalPath, journal);
|
|
277
|
+
await verifyLease(lease);
|
|
278
|
+
{
|
|
279
|
+
await this.environment.runtime.files.move(this.currentDirectory, previousMemory);
|
|
280
|
+
const at = new Date().toISOString();
|
|
281
|
+
try {
|
|
282
|
+
await this.environment.runtime.files.move(stagedMemory, this.currentDirectory);
|
|
283
|
+
if (!(await lease.verify()))
|
|
284
|
+
throw new Error('Memory writer lease was lost after replacement');
|
|
285
|
+
// The journal proves publication before any pending evidence is acknowledged.
|
|
286
|
+
await writeJson(this.environment, journalPath, { ...journal, phase: 'published', publishedAt: at });
|
|
287
|
+
await verifyLease(lease);
|
|
288
|
+
await writeState(this.environment, this.statePath, acknowledgedState(state, fingerprint, processed, at));
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
await writeJson(this.environment, journalPath, { ...journal, phase: 'rolled_back' });
|
|
292
|
+
await restorePrevious(this.environment, this, previousMemory, baseFingerprint);
|
|
293
|
+
await removeIfPresent(this.environment, staging, { recursive: true });
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
// Once state is committed, a control-write failure must not roll canonical memory back.
|
|
297
|
+
if (attempt) {
|
|
298
|
+
await verifyLease(lease);
|
|
299
|
+
await writeControl(this.environment, this.controlPath, {
|
|
300
|
+
...control,
|
|
301
|
+
activeAttempt: { ...control.activeAttempt, published: { fingerprint, at } },
|
|
302
|
+
updatedAt: at,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
await removeIfPresent(this.environment, staging, { recursive: true });
|
|
308
|
+
return fingerprint;
|
|
309
|
+
}
|
|
310
|
+
async clearStaging() {
|
|
311
|
+
await removeIfPresent(this.environment, this.stagingDirectory, { recursive: true });
|
|
312
|
+
await this.environment.runtime.files.ensurePrivateDirectory(this.stagingDirectory, { recursive: true });
|
|
313
|
+
}
|
|
314
|
+
get memoryPath() {
|
|
315
|
+
return this.#memoryPath;
|
|
316
|
+
}
|
|
317
|
+
async readState() {
|
|
318
|
+
const state = await readState(this.environment, this.statePath);
|
|
319
|
+
if (!state)
|
|
320
|
+
throw new Error('Local memory state is missing or invalid');
|
|
321
|
+
return state;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function recoverTransaction(environment, store, lease, now) {
|
|
325
|
+
await verifyLease(lease);
|
|
326
|
+
await store.readControl();
|
|
327
|
+
const staging = await realDirectory(environment, store.stagingDirectory, 'Memory staging directory is unsafe; recovery is blocked');
|
|
328
|
+
for (const transactionDirectory of await transactionDirectories(environment, store.stagingDirectory)) {
|
|
329
|
+
const path = transactionDirectory.path;
|
|
330
|
+
const journalPath = join(path, 'commit.json');
|
|
331
|
+
const journal = await readBoundedRecord(environment, journalPath, MAX_CONTROL_BYTES, 'Memory commit journal is unsafe; recovery is blocked');
|
|
332
|
+
if (!journal) {
|
|
333
|
+
await verifyLease(lease);
|
|
334
|
+
await verifyDirectory(environment, staging, store.stagingDirectory, 'Memory staging directory changed; recovery is blocked');
|
|
335
|
+
await verifyDirectory(environment, transactionDirectory, path, 'Memory transaction directory changed; recovery is blocked');
|
|
336
|
+
await removeIfPresent(environment, path, { recursive: true });
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (!isLegacyCommitJournal(journal) && !isCommitJournal(journal)) {
|
|
340
|
+
throw new Error('Memory commit journal is malformed or unsupported; recovery is blocked');
|
|
341
|
+
}
|
|
342
|
+
const current = await safeLstat(environment, store.currentDirectory);
|
|
343
|
+
if (current && current.kind !== 'directory') {
|
|
344
|
+
throw new Error('Canonical memory current directory is unsafe; recovery is blocked');
|
|
345
|
+
}
|
|
346
|
+
const memory = join(path, 'memory');
|
|
347
|
+
const previous = join(path, 'previous');
|
|
348
|
+
await optionalRealDirectory(environment, memory, 'Staged memory directory is unsafe; recovery is blocked');
|
|
349
|
+
await optionalRealDirectory(environment, previous, 'Previous memory directory is unsafe; recovery is blocked');
|
|
350
|
+
await verifyLease(lease);
|
|
351
|
+
if (journal.version === 1) {
|
|
352
|
+
// Legacy journals have only session IDs and cannot prove which cursor was processed.
|
|
353
|
+
if (!current && await safeLstat(environment, previous))
|
|
354
|
+
await environment.runtime.files.move(previous, store.currentDirectory);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
const transaction = journal;
|
|
358
|
+
const currentFingerprint = current
|
|
359
|
+
? await fingerprintAt(environment, store.currentDirectory, store.memoryPath)
|
|
360
|
+
: undefined;
|
|
361
|
+
const state = await readState(environment, store.statePath);
|
|
362
|
+
if (!state)
|
|
363
|
+
throw new Error('Memory state is missing during commit recovery');
|
|
364
|
+
if (currentFingerprint === transaction.fingerprint
|
|
365
|
+
&& hasAcknowledgedCommit(state, transaction.fingerprint, transaction.checkpoints)) {
|
|
366
|
+
// Durable state acknowledgement is the only proof that publication completed.
|
|
367
|
+
}
|
|
368
|
+
else if (!current || (currentFingerprint === transaction.fingerprint
|
|
369
|
+
&& transaction.fingerprint !== transaction.baseFingerprint)) {
|
|
370
|
+
await restorePrevious(environment, store, previous, transaction.baseFingerprint);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
await verifyLease(lease);
|
|
374
|
+
await verifyDirectory(environment, staging, store.stagingDirectory, 'Memory staging directory changed; recovery is blocked');
|
|
375
|
+
await verifyDirectory(environment, transactionDirectory, path, 'Memory transaction directory changed; recovery is blocked');
|
|
376
|
+
await removeIfPresent(environment, path, { recursive: true });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function acknowledgedState(state, fingerprint, checkpoints, processedAt) {
|
|
380
|
+
const pending = { ...state.pending };
|
|
381
|
+
const processed = { ...state.processed };
|
|
382
|
+
for (const entry of checkpoints) {
|
|
383
|
+
const sessionId = entry.checkpoint.sessionId;
|
|
384
|
+
const current = pending[sessionId];
|
|
385
|
+
if (current !== undefined) {
|
|
386
|
+
const remaining = acceptedCheckpointUpdates(current) - acceptedCheckpointUpdates(entry);
|
|
387
|
+
if (remaining > 0)
|
|
388
|
+
pending[sessionId] = { ...current, acceptedUpdates: remaining };
|
|
389
|
+
else
|
|
390
|
+
delete pending[sessionId];
|
|
391
|
+
}
|
|
392
|
+
processed[sessionId] = { ...entry, processedAt, memoryFingerprint: fingerprint };
|
|
393
|
+
}
|
|
394
|
+
return { ...state, memoryFingerprint: fingerprint, pending, processed, updatedAt: processedAt };
|
|
395
|
+
}
|
|
396
|
+
async function restorePrevious(environment, store, previous, baseFingerprint) {
|
|
397
|
+
const previousDirectory = await realDirectory(environment, previous, 'Previous canonical memory is unsafe; recovery is blocked');
|
|
398
|
+
if (await fingerprintAt(environment, previous, store.memoryPath) !== baseFingerprint) {
|
|
399
|
+
throw new Error('Previous canonical memory is missing or changed; recovery is blocked');
|
|
400
|
+
}
|
|
401
|
+
const current = await safeLstat(environment, store.currentDirectory);
|
|
402
|
+
if (current && current.kind !== 'directory') {
|
|
403
|
+
throw new Error('Canonical memory current directory is unsafe; recovery is blocked');
|
|
404
|
+
}
|
|
405
|
+
await verifyDirectory(environment, previousDirectory, previous, 'Previous canonical memory changed; recovery is blocked');
|
|
406
|
+
await removeIfPresent(environment, store.currentDirectory, { recursive: true });
|
|
407
|
+
await environment.runtime.files.move(previous, store.currentDirectory);
|
|
408
|
+
}
|
|
409
|
+
async function withStateLock(environment, store, operation) {
|
|
410
|
+
await environment.runtime.files.ensurePrivateDirectory(dirname(store.statePath), { recursive: true });
|
|
411
|
+
return withLocalFileLock(environment, store.statePath, {
|
|
412
|
+
realpath: false,
|
|
413
|
+
retries: { retries: 20, minTimeout: 10, maxTimeout: 100 },
|
|
414
|
+
}, operationWithLock(operation));
|
|
415
|
+
}
|
|
416
|
+
function operationWithLock(operation) {
|
|
417
|
+
return async (lock) => {
|
|
418
|
+
const result = await operation();
|
|
419
|
+
lock.throwIfCompromised();
|
|
420
|
+
return result;
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
async function writeState(environment, path, state) {
|
|
424
|
+
await writeJson(environment, path, state);
|
|
425
|
+
}
|
|
426
|
+
async function writeJson(environment, path, value) {
|
|
427
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
428
|
+
try {
|
|
429
|
+
await environment.runtime.writeFile(temporary, new TextEncoder().encode(`${JSON.stringify(value, null, 2)}\n`), { noFollow: true, exclusive: true });
|
|
430
|
+
await environment.runtime.files.move(temporary, path);
|
|
431
|
+
}
|
|
432
|
+
finally {
|
|
433
|
+
await removeIfPresent(environment, temporary);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function readState(environment, path) {
|
|
437
|
+
try {
|
|
438
|
+
const value = JSON.parse(new TextDecoder().decode(await environment.runtime.readFile(path)));
|
|
439
|
+
if (!isRecord(value) || value.version !== STATE_VERSION || typeof value.memoryFingerprint !== 'string'
|
|
440
|
+
|| !isRecord(value.pending) || !isRecord(value.processed) || typeof value.updatedAt !== 'string') {
|
|
441
|
+
throw new Error('Local memory state is malformed or unsupported; processing is blocked');
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
version: STATE_VERSION,
|
|
445
|
+
memoryFingerprint: value.memoryFingerprint,
|
|
446
|
+
pending: value.pending,
|
|
447
|
+
processed: value.processed,
|
|
448
|
+
updatedAt: value.updatedAt,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
if (isMissing(error))
|
|
453
|
+
return undefined;
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async function fingerprintAt(environment, path, memoryPath) {
|
|
458
|
+
try {
|
|
459
|
+
return memoryArtifactFingerprint(await readMemoryDirectory(environment, path, { memoryPath, mode: 'read' }));
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
if (isMissing(error))
|
|
463
|
+
return undefined;
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function transactionDirectories(environment, path) {
|
|
468
|
+
try {
|
|
469
|
+
const entries = await environment.runtime.listFiles(path, { includeDirectories: true, includeOther: true });
|
|
470
|
+
const directories = [];
|
|
471
|
+
for (const name of entries) {
|
|
472
|
+
const entryPath = join(path, name);
|
|
473
|
+
const stat = await environment.runtime.files.inspect(entryPath);
|
|
474
|
+
if (stat.kind !== 'directory') {
|
|
475
|
+
throw new Error('Memory staging contains an unsafe transaction entry; recovery is blocked');
|
|
476
|
+
}
|
|
477
|
+
directories.push({ path: entryPath, stat });
|
|
478
|
+
}
|
|
479
|
+
return directories;
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
if (isMissing(error))
|
|
483
|
+
return [];
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async function readBoundedRecord(environment, path, maxBytes, unsafeMessage) {
|
|
488
|
+
try {
|
|
489
|
+
const snapshot = await environment.runtime.files.snapshot(path, maxBytes);
|
|
490
|
+
if (snapshot.truncated)
|
|
491
|
+
throw new Error(unsafeMessage);
|
|
492
|
+
const value = JSON.parse(new TextDecoder().decode(snapshot.content));
|
|
493
|
+
if (!isRecord(value))
|
|
494
|
+
throw new Error(unsafeMessage);
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
if (isMissing(error))
|
|
499
|
+
return undefined;
|
|
500
|
+
throw new Error(unsafeMessage, { cause: error });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async function realDirectory(environment, path, message) {
|
|
504
|
+
const stat = await safeLstat(environment, path);
|
|
505
|
+
if (stat?.kind !== 'directory')
|
|
506
|
+
throw new Error(message);
|
|
507
|
+
return { path, stat };
|
|
508
|
+
}
|
|
509
|
+
async function optionalRealDirectory(environment, path, message) {
|
|
510
|
+
const stat = await safeLstat(environment, path);
|
|
511
|
+
if (stat && stat.kind !== 'directory')
|
|
512
|
+
throw new Error(message);
|
|
513
|
+
}
|
|
514
|
+
async function verifyDirectory(environment, expected, path, message) {
|
|
515
|
+
const actual = await safeLstat(environment, path);
|
|
516
|
+
if (actual?.kind !== 'directory'
|
|
517
|
+
|| actual.identity !== expected.stat.identity)
|
|
518
|
+
throw new Error(message);
|
|
519
|
+
}
|
|
520
|
+
async function safeLstat(environment, path) {
|
|
521
|
+
try {
|
|
522
|
+
return await environment.runtime.files.inspect(path);
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
if (isMissing(error))
|
|
526
|
+
return undefined;
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function sameCursor(left, right) {
|
|
531
|
+
return left?.leafId === right.leafId && left?.transcriptDigest === right.transcriptDigest;
|
|
532
|
+
}
|
|
533
|
+
function acceptedCheckpointUpdates(entry) {
|
|
534
|
+
return entry?.acceptedUpdates ?? 1;
|
|
535
|
+
}
|
|
536
|
+
function acceptedCheckpointUpdatesFor(entries) {
|
|
537
|
+
let total = 0;
|
|
538
|
+
for (const entry of entries)
|
|
539
|
+
total += acceptedCheckpointUpdates(entry);
|
|
540
|
+
return total;
|
|
541
|
+
}
|
|
542
|
+
function sameStoredCheckpoint(left, right) {
|
|
543
|
+
return left?.recordedAt === right.recordedAt && left.checkpoint.sessionId === right.checkpoint.sessionId
|
|
544
|
+
&& left.checkpoint.sessionFile === right.checkpoint.sessionFile && sameCursor(left.checkpoint, right.checkpoint);
|
|
545
|
+
}
|
|
546
|
+
function sameCheckpoints(left, right) {
|
|
547
|
+
return left.length === right.length && left.every((entry, index) => sameStoredCheckpoint(right[index], entry));
|
|
548
|
+
}
|
|
549
|
+
function hasAcknowledgedCommit(state, fingerprint, checkpoints) {
|
|
550
|
+
return state.memoryFingerprint === fingerprint && checkpoints.every((entry) => {
|
|
551
|
+
const processed = state.processed[entry.checkpoint.sessionId];
|
|
552
|
+
return sameStoredCheckpoint(processed, entry) && processed.memoryFingerprint === fingerprint;
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
async function withPublicationEvidence(environment, store, control) {
|
|
556
|
+
const active = control.activeAttempt;
|
|
557
|
+
if (!active || active.published)
|
|
558
|
+
return control;
|
|
559
|
+
const state = await readState(environment, store.statePath);
|
|
560
|
+
if (!state || !hasAcknowledgedCommit(state, state.memoryFingerprint, active.checkpoints)
|
|
561
|
+
|| await fingerprintAt(environment, store.currentDirectory, store.memoryPath) !== state.memoryFingerprint)
|
|
562
|
+
return control;
|
|
563
|
+
return {
|
|
564
|
+
...control,
|
|
565
|
+
activeAttempt: { ...active, published: { fingerprint: state.memoryFingerprint, at: state.updatedAt } },
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
async function verifyInitializationLease(lease) {
|
|
569
|
+
if (!lease)
|
|
570
|
+
throw new Error('Local memory initialization needs writer ownership for recovery; retry after the writer releases its lease');
|
|
571
|
+
await verifyLease(lease);
|
|
572
|
+
}
|
|
573
|
+
async function verifyLease(lease) {
|
|
574
|
+
if (!lease || !(await lease.verify()))
|
|
575
|
+
throw new Error('Memory writer lease was lost');
|
|
576
|
+
}
|
|
577
|
+
function publicationAllowed(control, attempt, leaseToken) {
|
|
578
|
+
if (control.autoDisabled)
|
|
579
|
+
return false;
|
|
580
|
+
if (!attempt)
|
|
581
|
+
return !control.activeAttempt;
|
|
582
|
+
return control.generation === attempt.generation && sameAttempt(control.activeAttempt, attempt)
|
|
583
|
+
&& control.activeAttempt.leaseToken === leaseToken;
|
|
584
|
+
}
|
|
585
|
+
function assertPublicationAllowed(control, attempt, token) {
|
|
586
|
+
if (!publicationAllowed(control, attempt, token))
|
|
587
|
+
throw new Error('Memory publication is disabled or the attempt was fenced');
|
|
588
|
+
}
|
|
589
|
+
function emptyControl(now) {
|
|
590
|
+
return { version: 1, generation: 0, consecutiveFailures: 0, updatedAt: new Date(now).toISOString() };
|
|
591
|
+
}
|
|
592
|
+
async function readControl(environment, path) {
|
|
593
|
+
try {
|
|
594
|
+
const info = await environment.runtime.files.inspect(path);
|
|
595
|
+
if (info.kind !== 'file' || info.size > MAX_CONTROL_BYTES)
|
|
596
|
+
throw new LocalMemoryControlError(path);
|
|
597
|
+
const value = JSON.parse(new TextDecoder().decode(await environment.runtime.readFile(path)));
|
|
598
|
+
if (!isLocalMemoryControl(value))
|
|
599
|
+
throw new LocalMemoryControlError(path);
|
|
600
|
+
return value;
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
if (error instanceof SyntaxError)
|
|
604
|
+
throw new LocalMemoryControlError(path);
|
|
605
|
+
if (isMissing(error))
|
|
606
|
+
return undefined;
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async function writeControl(environment, path, control) {
|
|
611
|
+
if (!isLocalMemoryControl(control) || Buffer.byteLength(JSON.stringify(control), 'utf8') > MAX_CONTROL_BYTES) {
|
|
612
|
+
throw new LocalMemoryControlError(path);
|
|
613
|
+
}
|
|
614
|
+
await writeJson(environment, path, control);
|
|
615
|
+
}
|
|
616
|
+
function finishControl(control, outcome, now, retryDelaysMs) {
|
|
617
|
+
const { activeAttempt, ...rest } = control;
|
|
618
|
+
if (!activeAttempt)
|
|
619
|
+
return control;
|
|
620
|
+
const at = new Date(now).toISOString();
|
|
621
|
+
const status = activeAttempt.published ? 'success' : outcome.status;
|
|
622
|
+
const lastOutcome = { runId: activeAttempt.runId, generation: activeAttempt.generation, status, at };
|
|
623
|
+
if (status === 'cancelled') {
|
|
624
|
+
return { ...rest, generation: control.generation + 1, lastOutcome, updatedAt: at };
|
|
625
|
+
}
|
|
626
|
+
const { nextRetryAt: _retry, autoDisabled: _disabled, ...cleared } = rest;
|
|
627
|
+
if (status === 'success')
|
|
628
|
+
return {
|
|
629
|
+
...cleared,
|
|
630
|
+
...(outcome.status === 'cancelled' ? { generation: control.generation + 1 } : {}),
|
|
631
|
+
consecutiveFailures: 0,
|
|
632
|
+
lastOutcome,
|
|
633
|
+
updatedAt: at,
|
|
634
|
+
};
|
|
635
|
+
const consecutiveFailures = Math.min(control.consecutiveFailures + 1, 3);
|
|
636
|
+
return {
|
|
637
|
+
...cleared,
|
|
638
|
+
consecutiveFailures,
|
|
639
|
+
...(consecutiveFailures === 3 ? {
|
|
640
|
+
generation: control.generation + 1,
|
|
641
|
+
autoDisabled: { reason: 'Three consecutive memory processing attempts failed', runId: activeAttempt.runId, at },
|
|
642
|
+
lastDisabledRunId: activeAttempt.runId,
|
|
643
|
+
} : { nextRetryAt: new Date(now + retryDelaysMs[consecutiveFailures - 1]).toISOString() }),
|
|
644
|
+
lastOutcome,
|
|
645
|
+
updatedAt: at,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function sameAttempt(active, identity) {
|
|
649
|
+
return active?.runId === identity.runId && active.generation === identity.generation;
|
|
650
|
+
}
|
|
651
|
+
function isStoredCheckpoint(value) {
|
|
652
|
+
if (!isRecord(value) || !isTimestamp(value.recordedAt) || !isRecord(value.checkpoint))
|
|
653
|
+
return false;
|
|
654
|
+
if (value.acceptedUpdates !== undefined
|
|
655
|
+
&& (!Number.isSafeInteger(value.acceptedUpdates) || Number(value.acceptedUpdates) < 1))
|
|
656
|
+
return false;
|
|
657
|
+
const checkpoint = value.checkpoint;
|
|
658
|
+
return isNonemptyString(checkpoint.sessionId) && isNonemptyString(checkpoint.sessionFile)
|
|
659
|
+
&& (checkpoint.leafId === null || isNonemptyString(checkpoint.leafId))
|
|
660
|
+
&& isNonemptyString(checkpoint.transcriptDigest);
|
|
661
|
+
}
|
|
662
|
+
function isMemoryAttempt(value) {
|
|
663
|
+
return isRecord(value) && isIdentity(value) && isNonemptyString(value.leaseToken)
|
|
664
|
+
&& isTimestamp(value.startedAt) && isTimestamp(value.deadlineAt)
|
|
665
|
+
&& Date.parse(value.deadlineAt) > Date.parse(value.startedAt)
|
|
666
|
+
&& isFingerprint(value.baseFingerprint) && Array.isArray(value.checkpoints)
|
|
667
|
+
&& value.checkpoints.length > 0 && value.checkpoints.every(isStoredCheckpoint)
|
|
668
|
+
&& new Set(value.checkpoints.map((entry) => entry.checkpoint.sessionId)).size === value.checkpoints.length
|
|
669
|
+
&& (value.published === undefined || (isRecord(value.published)
|
|
670
|
+
&& isFingerprint(value.published.fingerprint) && isTimestamp(value.published.at)));
|
|
671
|
+
}
|
|
672
|
+
function isLocalMemoryControl(value) {
|
|
673
|
+
if (!isRecord(value) || value.version !== 1 || !isGeneration(value.generation)
|
|
674
|
+
|| !Number.isInteger(value.consecutiveFailures) || Number(value.consecutiveFailures) < 0
|
|
675
|
+
|| Number(value.consecutiveFailures) > 3 || !isTimestamp(value.updatedAt))
|
|
676
|
+
return false;
|
|
677
|
+
if (value.nextRetryAt !== undefined && !isTimestamp(value.nextRetryAt))
|
|
678
|
+
return false;
|
|
679
|
+
if (value.autoDisabled !== undefined && (!isRecord(value.autoDisabled)
|
|
680
|
+
|| !isNonemptyString(value.autoDisabled.reason) || !isRunId(value.autoDisabled.runId)
|
|
681
|
+
|| !isTimestamp(value.autoDisabled.at)))
|
|
682
|
+
return false;
|
|
683
|
+
if (value.lastDisabledRunId !== undefined && (!isRunId(value.lastDisabledRunId)
|
|
684
|
+
|| (isRecord(value.autoDisabled) && value.autoDisabled.runId !== value.lastDisabledRunId)))
|
|
685
|
+
return false;
|
|
686
|
+
if (value.activeAttempt !== undefined && (!isMemoryAttempt(value.activeAttempt)
|
|
687
|
+
|| value.activeAttempt.generation !== value.generation || value.autoDisabled !== undefined))
|
|
688
|
+
return false;
|
|
689
|
+
if (value.lastOutcome !== undefined && (!isRecord(value.lastOutcome) || !isIdentity(value.lastOutcome)
|
|
690
|
+
|| !['success', 'failure', 'cancelled'].includes(String(value.lastOutcome.status))
|
|
691
|
+
|| !isTimestamp(value.lastOutcome.at) || value.lastOutcome.generation > Number(value.generation)))
|
|
692
|
+
return false;
|
|
693
|
+
if (value.consecutiveFailures === 3)
|
|
694
|
+
return value.autoDisabled !== undefined && value.nextRetryAt === undefined;
|
|
695
|
+
return value.autoDisabled === undefined
|
|
696
|
+
&& (value.consecutiveFailures === 0 ? value.nextRetryAt === undefined : value.nextRetryAt !== undefined);
|
|
697
|
+
}
|
|
698
|
+
function isCommitJournal(value) {
|
|
699
|
+
return value.version === 2 && ['prepared', 'published', 'rolled_back'].includes(String(value.phase))
|
|
700
|
+
&& isFingerprint(value.baseFingerprint) && isFingerprint(value.fingerprint)
|
|
701
|
+
&& Array.isArray(value.checkpoints) && value.checkpoints.every(isStoredCheckpoint)
|
|
702
|
+
&& new Set(value.checkpoints.map((entry) => entry.checkpoint.sessionId)).size === value.checkpoints.length
|
|
703
|
+
&& (value.attempt === undefined || (isRecord(value.attempt) && isIdentity(value.attempt)
|
|
704
|
+
&& isNonemptyString(value.attempt.leaseToken)))
|
|
705
|
+
&& (value.publishedAt === undefined || isTimestamp(value.publishedAt));
|
|
706
|
+
}
|
|
707
|
+
function isLegacyCommitJournal(value) {
|
|
708
|
+
return value.version === 1 && isFingerprint(value.baseFingerprint) && isFingerprint(value.fingerprint)
|
|
709
|
+
&& Array.isArray(value.processed) && value.processed.every(isNonemptyString);
|
|
710
|
+
}
|
|
711
|
+
function isIdentity(value) {
|
|
712
|
+
return isRunId(value.runId) && isGeneration(value.generation);
|
|
713
|
+
}
|
|
714
|
+
function isGeneration(value) {
|
|
715
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value < Number.MAX_SAFE_INTEGER;
|
|
716
|
+
}
|
|
717
|
+
function isRunId(value) {
|
|
718
|
+
return typeof value === 'string' && /^[a-zA-Z0-9_-]+$/u.test(value);
|
|
719
|
+
}
|
|
720
|
+
function isFingerprint(value) {
|
|
721
|
+
return typeof value === 'string' && /^[a-f0-9]{64}$/u.test(value);
|
|
722
|
+
}
|
|
723
|
+
function isTimestamp(value) {
|
|
724
|
+
return typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
725
|
+
}
|
|
726
|
+
function isNonemptyString(value) {
|
|
727
|
+
return typeof value === 'string' && value.length > 0;
|
|
728
|
+
}
|
|
729
|
+
function isMissing(error) {
|
|
730
|
+
return typeof error === 'object' && error !== null && Reflect.get(error, 'code') === 'ENOENT';
|
|
731
|
+
}
|
|
732
|
+
function isRecord(value) {
|
|
733
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
734
|
+
}
|
|
735
|
+
//# sourceMappingURL=store.js.map
|