narraeon 0.7.0 → 0.7.2
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/dist/node/runtime/V1Runtime.js +93 -69
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +59 -12
- package/dist/node/runtime/config/dataRootCleanup.js +52 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +236 -26
- package/dist/node/runtime/play/PlayCallChain.js +172 -54
- package/dist/node/runtime/play/PlayPromptRun.js +157 -3
- package/dist/node/runtime/play/PlayTraceRetention.js +2 -2
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +5 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +10 -4
- package/dist/node/runtime/world/WorldLedgerAuthority.js +13 -10
- package/dist/node/runtime/world/WorldOperationCoordinator.js +4 -0
- package/dist/node/runtime/world/WorldQueue.js +14 -1
- package/dist/node/runtime/world/WorldTurnLedger.js +48 -20
- package/dist/node/server/createServer.js +3 -1
- package/dist/node/server/startWebServer.js +2 -5
- package/dist/web/assets/index-DkamBH6c.js +1408 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/node/runtime/config/dataRootLock.js +0 -108
- package/dist/web/assets/index-Ro7dnSsC.js +0 -1408
|
@@ -2,10 +2,10 @@ import { isResolutionReceipt } from "../../protocol/resolution.js";
|
|
|
2
2
|
import { legacyModelBindingFailure, projectLegacyModelBindingFailure, } from "./LegacyModelBindingFailure.js";
|
|
3
3
|
import { isPlayFailureKind, } from "../../protocol/playFailure.js";
|
|
4
4
|
import { isNarrativeOrigin, } from "../history/HistoryBackground.js";
|
|
5
|
-
import {
|
|
5
|
+
import { contextPromptBasis, decodePlayPromptRun, encodePlayPromptRunRecord, isPlayPromptRunReference, promptRunCompiled, promptRunFromReference, sharePromptCompiled, sharePromptRun, } from "./PlayPromptRun.js";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { constants } from "node:fs";
|
|
8
|
-
import { chmod, copyFile, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
8
|
+
import { chmod, copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
9
9
|
import { linkExclusive } from "../FileNativeExclusiveLink.js";
|
|
10
10
|
import { dirname, join, resolve } from "node:path";
|
|
11
11
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -43,16 +43,10 @@ export class FileNativePlayTimelineStore {
|
|
|
43
43
|
const transcriptCount = prefixLength(input.source.transcript, input.target.transcript);
|
|
44
44
|
const eventCount = prefixLength(input.source.events, input.target.events);
|
|
45
45
|
const completedToolCount = prefixLength(input.source.completedTools, input.target.completedTools);
|
|
46
|
-
|
|
47
|
-
input.
|
|
48
|
-
const encoding = await this.readInitialEncoding(input.sourceWorldId, input.source.chainId).catch(() => null);
|
|
46
|
+
for (const exchange of (await recordedEncodingExchanges(sourceRoot)).slice(0, 2)) {
|
|
47
|
+
const encoding = await this.readInitialEncoding(input.sourceWorldId, input.source.chainId, exchange).catch(() => null);
|
|
49
48
|
if (encoding !== null)
|
|
50
|
-
await publishImmutableJson(join(targetRoot,
|
|
51
|
-
}
|
|
52
|
-
for (const run of input.target.promptRuns ?? []) {
|
|
53
|
-
const encoding = await this.readInitialEncoding(input.sourceWorldId, input.source.chainId, run.exchange).catch(() => null);
|
|
54
|
-
if (encoding !== null)
|
|
55
|
-
await publishImmutableJson(join(targetRoot, `request-encoding-${run.exchange}.json`), encoding);
|
|
49
|
+
await publishImmutableJson(join(targetRoot, encodingFileName(exchange)), encoding);
|
|
56
50
|
}
|
|
57
51
|
await Promise.all([
|
|
58
52
|
cloneNumberedPrefix({
|
|
@@ -120,7 +114,7 @@ export class FileNativePlayTimelineStore {
|
|
|
120
114
|
};
|
|
121
115
|
const lastRequestDigest = value.lastRequest === null
|
|
122
116
|
? null
|
|
123
|
-
: await this.#publishRequest(
|
|
117
|
+
: await this.#publishRequest(root, value.lastRequest);
|
|
124
118
|
const continuation = {
|
|
125
119
|
schemaVersion: 3,
|
|
126
120
|
kind: "play_context_continuation",
|
|
@@ -148,8 +142,9 @@ export class FileNativePlayTimelineStore {
|
|
|
148
142
|
for (let index = previous.completedToolCount; index < value.completedTools.length; index += 1)
|
|
149
143
|
await publishImmutableJson(join(root, "completed-tools", numbered(index + 1)), value.completedTools[index]);
|
|
150
144
|
const promptRuns = value.promptRuns ?? [];
|
|
145
|
+
const promptBase = contextPromptBasis(value);
|
|
151
146
|
for (let index = supplied.promptRunCount ?? 0; index < promptRuns.length; index += 1) {
|
|
152
|
-
await publishImmutableJson(join(root, "prompt-runs", numbered(index + 1)),
|
|
147
|
+
await publishImmutableJson(join(root, "prompt-runs", numbered(index + 1)), encodePlayPromptRunRecord(promptRuns[index], promptRunBasisAt(promptRuns, index, promptBase)));
|
|
153
148
|
}
|
|
154
149
|
const state = {
|
|
155
150
|
schemaVersion: 3,
|
|
@@ -173,6 +168,17 @@ export class FileNativePlayTimelineStore {
|
|
|
173
168
|
updatedAt: value.updatedAt,
|
|
174
169
|
};
|
|
175
170
|
await publishJson(join(root, "state.json"), state);
|
|
171
|
+
// Only after state.json names the new continuation and request do the
|
|
172
|
+
// superseded ones go; a reader holding the old state retries (readContext).
|
|
173
|
+
await pruneSupersededContextFiles(root, {
|
|
174
|
+
continuationDigest,
|
|
175
|
+
lastRequestDigest,
|
|
176
|
+
});
|
|
177
|
+
// Every write path upgrades the world first (ensureCurrentStorage), so a
|
|
178
|
+
// context written here is already in the current layout.
|
|
179
|
+
if ((await readOptionalJson(join(runtimeRoot, LAYOUT_MARKER))) ===
|
|
180
|
+
null)
|
|
181
|
+
await publishJson(join(runtimeRoot, LAYOUT_MARKER), layoutMarker());
|
|
176
182
|
if (publishHead)
|
|
177
183
|
await publishJson(join(runtimeRoot, "play-timeline-head.json"), {
|
|
178
184
|
schemaVersion: 3,
|
|
@@ -214,20 +220,33 @@ export class FileNativePlayTimelineStore {
|
|
|
214
220
|
async readContext(worldId, chainId) {
|
|
215
221
|
assertIdentity(worldId, "World ID");
|
|
216
222
|
assertIdentity(chainId, "Call-chain ID");
|
|
223
|
+
// Reads take no lock. A writer prunes the superseded continuation and request
|
|
224
|
+
// only after publishing the state that names their replacements, so a reader
|
|
225
|
+
// that loaded the older state retries from the current one.
|
|
226
|
+
for (let attempt = 1;; attempt += 1)
|
|
227
|
+
try {
|
|
228
|
+
return await this.#readContextOnce(worldId, chainId);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (attempt === 3 || !isNodeError(error) || error.code !== "ENOENT")
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async #readContextOnce(worldId, chainId) {
|
|
217
236
|
const metadata = await this.#readContextMetadata(worldId, chainId);
|
|
218
237
|
if (metadata === null)
|
|
219
238
|
return null;
|
|
220
239
|
const { index: indexValue, state: stateValue, root } = metadata;
|
|
221
|
-
const [baseValue, continuationValue, transcript, events, completedTools
|
|
240
|
+
const [baseValue, continuationValue, transcript, events, completedTools] = await Promise.all([
|
|
222
241
|
readJson(join(root, "base.json")),
|
|
223
242
|
readJson(join(root, "continuations", `${stateValue.continuationDigest}.json`)),
|
|
224
243
|
readNumbered(join(root, "transcript"), stateValue.transcriptCount),
|
|
225
244
|
readNumberedValidated(join(root, "events"), stateValue.eventCount, assertPlayEvent),
|
|
226
245
|
readNumbered(join(root, "completed-tools"), stateValue.completedToolCount),
|
|
227
|
-
readNumbered(join(root, "prompt-runs"), stateValue.promptRunCount ?? 0).then((records) => records.map(decodePlayPromptRun)),
|
|
228
246
|
]);
|
|
229
247
|
assertContextBase(baseValue);
|
|
230
248
|
assertContextContinuation(continuationValue);
|
|
249
|
+
const promptRuns = await readPromptRuns(join(root, "prompt-runs"), stateValue.promptRunCount ?? 0, contextPromptBasis({ ...baseValue, playPreset: indexValue.playPreset }));
|
|
231
250
|
if (baseValue.worldId !== worldId ||
|
|
232
251
|
baseValue.chainId !== chainId ||
|
|
233
252
|
continuationValue.chainId !== chainId ||
|
|
@@ -237,7 +256,7 @@ export class FileNativePlayTimelineStore {
|
|
|
237
256
|
throw new Error("Play context durable identity is inconsistent");
|
|
238
257
|
const lastRequest = continuationValue.lastRequestDigest === null
|
|
239
258
|
? null
|
|
240
|
-
: await readJson(join(
|
|
259
|
+
: await readJson(join(root, "requests", `${continuationValue.lastRequestDigest}.json`));
|
|
241
260
|
if (lastRequest !== null &&
|
|
242
261
|
(!isRecord(lastRequest) ||
|
|
243
262
|
createHash("sha256")
|
|
@@ -316,17 +335,18 @@ export class FileNativePlayTimelineStore {
|
|
|
316
335
|
},
|
|
317
336
|
};
|
|
318
337
|
}
|
|
319
|
-
/**
|
|
338
|
+
/**
|
|
339
|
+
* The first dispatched request encoding is diagnostic evidence, not Authority.
|
|
340
|
+
* A context keeps only the encodings of its two latest exchanges, which are
|
|
341
|
+
* what the AI-reading diagnostics compare.
|
|
342
|
+
*/
|
|
320
343
|
async recordInitialEncoding(worldId, chainId, encoding, exchange = 1) {
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
await publishImmutableJson(path, encoding);
|
|
344
|
+
const root = this.#contextRoot(worldId, chainId);
|
|
345
|
+
await publishImmutableJson(join(root, encodingFileName(exchange)), encoding);
|
|
346
|
+
await pruneEncodings(root);
|
|
325
347
|
}
|
|
326
348
|
async readInitialEncoding(worldId, chainId, exchange = 1) {
|
|
327
|
-
const value = await readOptionalJson(join(this.#contextRoot(worldId, chainId), exchange
|
|
328
|
-
? "initial-request-encoding.json"
|
|
329
|
-
: `request-encoding-${exchange}.json`));
|
|
349
|
+
const value = await readOptionalJson(join(this.#contextRoot(worldId, chainId), encodingFileName(exchange)));
|
|
330
350
|
if (value === null)
|
|
331
351
|
return null;
|
|
332
352
|
if (!isRecord(value) ||
|
|
@@ -354,6 +374,26 @@ export class FileNativePlayTimelineStore {
|
|
|
354
374
|
}
|
|
355
375
|
return reverse.reverse();
|
|
356
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* The timeline's context IDs, oldest first, and its generation, read from the
|
|
379
|
+
* small context indexes only, never their transcripts or prompt runs.
|
|
380
|
+
*/
|
|
381
|
+
async readTimelineOutline(worldId) {
|
|
382
|
+
assertIdentity(worldId, "World ID");
|
|
383
|
+
const head = await this.#readTimelineHead(worldId);
|
|
384
|
+
if (head === null)
|
|
385
|
+
return null;
|
|
386
|
+
const reverse = [];
|
|
387
|
+
let chainId = head.chainId;
|
|
388
|
+
while (chainId !== null) {
|
|
389
|
+
const metadata = await this.#readContextMetadata(worldId, chainId);
|
|
390
|
+
if (metadata === null)
|
|
391
|
+
throw new Error("Play timeline points to a missing context");
|
|
392
|
+
reverse.push(chainId);
|
|
393
|
+
chainId = metadata.index.previousChainId;
|
|
394
|
+
}
|
|
395
|
+
return { generation: head.generation, chainIds: reverse.reverse() };
|
|
396
|
+
}
|
|
357
397
|
/** Resolve only requested reply summaries; decoration reads never load Provider transcripts. */
|
|
358
398
|
async resolveReplies(worldId, targets) {
|
|
359
399
|
if (targets.length === 0)
|
|
@@ -503,6 +543,47 @@ export class FileNativePlayTimelineStore {
|
|
|
503
543
|
newGeneration() {
|
|
504
544
|
return `timeline:${randomUUID()}`;
|
|
505
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Whether the world's play timeline is in the current layout (ADR-0034). A world
|
|
548
|
+
* with no play timeline has nothing to upgrade, so reads never write its marker.
|
|
549
|
+
*/
|
|
550
|
+
async layoutCurrent(worldId) {
|
|
551
|
+
assertIdentity(worldId, "World ID");
|
|
552
|
+
const runtimeRoot = this.#worldRuntimeRoot(worldId);
|
|
553
|
+
if ((await readOptionalJson(this.#layoutMarkerPath(worldId))) !==
|
|
554
|
+
null)
|
|
555
|
+
return true;
|
|
556
|
+
return ((await pathExists(join(runtimeRoot, "play-contexts"))) === false &&
|
|
557
|
+
(await pathExists(join(runtimeRoot, "play-requests"))) === false);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Upgrade a released play timeline in place: each context takes its own copy
|
|
561
|
+
* of its frozen request, snapshots that repeat their basis become references,
|
|
562
|
+
* and superseded encodings and records go. The released request directory is
|
|
563
|
+
* removed only after every context is complete, and the marker is written
|
|
564
|
+
* last, so an interrupted run starts again from the released files.
|
|
565
|
+
*/
|
|
566
|
+
async migrateLayout(worldId) {
|
|
567
|
+
assertIdentity(worldId, "World ID");
|
|
568
|
+
if (await this.layoutCurrent(worldId))
|
|
569
|
+
return;
|
|
570
|
+
const runtimeRoot = this.#worldRuntimeRoot(worldId);
|
|
571
|
+
const contextsRoot = join(runtimeRoot, "play-contexts");
|
|
572
|
+
for (const entry of await readDirectoryNames(contextsRoot)) {
|
|
573
|
+
const root = join(contextsRoot, entry);
|
|
574
|
+
if ((await readOptionalJson(join(root, "state.json"))) === null)
|
|
575
|
+
continue;
|
|
576
|
+
await migrateContextLayout(runtimeRoot, root);
|
|
577
|
+
}
|
|
578
|
+
await rm(join(runtimeRoot, "play-requests"), {
|
|
579
|
+
recursive: true,
|
|
580
|
+
force: true,
|
|
581
|
+
});
|
|
582
|
+
await publishJson(this.#layoutMarkerPath(worldId), layoutMarker());
|
|
583
|
+
}
|
|
584
|
+
#layoutMarkerPath(worldId) {
|
|
585
|
+
return join(this.#worldRuntimeRoot(worldId), LAYOUT_MARKER);
|
|
586
|
+
}
|
|
506
587
|
#worldRuntimeRoot(worldId) {
|
|
507
588
|
return join(this.#worldsRoot, worldId, "runtime");
|
|
508
589
|
}
|
|
@@ -522,11 +603,12 @@ export class FileNativePlayTimelineStore {
|
|
|
522
603
|
assertIdentity(value.chainId, "Call-chain ID");
|
|
523
604
|
return value;
|
|
524
605
|
}
|
|
525
|
-
|
|
606
|
+
/** The context's frozen request lives in the context: forks copy the one they keep. */
|
|
607
|
+
async #publishRequest(contextRoot, request) {
|
|
526
608
|
const digest = createHash("sha256")
|
|
527
609
|
.update(JSON.stringify(request))
|
|
528
610
|
.digest("hex");
|
|
529
|
-
await publishImmutableJson(join(
|
|
611
|
+
await publishImmutableJson(join(contextRoot, "requests", `${digest}.json`), request);
|
|
530
612
|
return digest;
|
|
531
613
|
}
|
|
532
614
|
async #initialCursor(worldId, generation, chainId) {
|
|
@@ -1062,6 +1144,134 @@ async function assertSameFileBytes(source, target) {
|
|
|
1062
1144
|
async function readNumbered(root, count) {
|
|
1063
1145
|
return Promise.all(Array.from({ length: count }, (_, index) => readJson(join(root, numbered(index + 1)))));
|
|
1064
1146
|
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Runs are read one at a time and share their strings as they are decoded, so
|
|
1149
|
+
* a long context never holds every run's raw text and parsed copy at once. A
|
|
1150
|
+
* reference repeats an earlier compiled prompt: the context base (0) or a run
|
|
1151
|
+
* decoded before it.
|
|
1152
|
+
*/
|
|
1153
|
+
async function readPromptRuns(root, count, base) {
|
|
1154
|
+
const strings = new Map();
|
|
1155
|
+
const sharedBase = base === null ? null : sharePromptCompiled(base, strings);
|
|
1156
|
+
const runs = [];
|
|
1157
|
+
for (let index = 0; index < count; index += 1) {
|
|
1158
|
+
const record = await readJson(join(root, numbered(index + 1)));
|
|
1159
|
+
runs.push(isPlayPromptRunReference(record)
|
|
1160
|
+
? promptRunFromReference(record, (basisRun) => basisRun === 0
|
|
1161
|
+
? sharedBase
|
|
1162
|
+
: basisRun <= runs.length
|
|
1163
|
+
? runs[basisRun - 1]
|
|
1164
|
+
: null)
|
|
1165
|
+
: sharePromptRun(decodePlayPromptRun(record), strings));
|
|
1166
|
+
}
|
|
1167
|
+
return runs;
|
|
1168
|
+
}
|
|
1169
|
+
/** The basis a run repeats: the context base for the first run, else the run before it. */
|
|
1170
|
+
function promptRunBasisAt(runs, index, base) {
|
|
1171
|
+
if (index === 0)
|
|
1172
|
+
return base === null ? null : { index: 0, compiled: base };
|
|
1173
|
+
return { index, compiled: promptRunCompiled(runs[index - 1]) };
|
|
1174
|
+
}
|
|
1175
|
+
const LAYOUT_MARKER = "play-timeline-layout.json";
|
|
1176
|
+
function layoutMarker() {
|
|
1177
|
+
return { schemaVersion: 1, kind: "play_timeline_layout" };
|
|
1178
|
+
}
|
|
1179
|
+
async function pathExists(path) {
|
|
1180
|
+
return stat(path).then(() => true, (error) => {
|
|
1181
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
1182
|
+
return false;
|
|
1183
|
+
throw error;
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
function encodingFileName(exchange) {
|
|
1187
|
+
return exchange === 1
|
|
1188
|
+
? "initial-request-encoding.json"
|
|
1189
|
+
: `request-encoding-${exchange}.json`;
|
|
1190
|
+
}
|
|
1191
|
+
/** The exchanges with a recorded encoding in a context, latest first. */
|
|
1192
|
+
async function recordedEncodingExchanges(contextRoot) {
|
|
1193
|
+
return (await readDirectoryNames(contextRoot))
|
|
1194
|
+
.map((name) => {
|
|
1195
|
+
if (name === "initial-request-encoding.json")
|
|
1196
|
+
return 1;
|
|
1197
|
+
const match = /^request-encoding-([1-9][0-9]*)\.json$/u.exec(name);
|
|
1198
|
+
return match === null ? null : Number(match[1]);
|
|
1199
|
+
})
|
|
1200
|
+
.filter((exchange) => exchange !== null)
|
|
1201
|
+
.sort((left, right) => right - left);
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Keep the encodings of the two latest exchanges, the pair the AI-reading
|
|
1205
|
+
* diagnostics compare. A context without runs is read at exchange 1.
|
|
1206
|
+
*/
|
|
1207
|
+
async function pruneEncodings(contextRoot) {
|
|
1208
|
+
for (const exchange of (await recordedEncodingExchanges(contextRoot)).slice(2))
|
|
1209
|
+
await rm(join(contextRoot, encodingFileName(exchange)), { force: true });
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Bring one released context to the current layout. Every step is idempotent:
|
|
1213
|
+
* an existing request copy is kept, and a record already in its current form is
|
|
1214
|
+
* left alone, so a rerun after an interruption converges on the same files.
|
|
1215
|
+
*/
|
|
1216
|
+
async function migrateContextLayout(runtimeRoot, root) {
|
|
1217
|
+
const indexValue = await readJson(join(root, "index.json"));
|
|
1218
|
+
const stateValue = await readJson(join(root, "state.json"));
|
|
1219
|
+
assertContextIndex(indexValue);
|
|
1220
|
+
assertContextState(stateValue);
|
|
1221
|
+
const baseValue = await readJson(join(root, "base.json"));
|
|
1222
|
+
assertContextBase(baseValue);
|
|
1223
|
+
const continuationValue = await readJson(join(root, "continuations", `${stateValue.continuationDigest}.json`));
|
|
1224
|
+
assertContextContinuation(continuationValue);
|
|
1225
|
+
const digest = continuationValue.lastRequestDigest;
|
|
1226
|
+
if (digest !== null) {
|
|
1227
|
+
const target = join(root, "requests", `${digest}.json`);
|
|
1228
|
+
if ((await readOptionalJson(target)) === null) {
|
|
1229
|
+
const request = await readJson(join(runtimeRoot, "play-requests", `${digest}.json`));
|
|
1230
|
+
if (!isRecord(request) ||
|
|
1231
|
+
createHash("sha256").update(JSON.stringify(request)).digest("hex") !==
|
|
1232
|
+
digest)
|
|
1233
|
+
throw new Error("Frozen play request does not match its durable identity");
|
|
1234
|
+
await publishImmutableJson(target, request);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const basis = contextPromptBasis({
|
|
1238
|
+
...baseValue,
|
|
1239
|
+
playPreset: indexValue.playPreset,
|
|
1240
|
+
});
|
|
1241
|
+
const promptRuns = await readPromptRuns(join(root, "prompt-runs"), stateValue.promptRunCount ?? 0, basis);
|
|
1242
|
+
for (let index = 0; index < promptRuns.length; index += 1) {
|
|
1243
|
+
const path = join(root, "prompt-runs", numbered(index + 1));
|
|
1244
|
+
const record = encodePlayPromptRunRecord(promptRuns[index], promptRunBasisAt(promptRuns, index, basis));
|
|
1245
|
+
if (!isDeepStrictEqual(await readJson(path), record))
|
|
1246
|
+
await publishJson(path, record);
|
|
1247
|
+
}
|
|
1248
|
+
await pruneEncodings(root);
|
|
1249
|
+
await pruneSupersededContextFiles(root, {
|
|
1250
|
+
continuationDigest: stateValue.continuationDigest,
|
|
1251
|
+
lastRequestDigest: digest,
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
/** Remove continuations and requests that the current state no longer names. */
|
|
1255
|
+
async function pruneSupersededContextFiles(root, current) {
|
|
1256
|
+
for (const [directory, keep] of [
|
|
1257
|
+
["continuations", current.continuationDigest],
|
|
1258
|
+
["requests", current.lastRequestDigest],
|
|
1259
|
+
]) {
|
|
1260
|
+
for (const name of await readDirectoryNames(join(root, directory)))
|
|
1261
|
+
if (name !== `${keep}.json`)
|
|
1262
|
+
await rm(join(root, directory, name), { force: true });
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
async function readDirectoryNames(directory) {
|
|
1266
|
+
try {
|
|
1267
|
+
return await readdir(directory);
|
|
1268
|
+
}
|
|
1269
|
+
catch (error) {
|
|
1270
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
1271
|
+
return [];
|
|
1272
|
+
throw error;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1065
1275
|
async function readNumberedValidated(root, count, assertValue) {
|
|
1066
1276
|
const values = await readNumbered(root, count);
|
|
1067
1277
|
const validated = [];
|