ai-workflow-logic 0.1.3 → 0.1.4

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.
@@ -0,0 +1,1223 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import type {
5
+ AIWorkflowFlowRunCheckpoint,
6
+ FlowClosedObject,
7
+ FlowClosedValue,
8
+ FlowDefinitionArtifactIdentity,
9
+ FlowRunCheckpointCommit,
10
+ FlowRunCheckpointKey,
11
+ FlowRunCheckpointRuntime,
12
+ FlowRunCheckpointStore,
13
+ FlowRunProfileState,
14
+ } from 'ai-workflow-contract';
15
+ import type { WorkCtrlFlowSnapshot } from 'work-ctrl-flow-contract';
16
+ import {
17
+ createMaterializedWorkCtrlFlowEngineFromDirectory,
18
+ loadMaterializedFlowInstance,
19
+ materializeFlowInstance,
20
+ type MaterializedWorkCtrlFlowDirectoryOptions,
21
+ } from 'work-ctrl-flow-logic';
22
+ import {
23
+ createWorkCtrlFlowCheckpointStore,
24
+ FlowRunCheckpointTransitionError,
25
+ FlowRunCheckpointVersionConflictError,
26
+ validateFlowRunCheckpointTransition,
27
+ } from './checkpoint-store.js';
28
+ import {
29
+ normalizeFlowInstanceDescriptor,
30
+ normalizeFlowClosedObject,
31
+ normalizeFlowRunCheckpoint,
32
+ } from './run-checkpoint.js';
33
+
34
+ const CHECKPOINT_FILE = 'checkpoint.json';
35
+ const CHECKPOINT_STAGING_FILE = 'checkpoint.staging.json';
36
+ const CHECKPOINT_LOCK_FILE = 'checkpoint.lock';
37
+ const RUN_INDEX_FILE = 'run-index.json';
38
+ const RUN_INDEX_STAGING_FILE = 'run-index.staging.json';
39
+ const RUN_INDEX_LOCK_FILE = 'run-index.lock';
40
+ const LEGACY_IMPORT_RECEIPT_FILE = 'legacy-import.receipt.json';
41
+ const LEGACY_IMPORT_RECEIPT_STAGING_FILE = 'legacy-import.receipt.staging.json';
42
+ const LOCK_RETRY_MS = 2;
43
+ const LOCK_TIMEOUT_MS = 5_000;
44
+ const ABANDONED_INCOMPLETE_LOCK_MS = 30_000;
45
+ const NO_FOLLOW = fs.constants.O_NOFOLLOW ?? 0;
46
+
47
+ export interface FilesystemFlowRunCheckpointStoreOptions {
48
+ /** Injected support root; the domain contract never derives a host path. */
49
+ readonly storeRoot: string;
50
+ }
51
+
52
+ export interface CanonicalMaterializedWorkCtrlFlowOptions
53
+ extends Omit<MaterializedWorkCtrlFlowDirectoryOptions, 'store' | 'instancesDir' | 'instanceId'> {
54
+ readonly storeRoot: string;
55
+ readonly instanceId: string;
56
+ readonly checkpointConfig: FlowClosedValue;
57
+ readonly projectCheckpointOutput?: (snapshot: WorkCtrlFlowSnapshot) => FlowClosedValue;
58
+ }
59
+
60
+ /** Canonical WorkCtrl filesystem facade: frozen instance plus one checkpoint owner. */
61
+ export function createCanonicalMaterializedWorkCtrlFlowEngine(
62
+ options: CanonicalMaterializedWorkCtrlFlowOptions,
63
+ ) {
64
+ const instancesDir = path.join(options.storeRoot, 'instances');
65
+ const instanceDir = path.join(instancesDir, options.instanceId);
66
+ const instance = fs.existsSync(instanceDir)
67
+ ? loadMaterializedFlowInstance(instanceDir, options.instanceId)
68
+ : options.bundleDir && options.source
69
+ ? materializeFlowInstance({
70
+ definitionDir: options.bundleDir,
71
+ instancesDir,
72
+ instanceId: options.instanceId,
73
+ source: options.source,
74
+ materializedAtMs: options.clock?.now(),
75
+ })
76
+ : (() => {
77
+ if (options.bundleDir || options.source) {
78
+ throw new FlowRunFilesystemStoreError(
79
+ 'flow-run-instance-source-incomplete',
80
+ 'bundleDir and source identity must be provided together.',
81
+ );
82
+ }
83
+ throw new FlowRunFilesystemStoreError(
84
+ 'flow-run-instance-source-required',
85
+ 'bundleDir and source identity are required to admit a new canonical WorkCtrl instance.',
86
+ );
87
+ })();
88
+ const checkpointStore = new FilesystemFlowRunCheckpointStore({ storeRoot: options.storeRoot });
89
+ const store = createWorkCtrlFlowCheckpointStore(
90
+ { checkpointStore },
91
+ {
92
+ instanceId: options.instanceId,
93
+ definition: instance.descriptor.definition,
94
+ config: options.checkpointConfig,
95
+ ...(options.projectCheckpointOutput
96
+ ? { projectOutput: options.projectCheckpointOutput }
97
+ : {}),
98
+ },
99
+ );
100
+ const {
101
+ storeRoot: _storeRoot,
102
+ checkpointConfig: _checkpointConfig,
103
+ projectCheckpointOutput: _projectCheckpointOutput,
104
+ bundleDir: _bundleDir,
105
+ source: _source,
106
+ ...engineOptions
107
+ } = options;
108
+ return createMaterializedWorkCtrlFlowEngineFromDirectory({
109
+ ...engineOptions,
110
+ instancesDir,
111
+ instanceId: options.instanceId,
112
+ store,
113
+ });
114
+ }
115
+
116
+ export interface FlowRunIndexEntry {
117
+ readonly instanceId: string;
118
+ readonly runId: string;
119
+ readonly version: number;
120
+ readonly definitionRevision: string;
121
+ readonly definitionDigest: `sha256:${string}`;
122
+ readonly profileKind: FlowRunProfileState['kind'];
123
+ }
124
+
125
+ export interface FlowRunIndexProjection {
126
+ readonly schemaVersion: 'depa.flow-run-index/v1';
127
+ readonly entries: readonly FlowRunIndexEntry[];
128
+ }
129
+
130
+ export interface LegacyWorkCtrlSnapshotIdentity {
131
+ readonly treeId: string;
132
+ readonly digest: `sha256:${string}`;
133
+ }
134
+
135
+ export interface LegacyWorkCtrlSnapshotRead {
136
+ readonly identity: LegacyWorkCtrlSnapshotIdentity;
137
+ readonly snapshot: WorkCtrlFlowSnapshot;
138
+ }
139
+
140
+ export interface LegacyWorkCtrlSnapshotReader {
141
+ read(treeId: string): Promise<LegacyWorkCtrlSnapshotRead | undefined>;
142
+ }
143
+
144
+ export interface LegacyWorkCtrlCheckpointConversionOptions {
145
+ readonly target: FlowRunCheckpointKey;
146
+ readonly definition: FlowDefinitionArtifactIdentity;
147
+ readonly config: FlowClosedValue;
148
+ readonly output?: FlowClosedValue;
149
+ }
150
+
151
+ export interface LegacyWorkCtrlImportReceipt {
152
+ readonly schemaVersion: 'depa.flow-legacy-import-receipt/v1';
153
+ readonly legacyTreeId: string;
154
+ readonly legacySnapshotDigest: `sha256:${string}`;
155
+ readonly target: FlowRunCheckpointKey;
156
+ readonly definitionRevision: string;
157
+ readonly definitionDigest: `sha256:${string}`;
158
+ readonly acceptedCheckpointVersion: 0;
159
+ readonly acceptedCheckpointDigest: `sha256:${string}`;
160
+ }
161
+
162
+ export interface LegacyWorkCtrlImportRuntime extends FlowRunCheckpointRuntime {
163
+ readonly checkpointStore: FilesystemFlowRunCheckpointStore;
164
+ readonly legacyReader: LegacyWorkCtrlSnapshotReader;
165
+ }
166
+
167
+ export interface ImportLegacyWorkCtrlSnapshotInput
168
+ extends Omit<LegacyWorkCtrlCheckpointConversionOptions, 'target'> {
169
+ readonly legacyTreeId: string;
170
+ readonly target: FlowRunCheckpointKey;
171
+ }
172
+
173
+ export class FlowRunFilesystemStoreError extends Error {
174
+ constructor(public readonly code: string, message: string) {
175
+ super(message);
176
+ this.name = 'FlowRunFilesystemStoreError';
177
+ }
178
+ }
179
+
180
+ export class LegacyWorkCtrlImportError extends Error {
181
+ constructor(public readonly code: string, message: string) {
182
+ super(message);
183
+ this.name = 'LegacyWorkCtrlImportError';
184
+ }
185
+ }
186
+
187
+ export class LegacyWorkCtrlImportConflictError extends LegacyWorkCtrlImportError {
188
+ constructor(message: string) {
189
+ super('legacy-work-ctrl-import-conflict', message);
190
+ this.name = 'LegacyWorkCtrlImportConflictError';
191
+ }
192
+ }
193
+
194
+ type HeldLock = {
195
+ readonly fd: number;
196
+ readonly file: string;
197
+ readonly token: string;
198
+ readonly device: number;
199
+ readonly inode: number;
200
+ };
201
+
202
+ type LockObservation = {
203
+ readonly device: number;
204
+ readonly inode: number;
205
+ readonly mtimeMs: number;
206
+ readonly pid: number | undefined;
207
+ readonly token: string | undefined;
208
+ readonly createdAtMs: number | undefined;
209
+ };
210
+
211
+ type FlowRunFilesystemLocations = {
212
+ readonly instancesDir: string;
213
+ readonly instanceDir: string;
214
+ readonly runsDir: string;
215
+ readonly runDir: string;
216
+ readonly checkpointFile: string;
217
+ readonly stagingFile: string;
218
+ readonly lockFile: string;
219
+ };
220
+
221
+ /** Reference Node filesystem support for the canonical FlowRunCheckpoint port. */
222
+ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<FlowRunProfileState> {
223
+ readonly #storeRoot: string;
224
+ readonly #realStoreRoot: string;
225
+
226
+ constructor(options: FilesystemFlowRunCheckpointStoreOptions) {
227
+ const root = path.resolve(options.storeRoot);
228
+ assertExistingDirectory(root, undefined, 'flow-run-store-root-invalid');
229
+ this.#storeRoot = root;
230
+ this.#realStoreRoot = fs.realpathSync(root);
231
+ }
232
+
233
+ async load(key: FlowRunCheckpointKey): Promise<AIWorkflowFlowRunCheckpoint | undefined> {
234
+ const locations = this.#locations(key);
235
+ this.#preflightExistingRunBoundary(locations);
236
+ const descriptor = this.#requireInstance(locations.instanceDir, key.instanceId);
237
+ if (!fs.existsSync(locations.runsDir)) return undefined;
238
+ assertExistingDirectory(locations.runsDir, this.#realStoreRoot, 'flow-run-boundary-invalid');
239
+ if (!fs.existsSync(locations.runDir)) return undefined;
240
+ assertExistingDirectory(locations.runDir, this.#realStoreRoot, 'flow-run-symlink-boundary');
241
+ if (!fs.existsSync(locations.checkpointFile)) return undefined;
242
+ const checkpoint = readCheckpointFile(locations.checkpointFile, this.#realStoreRoot);
243
+ this.#assertCheckpointIdentity(checkpoint, key, descriptor.definition);
244
+ return checkpoint;
245
+ }
246
+
247
+ async commit(input: FlowRunCheckpointCommit<FlowRunProfileState>): Promise<AIWorkflowFlowRunCheckpoint> {
248
+ // Program data and logical ids are validated before filesystem mutation.
249
+ const checkpoint = normalizeFlowRunCheckpoint(input.checkpoint);
250
+ assertSafeSegment(checkpoint.instanceId);
251
+ assertSafeSegment(checkpoint.runId);
252
+ const key = { instanceId: checkpoint.instanceId, runId: checkpoint.runId };
253
+ const locations = this.#locations(key);
254
+ this.#preflightExistingRunBoundary(locations);
255
+ const descriptor = this.#requireInstance(locations.instanceDir, key.instanceId);
256
+ this.#assertCheckpointIdentity(checkpoint, key, descriptor.definition);
257
+
258
+ this.#ensureRunDirectory(locations);
259
+ const lock = await acquireLock(locations.runDir, locations.lockFile, this.#realStoreRoot);
260
+ try {
261
+ this.#assertRunPhysicalBoundary(locations);
262
+ removeUncommittedStaging(locations.stagingFile, locations.runDir, this.#realStoreRoot);
263
+ const current = fs.existsSync(locations.checkpointFile)
264
+ ? readCheckpointFile(locations.checkpointFile, this.#realStoreRoot)
265
+ : undefined;
266
+ if (current) this.#assertCheckpointIdentity(current, key, descriptor.definition);
267
+ const actualVersion = current?.version ?? null;
268
+ if (actualVersion !== input.expectedVersion) {
269
+ throw new FlowRunCheckpointVersionConflictError(input.expectedVersion, actualVersion);
270
+ }
271
+ validateFlowRunCheckpointTransition(current, checkpoint);
272
+ writeCheckpointCandidate(locations.stagingFile, checkpoint, this.#realStoreRoot);
273
+ const staged = readCheckpointFile(locations.stagingFile, this.#realStoreRoot);
274
+ this.#assertCheckpointIdentity(staged, key, descriptor.definition);
275
+ if (stableJson(staged) !== stableJson(checkpoint)) {
276
+ throw new FlowRunFilesystemStoreError(
277
+ 'flow-run-checkpoint-readback-mismatch',
278
+ 'Checkpoint staging readback differs from the normalized transition.',
279
+ );
280
+ }
281
+ this.#assertRunPhysicalBoundary(locations);
282
+ assertRegularFile(locations.stagingFile, this.#realStoreRoot, 'flow-run-staging-invalid');
283
+ if (fs.existsSync(locations.checkpointFile)) {
284
+ assertRegularFile(locations.checkpointFile, this.#realStoreRoot, 'flow-run-checkpoint-invalid');
285
+ }
286
+ fs.renameSync(locations.stagingFile, locations.checkpointFile);
287
+ const accepted = readCheckpointFile(locations.checkpointFile, this.#realStoreRoot);
288
+ this.#assertCheckpointIdentity(accepted, key, descriptor.definition);
289
+ if (stableJson(accepted) !== stableJson(checkpoint)) {
290
+ throw new FlowRunFilesystemStoreError(
291
+ 'flow-run-checkpoint-readback-mismatch',
292
+ 'Accepted checkpoint readback differs from the normalized transition.',
293
+ );
294
+ }
295
+ return accepted;
296
+ } finally {
297
+ releaseLock(lock, locations.runDir, this.#realStoreRoot);
298
+ }
299
+ }
300
+
301
+ /** Rebuildable observation only; it never writes a checkpoint. */
302
+ async rebuildRunIndex(): Promise<FlowRunIndexProjection> {
303
+ const instancesDir = path.join(this.#storeRoot, 'instances');
304
+ const entries: FlowRunIndexEntry[] = [];
305
+ if (fs.existsSync(instancesDir)) {
306
+ assertExistingDirectory(instancesDir, this.#realStoreRoot, 'flow-run-boundary-invalid');
307
+ for (const instanceEntry of readDirectoryCodeUnitOrdered(instancesDir)) {
308
+ if (instanceEntry.name.startsWith('.candidate-')) continue;
309
+ if (!instanceEntry.isDirectory() || instanceEntry.isSymbolicLink()) {
310
+ throw new FlowRunFilesystemStoreError('flow-run-index-entry-invalid', 'Instance index entries must be directories.');
311
+ }
312
+ assertSafeSegment(instanceEntry.name);
313
+ const instanceDir = path.join(instancesDir, instanceEntry.name);
314
+ const descriptor = this.#requireInstance(instanceDir, instanceEntry.name);
315
+ const runsDir = path.join(instanceDir, 'runs');
316
+ if (!fs.existsSync(runsDir)) continue;
317
+ assertExistingDirectory(runsDir, this.#realStoreRoot, 'flow-run-boundary-invalid');
318
+ for (const runEntry of readDirectoryCodeUnitOrdered(runsDir)) {
319
+ if (!runEntry.isDirectory() || runEntry.isSymbolicLink()) {
320
+ throw new FlowRunFilesystemStoreError('flow-run-index-entry-invalid', 'Run index entries must be directories.');
321
+ }
322
+ assertSafeSegment(runEntry.name);
323
+ const checkpoint = await this.load({ instanceId: instanceEntry.name, runId: runEntry.name });
324
+ if (!checkpoint) continue;
325
+ entries.push(Object.freeze({
326
+ instanceId: instanceEntry.name,
327
+ runId: runEntry.name,
328
+ version: checkpoint.version,
329
+ definitionRevision: descriptor.definition.revision,
330
+ definitionDigest: descriptor.definition.digest,
331
+ profileKind: checkpoint.profile.kind,
332
+ }));
333
+ }
334
+ }
335
+ }
336
+ entries.sort(compareIndexEntries);
337
+ const projection: FlowRunIndexProjection = Object.freeze({
338
+ schemaVersion: 'depa.flow-run-index/v1',
339
+ entries: Object.freeze(entries),
340
+ });
341
+ await this.#writeRunIndex(projection);
342
+ return projection;
343
+ }
344
+
345
+ async readLegacyImportReceipt(
346
+ key: FlowRunCheckpointKey,
347
+ ): Promise<LegacyWorkCtrlImportReceipt | undefined> {
348
+ const locations = this.#locations(key);
349
+ this.#preflightExistingRunBoundary(locations);
350
+ if (!fs.existsSync(locations.runDir)) return undefined;
351
+ const receiptFile = containedPath(locations.runDir, LEGACY_IMPORT_RECEIPT_FILE);
352
+ const checkpoint = await this.load(key);
353
+ if (!fs.existsSync(receiptFile)) return undefined;
354
+ if (!checkpoint) {
355
+ throw new LegacyWorkCtrlImportError(
356
+ 'legacy-work-ctrl-receipt-without-checkpoint',
357
+ 'A legacy import receipt cannot exist without canonical checkpoint authority.',
358
+ );
359
+ }
360
+ const receipt = readLegacyImportReceiptFile(receiptFile, this.#realStoreRoot);
361
+ assertReceiptMatchesCheckpoint(receipt, checkpoint);
362
+ return receipt;
363
+ }
364
+
365
+ async recordLegacyImportReceipt(
366
+ key: FlowRunCheckpointKey,
367
+ value: LegacyWorkCtrlImportReceipt,
368
+ ): Promise<LegacyWorkCtrlImportReceipt> {
369
+ const receipt = normalizeLegacyWorkCtrlImportReceipt(value);
370
+ if (receipt.target.instanceId !== key.instanceId || receipt.target.runId !== key.runId) {
371
+ throw new LegacyWorkCtrlImportConflictError('Receipt target differs from the requested canonical run.');
372
+ }
373
+ const locations = this.#locations(key);
374
+ this.#preflightExistingRunBoundary(locations);
375
+ const lock = await acquireLock(locations.runDir, locations.lockFile, this.#realStoreRoot);
376
+ try {
377
+ this.#assertRunPhysicalBoundary(locations);
378
+ const checkpoint = fs.existsSync(locations.checkpointFile)
379
+ ? readCheckpointFile(locations.checkpointFile, this.#realStoreRoot)
380
+ : undefined;
381
+ if (!checkpoint) {
382
+ throw new LegacyWorkCtrlImportError(
383
+ 'legacy-work-ctrl-checkpoint-missing',
384
+ 'Completion cannot be recorded before canonical checkpoint acceptance.',
385
+ );
386
+ }
387
+ const descriptor = this.#requireInstance(locations.instanceDir, key.instanceId);
388
+ this.#assertCheckpointIdentity(checkpoint, key, descriptor.definition);
389
+ assertReceiptMatchesCheckpoint(receipt, checkpoint);
390
+ const receiptFile = containedPath(locations.runDir, LEGACY_IMPORT_RECEIPT_FILE);
391
+ const stagingFile = containedPath(locations.runDir, LEGACY_IMPORT_RECEIPT_STAGING_FILE);
392
+ if (fs.existsSync(receiptFile)) {
393
+ const existing = readLegacyImportReceiptFile(receiptFile, this.#realStoreRoot);
394
+ if (stableJson(existing) !== stableJson(receipt)) {
395
+ throw new LegacyWorkCtrlImportConflictError('An append-only legacy import receipt already records different facts.');
396
+ }
397
+ return existing;
398
+ }
399
+ if (checkpoint.version !== receipt.acceptedCheckpointVersion
400
+ || digestStableValue(checkpoint) !== receipt.acceptedCheckpointDigest) {
401
+ throw new LegacyWorkCtrlImportConflictError(
402
+ 'Receipt recovery without an existing marker requires the exact accepted checkpoint version.',
403
+ );
404
+ }
405
+ removeUncommittedStaging(stagingFile, locations.runDir, this.#realStoreRoot);
406
+ writeBytesNoFollow(stagingFile, stableJson(receipt));
407
+ const staged = readLegacyImportReceiptFile(stagingFile, this.#realStoreRoot);
408
+ if (stableJson(staged) !== stableJson(receipt)) {
409
+ throw new FlowRunFilesystemStoreError(
410
+ 'legacy-work-ctrl-receipt-readback-mismatch',
411
+ 'Legacy import receipt staging readback differs.',
412
+ );
413
+ }
414
+ this.#assertRunPhysicalBoundary(locations);
415
+ assertRegularFile(stagingFile, this.#realStoreRoot, 'legacy-work-ctrl-receipt-invalid');
416
+ fs.renameSync(stagingFile, receiptFile);
417
+ const accepted = readLegacyImportReceiptFile(receiptFile, this.#realStoreRoot);
418
+ if (stableJson(accepted) !== stableJson(receipt)) {
419
+ throw new FlowRunFilesystemStoreError(
420
+ 'legacy-work-ctrl-receipt-readback-mismatch',
421
+ 'Accepted legacy import receipt differs.',
422
+ );
423
+ }
424
+ return accepted;
425
+ } finally {
426
+ releaseLock(lock, locations.runDir, this.#realStoreRoot);
427
+ }
428
+ }
429
+
430
+ #locations(key: FlowRunCheckpointKey): FlowRunFilesystemLocations {
431
+ assertSafeSegment(key.instanceId);
432
+ assertSafeSegment(key.runId);
433
+ const instancesDir = containedPath(this.#storeRoot, 'instances');
434
+ const instanceDir = containedPath(instancesDir, key.instanceId);
435
+ const runsDir = containedPath(instanceDir, 'runs');
436
+ const runDir = containedPath(runsDir, key.runId);
437
+ return {
438
+ instancesDir,
439
+ instanceDir,
440
+ runsDir,
441
+ runDir,
442
+ checkpointFile: containedPath(runDir, CHECKPOINT_FILE),
443
+ stagingFile: containedPath(runDir, CHECKPOINT_STAGING_FILE),
444
+ lockFile: containedPath(runDir, CHECKPOINT_LOCK_FILE),
445
+ };
446
+ }
447
+
448
+ #requireInstance(instanceDir: string, expectedInstanceId: string) {
449
+ const instancesDir = path.dirname(instanceDir);
450
+ assertExistingDirectory(instancesDir, this.#realStoreRoot, 'flow-run-instance-boundary-invalid');
451
+ assertExistingDirectory(instanceDir, this.#realStoreRoot, 'flow-run-instance-boundary-invalid');
452
+ try {
453
+ // Product schema was checked during atomic admission. The canonical store
454
+ // rechecks descriptor, containment and digest without assuming a profile.
455
+ const instance = loadMaterializedFlowInstance(instanceDir, expectedInstanceId, () => undefined);
456
+ return normalizeFlowInstanceDescriptor(instance.descriptor);
457
+ } catch (error) {
458
+ if (error instanceof FlowRunFilesystemStoreError) throw error;
459
+ throw new FlowRunFilesystemStoreError(
460
+ 'flow-run-instance-invalid',
461
+ `Frozen instance verification failed: ${(error as Error).message}`,
462
+ );
463
+ }
464
+ }
465
+
466
+ #preflightExistingRunBoundary(locations: FlowRunFilesystemLocations): void {
467
+ assertExistingDirectory(locations.instancesDir, this.#realStoreRoot, 'flow-run-instance-boundary-invalid');
468
+ assertExistingDirectory(locations.instanceDir, this.#realStoreRoot, 'flow-run-instance-boundary-invalid');
469
+ if (!fs.existsSync(locations.runsDir)) return;
470
+ assertExistingDirectory(locations.runsDir, this.#realStoreRoot, 'flow-run-boundary-invalid');
471
+ if (!fs.existsSync(locations.runDir)) return;
472
+ assertExistingDirectory(locations.runDir, this.#realStoreRoot, 'flow-run-symlink-boundary');
473
+ }
474
+
475
+ #assertCheckpointIdentity(
476
+ checkpoint: AIWorkflowFlowRunCheckpoint,
477
+ key: FlowRunCheckpointKey,
478
+ definition: AIWorkflowFlowRunCheckpoint['definition'],
479
+ ): void {
480
+ if (checkpoint.instanceId !== key.instanceId || checkpoint.runId !== key.runId) {
481
+ throw new FlowRunFilesystemStoreError(
482
+ 'flow-run-checkpoint-identity-mismatch',
483
+ 'Checkpoint identity differs from its canonical instance/run location.',
484
+ );
485
+ }
486
+ if (stableJson(checkpoint.definition) !== stableJson(definition)) {
487
+ throw new FlowRunFilesystemStoreError(
488
+ 'flow-run-definition-mismatch',
489
+ 'Checkpoint definition identity differs from the frozen instance.',
490
+ );
491
+ }
492
+ }
493
+
494
+ #ensureRunDirectory(locations: FlowRunFilesystemLocations): void {
495
+ ensureChildDirectory(locations.instanceDir, locations.runsDir, this.#realStoreRoot);
496
+ ensureChildDirectory(locations.runsDir, locations.runDir, this.#realStoreRoot);
497
+ this.#assertRunPhysicalBoundary(locations);
498
+ }
499
+
500
+ #assertRunPhysicalBoundary(locations: FlowRunFilesystemLocations): void {
501
+ assertExistingDirectory(locations.instanceDir, this.#realStoreRoot, 'flow-run-instance-boundary-invalid');
502
+ assertExistingDirectory(locations.runsDir, this.#realStoreRoot, 'flow-run-boundary-invalid');
503
+ assertExistingDirectory(locations.runDir, this.#realStoreRoot, 'flow-run-symlink-boundary');
504
+ }
505
+
506
+ async #writeRunIndex(projection: FlowRunIndexProjection): Promise<void> {
507
+ const indexFile = containedPath(this.#storeRoot, RUN_INDEX_FILE);
508
+ const stagingFile = containedPath(this.#storeRoot, RUN_INDEX_STAGING_FILE);
509
+ const lockFile = containedPath(this.#storeRoot, RUN_INDEX_LOCK_FILE);
510
+ const lock = await acquireLock(this.#storeRoot, lockFile, this.#realStoreRoot);
511
+ try {
512
+ removeUncommittedStaging(stagingFile, this.#storeRoot, this.#realStoreRoot);
513
+ writeBytesNoFollow(stagingFile, stableJson(projection));
514
+ const readback = readJsonFile(stagingFile, this.#realStoreRoot, 'flow-run-index-invalid');
515
+ if (stableJson(readback) !== stableJson(projection)) {
516
+ throw new FlowRunFilesystemStoreError('flow-run-index-readback-mismatch', 'Run index readback differs.');
517
+ }
518
+ assertExistingDirectory(this.#storeRoot, this.#realStoreRoot, 'flow-run-store-root-invalid');
519
+ assertRegularFile(stagingFile, this.#realStoreRoot, 'flow-run-index-invalid');
520
+ if (fs.existsSync(indexFile)) assertRegularFile(indexFile, this.#realStoreRoot, 'flow-run-index-invalid');
521
+ fs.renameSync(stagingFile, indexFile);
522
+ const accepted = readJsonFile(indexFile, this.#realStoreRoot, 'flow-run-index-invalid');
523
+ if (stableJson(accepted) !== stableJson(projection)) {
524
+ throw new FlowRunFilesystemStoreError('flow-run-index-readback-mismatch', 'Accepted run index differs.');
525
+ }
526
+ } finally {
527
+ releaseLock(lock, this.#storeRoot, this.#realStoreRoot);
528
+ }
529
+ }
530
+ }
531
+
532
+ /** Explicit read-only adapter for the existing FileWorkCtrlFlowStore layout. */
533
+ export class FilesystemLegacyWorkCtrlSnapshotReader implements LegacyWorkCtrlSnapshotReader {
534
+ readonly #legacyRoot: string;
535
+ readonly #realLegacyRoot: string;
536
+
537
+ constructor(options: { readonly legacyRoot: string }) {
538
+ this.#legacyRoot = path.resolve(options.legacyRoot);
539
+ assertExistingDirectory(this.#legacyRoot, undefined, 'legacy-work-ctrl-root-invalid');
540
+ this.#realLegacyRoot = fs.realpathSync(this.#legacyRoot);
541
+ }
542
+
543
+ async read(treeId: string): Promise<LegacyWorkCtrlSnapshotRead | undefined> {
544
+ if (typeof treeId !== 'string' || treeId.length === 0 || treeId !== treeId.trim()) {
545
+ throw new LegacyWorkCtrlImportError('legacy-work-ctrl-id-invalid', 'Legacy tree identity must be canonical.');
546
+ }
547
+ let encoded: string;
548
+ try {
549
+ encoded = encodeURIComponent(treeId);
550
+ } catch {
551
+ throw new LegacyWorkCtrlImportError('legacy-work-ctrl-id-invalid', 'Legacy tree identity cannot be encoded safely.');
552
+ }
553
+ const file = containedPath(this.#legacyRoot, `${encoded}.snapshot.json`);
554
+ if (!fs.existsSync(file)) return undefined;
555
+ const bytes = readFileBytesNoFollow(file, this.#realLegacyRoot, 'legacy-work-ctrl-snapshot-invalid');
556
+ let value: unknown;
557
+ try {
558
+ value = JSON.parse(bytes.toString('utf8')) as unknown;
559
+ } catch (error) {
560
+ throw new LegacyWorkCtrlImportError(
561
+ 'legacy-work-ctrl-snapshot-invalid',
562
+ `Legacy snapshot decode failed: ${(error as Error).message}`,
563
+ );
564
+ }
565
+ if (!isPlainRecord(value)
566
+ || value.schemaVersion !== 'depa.flows.workflow/v4'
567
+ || value.treeId !== treeId) {
568
+ throw new LegacyWorkCtrlImportError(
569
+ 'legacy-work-ctrl-identity-mismatch',
570
+ 'Legacy snapshot identity or schema differs from its requested flat-store identity.',
571
+ );
572
+ }
573
+ return Object.freeze({
574
+ identity: Object.freeze({ treeId, digest: digestBytes(bytes) }),
575
+ snapshot: value as unknown as WorkCtrlFlowSnapshot,
576
+ });
577
+ }
578
+ }
579
+
580
+ /** Pure legacy snapshot projection; it performs no read, write or completion action. */
581
+ export function convertLegacyWorkCtrlSnapshotToCheckpoint(
582
+ snapshot: WorkCtrlFlowSnapshot,
583
+ options: LegacyWorkCtrlCheckpointConversionOptions,
584
+ ): AIWorkflowFlowRunCheckpoint {
585
+ const closedSnapshot = normalizeFlowClosedObject(snapshot, 'legacy.snapshot') as unknown as WorkCtrlFlowSnapshot;
586
+ assertLegacyWorkCtrlSnapshotShape(closedSnapshot);
587
+ const legacyTreeId = closedSnapshot.treeId;
588
+ const rebindHandle = (handle: WorkCtrlFlowSnapshot['openWaitHandles'][number]) => ({
589
+ ...handle,
590
+ treeId: options.target.runId,
591
+ waitHandleId: rebindLegacyOwnedIdentity(handle.waitHandleId, legacyTreeId, options.target.runId),
592
+ });
593
+ const canonicalSnapshot = Object.freeze({
594
+ ...closedSnapshot,
595
+ treeId: options.target.runId,
596
+ openWaitHandles: closedSnapshot.openWaitHandles.map(rebindHandle),
597
+ closedWaitHandles: closedSnapshot.closedWaitHandles.map(rebindHandle),
598
+ appliedResumeSignals: closedSnapshot.appliedResumeSignals.map((identity) => (
599
+ rebindLegacyOwnedIdentity(identity, legacyTreeId, options.target.runId)
600
+ )),
601
+ orchestration: {
602
+ ...closedSnapshot.orchestration,
603
+ composites: Object.fromEntries(Object.entries(closedSnapshot.orchestration.composites).map(([key, fact]) => [
604
+ key,
605
+ {
606
+ ...fact,
607
+ ...(fact.childTreeId === undefined ? {} : {
608
+ childTreeId: rebindLegacyOwnedIdentity(fact.childTreeId, legacyTreeId, options.target.runId),
609
+ }),
610
+ },
611
+ ])),
612
+ },
613
+ }) as WorkCtrlFlowSnapshot;
614
+ const controllerSidecars = {
615
+ status: canonicalSnapshot.status,
616
+ lastTickNo: canonicalSnapshot.lastTickNo,
617
+ branchDecisions: canonicalSnapshot.branchDecisions,
618
+ orchestration: canonicalSnapshot.orchestration,
619
+ openWaitHandles: canonicalSnapshot.openWaitHandles,
620
+ closedWaitHandles: canonicalSnapshot.closedWaitHandles,
621
+ appliedResumeSignals: canonicalSnapshot.appliedResumeSignals,
622
+ };
623
+ const nodeSidecars = {
624
+ nodeResults: canonicalSnapshot.nodeResults,
625
+ waitResults: canonicalSnapshot.waitResults,
626
+ };
627
+ return normalizeFlowRunCheckpoint({
628
+ schemaVersion: 'depa.flow-run-checkpoint/v1',
629
+ instanceId: options.target.instanceId,
630
+ runId: options.target.runId,
631
+ definition: options.definition,
632
+ version: 0,
633
+ input: canonicalSnapshot.input,
634
+ config: options.config,
635
+ state: canonicalSnapshot.vars,
636
+ output: options.output ?? legacySnapshotOutput(canonicalSnapshot),
637
+ controllerSidecars: controllerSidecars as unknown as FlowClosedObject,
638
+ nodeSidecars: nodeSidecars as unknown as FlowClosedObject,
639
+ profile: {
640
+ kind: 'WorkCtrlFlow',
641
+ snapshot: canonicalSnapshot as unknown as FlowClosedObject,
642
+ },
643
+ });
644
+ }
645
+
646
+ function rebindLegacyOwnedIdentity(value: string, legacyTreeId: string, targetRunId: string): string {
647
+ if (value === legacyTreeId) return targetRunId;
648
+ if (value.startsWith(`${legacyTreeId}:`)) return `${targetRunId}${value.slice(legacyTreeId.length)}`;
649
+ const legacyWaitPrefix = `wh_${legacyTreeId}_`;
650
+ if (value.startsWith(legacyWaitPrefix)) return `wh_${targetRunId}_${value.slice(legacyWaitPrefix.length)}`;
651
+ return value;
652
+ }
653
+
654
+ function assertLegacyWorkCtrlSnapshotShape(snapshot: WorkCtrlFlowSnapshot): void {
655
+ if (snapshot.schemaVersion !== 'depa.flows.workflow/v4'
656
+ || typeof snapshot.treeId !== 'string'
657
+ || snapshot.treeId.length === 0
658
+ || snapshot.treeId !== snapshot.treeId.trim()
659
+ || typeof snapshot.fqn !== 'string'
660
+ || snapshot.fqn.length === 0
661
+ || snapshot.fqn !== snapshot.fqn.trim()
662
+ || (snapshot.status !== 'Waiting' && snapshot.status !== 'Completed' && snapshot.status !== 'Failed')) {
663
+ throw new LegacyWorkCtrlImportError(
664
+ 'legacy-work-ctrl-snapshot-invalid',
665
+ 'Legacy snapshot schema, identity, FQN or status is invalid.',
666
+ );
667
+ }
668
+ }
669
+
670
+ /** Runtime-first one-way import. Canonical checkpoints are never projected back to legacy files. */
671
+ export async function importLegacyWorkCtrlSnapshot(
672
+ runtime: LegacyWorkCtrlImportRuntime,
673
+ input: ImportLegacyWorkCtrlSnapshotInput,
674
+ ): Promise<LegacyWorkCtrlImportReceipt> {
675
+ const legacy = await runtime.legacyReader.read(input.legacyTreeId);
676
+ if (!legacy) {
677
+ throw new LegacyWorkCtrlImportError('legacy-work-ctrl-snapshot-missing', 'Legacy flat snapshot does not exist.');
678
+ }
679
+ const candidate = convertLegacyWorkCtrlSnapshotToCheckpoint(legacy.snapshot, input);
680
+ const receipt = normalizeLegacyWorkCtrlImportReceipt({
681
+ schemaVersion: 'depa.flow-legacy-import-receipt/v1',
682
+ legacyTreeId: input.legacyTreeId,
683
+ legacySnapshotDigest: legacy.identity.digest,
684
+ target: input.target,
685
+ definitionRevision: candidate.definition.revision,
686
+ definitionDigest: candidate.definition.digest,
687
+ acceptedCheckpointVersion: 0,
688
+ acceptedCheckpointDigest: digestStableValue(candidate),
689
+ });
690
+ const completed = await runtime.checkpointStore.readLegacyImportReceipt(input.target);
691
+ if (completed) {
692
+ if (stableJson(completed) !== stableJson(receipt)) {
693
+ throw new LegacyWorkCtrlImportConflictError('Existing completion receipt differs from this legacy import.');
694
+ }
695
+ return completed;
696
+ }
697
+
698
+ let canonical = await runtime.checkpointStore.load(input.target);
699
+ if (!canonical) {
700
+ try {
701
+ canonical = await runtime.checkpointStore.commit({ expectedVersion: null, checkpoint: candidate });
702
+ } catch (error) {
703
+ if (!(error instanceof FlowRunCheckpointVersionConflictError)) throw error;
704
+ canonical = await runtime.checkpointStore.load(input.target);
705
+ }
706
+ }
707
+ if (!canonical || stableJson(canonical) !== stableJson(candidate)) {
708
+ throw new LegacyWorkCtrlImportConflictError(
709
+ 'Target run already contains non-equivalent canonical checkpoint facts.',
710
+ );
711
+ }
712
+ return runtime.checkpointStore.recordLegacyImportReceipt(input.target, receipt);
713
+ }
714
+
715
+ function legacySnapshotOutput(snapshot: WorkCtrlFlowSnapshot): FlowClosedValue {
716
+ const execution = Object.getOwnPropertyDescriptor(snapshot.vars, '__flow_execution__');
717
+ if (!execution || !('value' in execution) || !isPlainRecord(execution.value)) return null;
718
+ const output = Object.getOwnPropertyDescriptor(execution.value, 'output');
719
+ return !output || !('value' in output) || output.value === undefined
720
+ ? null
721
+ : output.value as FlowClosedValue;
722
+ }
723
+
724
+ function normalizeLegacyWorkCtrlImportReceipt(value: unknown): LegacyWorkCtrlImportReceipt {
725
+ const record = exactPlainDataRecord(value, [
726
+ 'schemaVersion',
727
+ 'legacyTreeId',
728
+ 'legacySnapshotDigest',
729
+ 'target',
730
+ 'definitionRevision',
731
+ 'definitionDigest',
732
+ 'acceptedCheckpointVersion',
733
+ 'acceptedCheckpointDigest',
734
+ ], 'legacyReceipt');
735
+ if (record.schemaVersion !== 'depa.flow-legacy-import-receipt/v1') {
736
+ throw new LegacyWorkCtrlImportError('legacy-work-ctrl-receipt-invalid', 'Legacy receipt schema is invalid.');
737
+ }
738
+ const legacyTreeId = canonicalReceiptString(record.legacyTreeId, 'legacyTreeId');
739
+ const legacySnapshotDigest = receiptDigest(record.legacySnapshotDigest, 'legacySnapshotDigest');
740
+ const targetRecord = exactPlainDataRecord(record.target, ['instanceId', 'runId'], 'legacyReceipt.target');
741
+ const target = Object.freeze({
742
+ instanceId: canonicalReceiptString(targetRecord.instanceId, 'target.instanceId'),
743
+ runId: canonicalReceiptString(targetRecord.runId, 'target.runId'),
744
+ });
745
+ assertSafeSegment(target.instanceId);
746
+ assertSafeSegment(target.runId);
747
+ const definitionRevision = canonicalReceiptString(record.definitionRevision, 'definitionRevision');
748
+ const definitionDigest = receiptDigest(record.definitionDigest, 'definitionDigest');
749
+ if (record.acceptedCheckpointVersion !== 0) {
750
+ throw new LegacyWorkCtrlImportError('legacy-work-ctrl-receipt-invalid', 'Imported checkpoint version must be zero.');
751
+ }
752
+ const acceptedCheckpointDigest = receiptDigest(record.acceptedCheckpointDigest, 'acceptedCheckpointDigest');
753
+ return Object.freeze({
754
+ schemaVersion: 'depa.flow-legacy-import-receipt/v1',
755
+ legacyTreeId,
756
+ legacySnapshotDigest,
757
+ target,
758
+ definitionRevision,
759
+ definitionDigest,
760
+ acceptedCheckpointVersion: 0,
761
+ acceptedCheckpointDigest,
762
+ });
763
+ }
764
+
765
+ function exactPlainDataRecord(
766
+ value: unknown,
767
+ fields: readonly string[],
768
+ location: string,
769
+ ): Record<string, unknown> {
770
+ if (!isPlainRecord(value)) {
771
+ throw new LegacyWorkCtrlImportError('legacy-work-ctrl-receipt-invalid', `${location} must be a plain-data object.`);
772
+ }
773
+ const keys = Reflect.ownKeys(value);
774
+ if (keys.some((key) => typeof key !== 'string')) {
775
+ throw new LegacyWorkCtrlImportError('legacy-work-ctrl-receipt-invalid', `${location} must not contain symbols.`);
776
+ }
777
+ const expected = new Set(fields);
778
+ if (keys.length !== fields.length || keys.some((key) => !expected.has(key as string))) {
779
+ throw new LegacyWorkCtrlImportError('legacy-work-ctrl-receipt-invalid', `${location} fields differ from the closed receipt contract.`);
780
+ }
781
+ for (const field of fields) {
782
+ const descriptor = Object.getOwnPropertyDescriptor(value, field);
783
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
784
+ throw new LegacyWorkCtrlImportError('legacy-work-ctrl-receipt-invalid', `${location}.${field} must be a data property.`);
785
+ }
786
+ }
787
+ return value;
788
+ }
789
+
790
+ function canonicalReceiptString(value: unknown, field: string): string {
791
+ if (typeof value !== 'string' || value.length === 0 || value !== value.trim()) {
792
+ throw new LegacyWorkCtrlImportError('legacy-work-ctrl-receipt-invalid', `${field} must be canonical.`);
793
+ }
794
+ return value;
795
+ }
796
+
797
+ function receiptDigest(value: unknown, field: string): `sha256:${string}` {
798
+ if (typeof value !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(value)) {
799
+ throw new LegacyWorkCtrlImportError('legacy-work-ctrl-receipt-invalid', `${field} must be a sha256 digest.`);
800
+ }
801
+ return value as `sha256:${string}`;
802
+ }
803
+
804
+ function readLegacyImportReceiptFile(file: string, realRoot: string): LegacyWorkCtrlImportReceipt {
805
+ try {
806
+ return normalizeLegacyWorkCtrlImportReceipt(
807
+ readJsonFile(file, realRoot, 'legacy-work-ctrl-receipt-invalid'),
808
+ );
809
+ } catch (error) {
810
+ if (error instanceof LegacyWorkCtrlImportError || error instanceof FlowRunFilesystemStoreError) throw error;
811
+ throw new LegacyWorkCtrlImportError(
812
+ 'legacy-work-ctrl-receipt-invalid',
813
+ `Legacy import receipt is invalid: ${(error as Error).message}`,
814
+ );
815
+ }
816
+ }
817
+
818
+ function assertReceiptMatchesCheckpoint(
819
+ receipt: LegacyWorkCtrlImportReceipt,
820
+ checkpoint: AIWorkflowFlowRunCheckpoint,
821
+ ): void {
822
+ if (receipt.target.instanceId !== checkpoint.instanceId
823
+ || receipt.target.runId !== checkpoint.runId
824
+ || receipt.definitionRevision !== checkpoint.definition.revision
825
+ || receipt.definitionDigest !== checkpoint.definition.digest
826
+ || checkpoint.version < receipt.acceptedCheckpointVersion) {
827
+ throw new LegacyWorkCtrlImportConflictError('Legacy import receipt conflicts with canonical checkpoint identity.');
828
+ }
829
+ }
830
+
831
+ function readFileBytesNoFollow(file: string, realRoot: string, code: string): Buffer {
832
+ assertRegularFile(file, realRoot, code);
833
+ let fd: number | undefined;
834
+ try {
835
+ fd = fs.openSync(file, fs.constants.O_RDONLY | NO_FOLLOW);
836
+ const stat = fs.fstatSync(fd);
837
+ if (!stat.isFile()) throw new LegacyWorkCtrlImportError(code, 'Legacy snapshot must be a regular file.');
838
+ return fs.readFileSync(fd);
839
+ } catch (error) {
840
+ if (error instanceof LegacyWorkCtrlImportError || error instanceof FlowRunFilesystemStoreError) throw error;
841
+ throw new LegacyWorkCtrlImportError(code, `Legacy snapshot read failed: ${(error as Error).message}`);
842
+ } finally {
843
+ if (fd !== undefined) fs.closeSync(fd);
844
+ }
845
+ }
846
+
847
+ function digestBytes(bytes: Uint8Array): `sha256:${string}` {
848
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
849
+ }
850
+
851
+ function digestStableValue(value: unknown): `sha256:${string}` {
852
+ return digestBytes(Buffer.from(stableJson(value), 'utf8'));
853
+ }
854
+
855
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
856
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
857
+ const prototype = Object.getPrototypeOf(value);
858
+ return prototype === Object.prototype || prototype === null;
859
+ }
860
+
861
+ function assertSafeSegment(value: string): void {
862
+ if (typeof value !== 'string'
863
+ || value.length === 0
864
+ || value.length > 200
865
+ || value !== value.trim()
866
+ || value === '.'
867
+ || value === '..'
868
+ || value.endsWith('.')
869
+ || value.normalize('NFC') !== value
870
+ || /[<>:"|?*\\/\0\u0001-\u001f\u007f]/u.test(value)
871
+ || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(value)) {
872
+ throw new FlowRunFilesystemStoreError('flow-run-id-invalid', 'Flow instance and run ids must be safe canonical path segments.');
873
+ }
874
+ }
875
+
876
+ function containedPath(parent: string, segment: string): string {
877
+ const target = path.resolve(parent, segment);
878
+ const relative = path.relative(parent, target);
879
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
880
+ throw new FlowRunFilesystemStoreError('flow-run-boundary-invalid', 'Flow run path escapes its canonical parent.');
881
+ }
882
+ return target;
883
+ }
884
+
885
+ function assertExistingDirectory(file: string, realRoot: string | undefined, code: string): void {
886
+ let stat: fs.Stats;
887
+ try {
888
+ stat = fs.lstatSync(file);
889
+ } catch {
890
+ throw new FlowRunFilesystemStoreError(code, 'Required filesystem directory does not exist.');
891
+ }
892
+ if (stat.isSymbolicLink()) {
893
+ throw new FlowRunFilesystemStoreError(
894
+ code === 'flow-run-symlink-boundary' ? code : 'flow-run-symlink-boundary',
895
+ 'Filesystem boundary must not be a symbolic link.',
896
+ );
897
+ }
898
+ if (!stat.isDirectory()) throw new FlowRunFilesystemStoreError(code, 'Filesystem boundary must be a directory.');
899
+ if (realRoot !== undefined) assertRealContainment(realRoot, file, code);
900
+ }
901
+
902
+ function assertRegularFile(file: string, realRoot: string, code: string): void {
903
+ let stat: fs.Stats;
904
+ try {
905
+ stat = fs.lstatSync(file);
906
+ } catch {
907
+ throw new FlowRunFilesystemStoreError(code, 'Required filesystem file does not exist.');
908
+ }
909
+ if (stat.isSymbolicLink()) throw new FlowRunFilesystemStoreError('flow-run-symlink-boundary', 'Filesystem file must not be a symbolic link.');
910
+ if (!stat.isFile()) throw new FlowRunFilesystemStoreError(code, 'Filesystem entry must be a regular file.');
911
+ assertRealContainment(realRoot, file, code);
912
+ }
913
+
914
+ function assertRealContainment(realRoot: string, file: string, code: string): void {
915
+ const realFile = fs.realpathSync(file);
916
+ const relative = path.relative(realRoot, realFile);
917
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
918
+ throw new FlowRunFilesystemStoreError(code, 'Filesystem entry escapes the injected store root.');
919
+ }
920
+ }
921
+
922
+ function ensureChildDirectory(parent: string, child: string, realRoot: string): void {
923
+ assertExistingDirectory(parent, realRoot, 'flow-run-boundary-invalid');
924
+ if (fs.existsSync(child)) {
925
+ assertExistingDirectory(child, realRoot, 'flow-run-symlink-boundary');
926
+ return;
927
+ }
928
+ fs.mkdirSync(child);
929
+ assertExistingDirectory(child, realRoot, 'flow-run-boundary-invalid');
930
+ }
931
+
932
+ function readCheckpointFile(file: string, realRoot: string): AIWorkflowFlowRunCheckpoint {
933
+ const value = readJsonFile(file, realRoot, 'flow-run-checkpoint-invalid');
934
+ try {
935
+ return normalizeFlowRunCheckpoint(value);
936
+ } catch (error) {
937
+ throw new FlowRunFilesystemStoreError(
938
+ 'flow-run-checkpoint-invalid',
939
+ `Checkpoint does not satisfy the closed contract: ${(error as Error).message}`,
940
+ );
941
+ }
942
+ }
943
+
944
+ function readJsonFile(file: string, realRoot: string, code: string): unknown {
945
+ assertRegularFile(file, realRoot, code);
946
+ let fd: number | undefined;
947
+ try {
948
+ fd = fs.openSync(file, fs.constants.O_RDONLY | NO_FOLLOW);
949
+ const stat = fs.fstatSync(fd);
950
+ if (!stat.isFile()) throw new FlowRunFilesystemStoreError(code, 'Filesystem entry is not a regular file.');
951
+ return JSON.parse(fs.readFileSync(fd, 'utf8')) as unknown;
952
+ } catch (error) {
953
+ if (error instanceof FlowRunFilesystemStoreError) throw error;
954
+ throw new FlowRunFilesystemStoreError(code, `Filesystem JSON decode failed: ${(error as Error).message}`);
955
+ } finally {
956
+ if (fd !== undefined) fs.closeSync(fd);
957
+ }
958
+ }
959
+
960
+ function writeCheckpointCandidate(
961
+ file: string,
962
+ checkpoint: AIWorkflowFlowRunCheckpoint,
963
+ realRoot: string,
964
+ ): void {
965
+ if (fs.existsSync(file)) {
966
+ throw new FlowRunFilesystemStoreError('flow-run-staging-invalid', 'Checkpoint staging file already exists after recovery.');
967
+ }
968
+ assertExistingDirectory(path.dirname(file), realRoot, 'flow-run-boundary-invalid');
969
+ writeBytesNoFollow(file, stableJson(checkpoint));
970
+ }
971
+
972
+ function writeBytesNoFollow(file: string, bytes: string): void {
973
+ let fd: number | undefined;
974
+ try {
975
+ fd = fs.openSync(
976
+ file,
977
+ fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | NO_FOLLOW,
978
+ 0o600,
979
+ );
980
+ fs.writeFileSync(fd, bytes, 'utf8');
981
+ fs.fsyncSync(fd);
982
+ } finally {
983
+ if (fd !== undefined) fs.closeSync(fd);
984
+ }
985
+ }
986
+
987
+ function removeUncommittedStaging(file: string, parent: string, realRoot: string): void {
988
+ if (!fs.existsSync(file)) return;
989
+ assertExistingDirectory(parent, realRoot, 'flow-run-boundary-invalid');
990
+ assertRegularFile(file, realRoot, 'flow-run-staging-invalid');
991
+ fs.unlinkSync(file);
992
+ }
993
+
994
+ async function acquireLock(parent: string, file: string, realRoot: string): Promise<HeldLock> {
995
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
996
+ const recoveryFile = `${file}.recovery`;
997
+ for (;;) {
998
+ assertExistingDirectory(parent, realRoot, 'flow-run-boundary-invalid');
999
+ if (lockRecoveryInProgress(recoveryFile, parent, realRoot)) {
1000
+ if (Date.now() >= deadline) {
1001
+ throw new FlowRunFilesystemStoreError('flow-run-lock-timeout', 'Timed out waiting for canonical checkpoint lock recovery.');
1002
+ }
1003
+ await delay(LOCK_RETRY_MS);
1004
+ continue;
1005
+ }
1006
+ try {
1007
+ const held = createHeldLock(file);
1008
+ // A recovery owner may have established its barrier after our first
1009
+ // observation. Never publish an owner across that boundary.
1010
+ if (lockRecoveryInProgress(recoveryFile, parent, realRoot)) {
1011
+ releaseLock(held, parent, realRoot);
1012
+ await delay(LOCK_RETRY_MS);
1013
+ continue;
1014
+ }
1015
+ return held;
1016
+ } catch (error) {
1017
+ if (!isErrorCode(error, 'EEXIST')) throw error;
1018
+ if (tryClaimAbandonedLock(file, recoveryFile, parent, realRoot)) continue;
1019
+ if (Date.now() >= deadline) {
1020
+ throw new FlowRunFilesystemStoreError('flow-run-lock-timeout', 'Timed out waiting for the canonical checkpoint lock.');
1021
+ }
1022
+ await delay(LOCK_RETRY_MS);
1023
+ }
1024
+ }
1025
+ }
1026
+
1027
+ function createHeldLock(file: string): HeldLock {
1028
+ const token = randomUUID();
1029
+ const fd = fs.openSync(
1030
+ file,
1031
+ fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_RDWR | NO_FOLLOW,
1032
+ 0o600,
1033
+ );
1034
+ try {
1035
+ fs.writeFileSync(fd, `${JSON.stringify({ pid: process.pid, token, createdAtMs: Date.now() })}\n`, 'utf8');
1036
+ fs.fsyncSync(fd);
1037
+ const stat = fs.fstatSync(fd);
1038
+ if (!stat.isFile()) throw new FlowRunFilesystemStoreError('flow-run-lock-invalid', 'Canonical lock must be a regular file.');
1039
+ return { fd, file, token, device: stat.dev, inode: stat.ino };
1040
+ } catch (error) {
1041
+ fs.closeSync(fd);
1042
+ throw error;
1043
+ }
1044
+ }
1045
+
1046
+ function lockRecoveryInProgress(file: string, parent: string, realRoot: string): boolean {
1047
+ assertExistingDirectory(parent, realRoot, 'flow-run-boundary-invalid');
1048
+ const observed = observeLock(file, parent, realRoot);
1049
+ if (!observed) return false;
1050
+ if (observed.pid !== undefined && processIsAlive(observed.pid)) return true;
1051
+ const age = Date.now() - (observed.createdAtMs ?? observed.mtimeMs);
1052
+ if ((observed.pid === undefined || observed.token === undefined) && age <= ABANDONED_INCOMPLETE_LOCK_MS) {
1053
+ return true;
1054
+ }
1055
+ return !tryRecoverAbandonedBarrier(file, observed, parent, realRoot);
1056
+ }
1057
+
1058
+ function tryRecoverAbandonedBarrier(
1059
+ file: string,
1060
+ observed: LockObservation,
1061
+ parent: string,
1062
+ realRoot: string,
1063
+ ): boolean {
1064
+ const quarantineFile = `${file}.abandoned-${randomUUID()}`;
1065
+ try {
1066
+ fs.renameSync(file, quarantineFile);
1067
+ } catch (error) {
1068
+ if (isErrorCode(error, 'ENOENT')) return true;
1069
+ throw error;
1070
+ }
1071
+ const claimed = observeLock(quarantineFile, parent, realRoot);
1072
+ if (!claimed) return true;
1073
+ if (sameLockIdentity(claimed, observed)) {
1074
+ fs.unlinkSync(quarantineFile);
1075
+ return true;
1076
+ }
1077
+
1078
+ // The observed barrier was replaced between observation and rename. Restore
1079
+ // the replacement by an O_EXCL hard link; never unlink its canonical path.
1080
+ try {
1081
+ fs.linkSync(quarantineFile, file);
1082
+ } catch (error) {
1083
+ if (!isErrorCode(error, 'EEXIST')) throw error;
1084
+ }
1085
+ const stillQuarantined = observeLock(quarantineFile, parent, realRoot);
1086
+ if (stillQuarantined && sameLockIdentity(stillQuarantined, claimed)) fs.unlinkSync(quarantineFile);
1087
+ return false;
1088
+ }
1089
+
1090
+ function tryClaimAbandonedLock(
1091
+ file: string,
1092
+ recoveryFile: string,
1093
+ parent: string,
1094
+ realRoot: string,
1095
+ ): boolean {
1096
+ assertExistingDirectory(parent, realRoot, 'flow-run-boundary-invalid');
1097
+ const observed = observeLock(file, parent, realRoot);
1098
+ if (!observed) return true;
1099
+ if (observed.pid !== undefined && processIsAlive(observed.pid)) return false;
1100
+ if (observed.pid === undefined && Date.now() - observed.mtimeMs <= ABANDONED_INCOMPLETE_LOCK_MS) return false;
1101
+
1102
+ let recovery: HeldLock;
1103
+ try {
1104
+ recovery = createHeldLock(recoveryFile);
1105
+ } catch (error) {
1106
+ if (isErrorCode(error, 'EEXIST')) return false;
1107
+ throw error;
1108
+ }
1109
+ const quarantineFile = `${file}.abandoned-${recovery.token}`;
1110
+ try {
1111
+ const current = observeLock(file, parent, realRoot);
1112
+ if (!current) return true;
1113
+ if (!sameLockIdentity(current, observed)) return false;
1114
+ if (!heldLockStillOwnsPath(recovery, parent, realRoot)) return false;
1115
+ fs.renameSync(file, quarantineFile);
1116
+ const claimed = observeLock(quarantineFile, parent, realRoot);
1117
+ if (!claimed || !sameLockIdentity(claimed, observed)) {
1118
+ throw new FlowRunFilesystemStoreError(
1119
+ 'flow-run-lock-claim-invalid',
1120
+ 'Abandoned lock atomic claim did not preserve the observed owner identity.',
1121
+ );
1122
+ }
1123
+ fs.unlinkSync(quarantineFile);
1124
+ return true;
1125
+ } finally {
1126
+ releaseLock(recovery, parent, realRoot);
1127
+ }
1128
+ }
1129
+
1130
+ function observeLock(file: string, parent: string, realRoot: string): LockObservation | undefined {
1131
+ assertExistingDirectory(parent, realRoot, 'flow-run-boundary-invalid');
1132
+ let fd: number | undefined;
1133
+ try {
1134
+ fd = fs.openSync(file, fs.constants.O_RDONLY | NO_FOLLOW);
1135
+ } catch (error) {
1136
+ if (isErrorCode(error, 'ENOENT')) return undefined;
1137
+ throw error;
1138
+ }
1139
+ try {
1140
+ const stat = fs.fstatSync(fd);
1141
+ if (!stat.isFile()) throw new FlowRunFilesystemStoreError('flow-run-lock-invalid', 'Canonical lock must be a regular file.');
1142
+ let value: unknown;
1143
+ try {
1144
+ value = JSON.parse(fs.readFileSync(fd, 'utf8')) as unknown;
1145
+ } catch {
1146
+ value = undefined;
1147
+ }
1148
+ const record = typeof value === 'object' && value !== null
1149
+ ? value as { readonly pid?: unknown; readonly token?: unknown; readonly createdAtMs?: unknown }
1150
+ : undefined;
1151
+ return {
1152
+ device: stat.dev,
1153
+ inode: stat.ino,
1154
+ mtimeMs: stat.mtimeMs,
1155
+ pid: record && Number.isSafeInteger(record.pid) && (record.pid as number) > 0
1156
+ ? record.pid as number
1157
+ : undefined,
1158
+ token: record && typeof record.token === 'string' && record.token.length > 0
1159
+ ? record.token
1160
+ : undefined,
1161
+ createdAtMs: record && Number.isSafeInteger(record.createdAtMs) && (record.createdAtMs as number) >= 0
1162
+ ? record.createdAtMs as number
1163
+ : undefined,
1164
+ };
1165
+ } finally {
1166
+ fs.closeSync(fd);
1167
+ }
1168
+ }
1169
+
1170
+ function sameLockIdentity(left: LockObservation, right: LockObservation): boolean {
1171
+ return left.device === right.device
1172
+ && left.inode === right.inode
1173
+ && left.token === right.token;
1174
+ }
1175
+
1176
+ function processIsAlive(pid: number): boolean {
1177
+ try {
1178
+ process.kill(pid, 0);
1179
+ return true;
1180
+ } catch (error) {
1181
+ return isErrorCode(error, 'EPERM');
1182
+ }
1183
+ }
1184
+
1185
+ function heldLockStillOwnsPath(lock: HeldLock, parent: string, realRoot: string): boolean {
1186
+ const current = observeLock(lock.file, parent, realRoot);
1187
+ return current !== undefined
1188
+ && current.device === lock.device
1189
+ && current.inode === lock.inode
1190
+ && current.token === lock.token;
1191
+ }
1192
+
1193
+ function releaseLock(lock: HeldLock, parent: string, realRoot: string): void {
1194
+ fs.closeSync(lock.fd);
1195
+ if (!heldLockStillOwnsPath(lock, parent, realRoot)) return;
1196
+ fs.unlinkSync(lock.file);
1197
+ }
1198
+
1199
+ function readDirectoryCodeUnitOrdered(directory: string): fs.Dirent[] {
1200
+ return fs.readdirSync(directory, { withFileTypes: true })
1201
+ .sort((left, right) => compareCodeUnits(left.name, right.name));
1202
+ }
1203
+
1204
+ function compareIndexEntries(left: FlowRunIndexEntry, right: FlowRunIndexEntry): number {
1205
+ const instance = compareCodeUnits(left.instanceId, right.instanceId);
1206
+ return instance === 0 ? compareCodeUnits(left.runId, right.runId) : instance;
1207
+ }
1208
+
1209
+ function compareCodeUnits(left: string, right: string): number {
1210
+ return left < right ? -1 : left > right ? 1 : 0;
1211
+ }
1212
+
1213
+ function stableJson(value: unknown): string {
1214
+ return `${JSON.stringify(value, null, 2)}\n`;
1215
+ }
1216
+
1217
+ function isErrorCode(error: unknown, code: string): boolean {
1218
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
1219
+ }
1220
+
1221
+ function delay(ms: number): Promise<void> {
1222
+ return new Promise((resolve) => setTimeout(resolve, ms));
1223
+ }