newmark-agent 0.6.1 → 0.6.3

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,989 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.GuardedContinuationStore = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const crypto_1 = require("crypto");
40
+ const contracts_1 = require("./contracts");
41
+ /**
42
+ * Single-writer continuation ledger for one workspace.
43
+ *
44
+ * The spec's reference uses SQLite. This implementation keeps the same
45
+ * boundaries with a lock-file + atomic-replace JSON ledger because the project
46
+ * does not ship a native SQLite module and an Electron native dependency would
47
+ * change the packaging/signing contract. All continuation writes must go
48
+ * through this class; callers never mutate the ledger directly.
49
+ */
50
+ class GuardedContinuationStore {
51
+ workspacePath;
52
+ workspaceId;
53
+ ledgerPath;
54
+ lockPath;
55
+ leaseMs;
56
+ eventRetention;
57
+ fingerprint = '';
58
+ cached = null;
59
+ constructor(workspacePath, workspaceId, options = {}) {
60
+ this.workspacePath = workspacePath;
61
+ this.workspaceId = workspaceId;
62
+ this.ledgerPath = path.join(path.resolve(workspacePath), 'conversations', 'continuation-ledger.json');
63
+ this.lockPath = `${this.ledgerPath}.lock`;
64
+ this.leaseMs = Math.max(5_000, Math.floor(options.leaseMs || 60_000));
65
+ this.eventRetention = Math.max(200, Math.floor(options.eventRetention || 5_000));
66
+ }
67
+ get filePath() {
68
+ return this.ledgerPath;
69
+ }
70
+ ensureRoot(rootId, defaultBranchId) {
71
+ this.mutate(ledger => {
72
+ if (!ledger.roots[rootId]) {
73
+ ledger.roots[rootId] = {
74
+ rootId,
75
+ workspaceId: this.workspaceId,
76
+ defaultBranchId,
77
+ createdAt: new Date().toISOString(),
78
+ };
79
+ }
80
+ this.ensureBranchRecord(ledger, { rootId, branchId: defaultBranchId, parentAnchorBuildId: null });
81
+ });
82
+ }
83
+ ensureBranch(input) {
84
+ return this.mutate(ledger => this.ensureBranchRecord(ledger, input));
85
+ }
86
+ getReceipt(actorId, commandId) {
87
+ return this.readLedger().commands[(0, contracts_1.commandKey)(actorId, commandId)];
88
+ }
89
+ enqueueBuild(command) {
90
+ if (!command.target?.workspaceId || !command.target.expectedRootId || !command.target.branchId) {
91
+ throw new contracts_1.ContinuationError('TARGET_REQUIRED', 'workspaceId, expectedRootId and branchId are required');
92
+ }
93
+ if (command.target.workspaceId !== this.workspaceId) {
94
+ throw new contracts_1.ContinuationError('ROUTE_MISMATCH', `Command workspace ${command.target.workspaceId} does not match ledger workspace ${this.workspaceId}`);
95
+ }
96
+ const hash = (0, contracts_1.requestHash)(command);
97
+ return this.mutate(ledger => {
98
+ const key = (0, contracts_1.commandKey)(command.actorId, command.commandId);
99
+ const existing = ledger.commands[key];
100
+ if (existing) {
101
+ if (existing.requestHash !== hash) {
102
+ throw new contracts_1.ContinuationError('IDEMPOTENCY_KEY_REUSE', `Command ${command.commandId} was already used with a different request`, { commandId: command.commandId });
103
+ }
104
+ return { ...existing, duplicate: true };
105
+ }
106
+ const branch = this.requireBranch(ledger, {
107
+ workspaceId: command.target.workspaceId,
108
+ rootId: command.target.expectedRootId,
109
+ branchId: command.target.branchId,
110
+ });
111
+ if (branch.queueRevision !== command.expectedQueueRevision) {
112
+ throw new contracts_1.ContinuationError('QUEUE_REVISION_CONFLICT', `Queue revision ${command.expectedQueueRevision} is stale; authoritative revision is ${branch.queueRevision}`, {
113
+ expected: command.expectedQueueRevision,
114
+ actual: branch.queueRevision,
115
+ });
116
+ }
117
+ if ((branch.tailBuildId || null) !== (command.expectedTailBuildId || null)) {
118
+ throw new contracts_1.ContinuationError('QUEUE_REVISION_CONFLICT', `Expected tail ${command.expectedTailBuildId || 'null'} does not match authoritative tail ${branch.tailBuildId || 'null'}`, {
119
+ expected: command.expectedTailBuildId,
120
+ actual: branch.tailBuildId,
121
+ });
122
+ }
123
+ const parentBuildId = branch.tailBuildId;
124
+ const parent = parentBuildId ? ledger.builds[parentBuildId] : null;
125
+ if (parentBuildId && (!parent || parent.branchId !== branch.branchId)) {
126
+ throw new contracts_1.ContinuationError('ROUTE_MISMATCH', `Branch tail ${parentBuildId} is not owned by branch ${branch.branchId}`);
127
+ }
128
+ const buildId = `build-${(0, crypto_1.randomUUID)()}`;
129
+ const queueSequence = ledger.nextQueueSequence++;
130
+ const now = new Date().toISOString();
131
+ const build = {
132
+ buildId,
133
+ workspaceId: this.workspaceId,
134
+ rootId: branch.rootId,
135
+ branchId: branch.branchId,
136
+ parentBuildId,
137
+ inputHash: (0, contracts_1.buildInputHash)(command.input),
138
+ input: cloneInput(command.input),
139
+ recipeSnapshotId: command.recipeSnapshotId,
140
+ status: 'QUEUED',
141
+ queueSequence,
142
+ createdAt: now,
143
+ updatedAt: now,
144
+ };
145
+ ledger.builds[buildId] = build;
146
+ branch.tailBuildId = buildId;
147
+ branch.queueRevision += 1;
148
+ branch.lifecycleRevision += 1;
149
+ branch.updatedAt = now;
150
+ const receipt = {
151
+ schemaVersion: 1,
152
+ commandId: command.commandId,
153
+ requestHash: hash,
154
+ workspaceId: this.workspaceId,
155
+ rootId: branch.rootId,
156
+ branchId: branch.branchId,
157
+ buildId,
158
+ parentBuildId,
159
+ queueSequence,
160
+ queueRevision: branch.queueRevision,
161
+ lifecycleRevision: branch.lifecycleRevision,
162
+ acceptedAt: now,
163
+ };
164
+ ledger.commands[key] = receipt;
165
+ this.appendEvent(ledger, {
166
+ type: 'BuildAccepted',
167
+ workspaceId: this.workspaceId,
168
+ rootId: branch.rootId,
169
+ branchId: branch.branchId,
170
+ buildId,
171
+ attemptId: null,
172
+ fence: null,
173
+ payload: { parentBuildId, queueSequence, inputHash: build.inputHash },
174
+ });
175
+ return receipt;
176
+ });
177
+ }
178
+ claimBuild(input) {
179
+ return this.mutate(ledger => {
180
+ const branch = this.requireBranch(ledger, input);
181
+ if (branch.paused)
182
+ throw new contracts_1.ContinuationError('BRANCH_PAUSED', `Branch ${branch.branchId} is paused`);
183
+ const now = input.now ?? Date.now();
184
+ const guard = this.ensureGuard(ledger, branch.branchId);
185
+ if (guard.activeBuildId && guard.leaseUntil > now) {
186
+ return null;
187
+ }
188
+ if (guard.activeBuildId)
189
+ this.releaseExpiredGuard(ledger, branch, guard, now);
190
+ const candidate = Object.values(ledger.builds)
191
+ .filter(build => build.branchId === branch.branchId && build.status === 'QUEUED')
192
+ .sort((a, b) => a.queueSequence - b.queueSequence)
193
+ .find(build => {
194
+ if ((branch.headBuildId || null) !== (build.parentBuildId || null))
195
+ return false;
196
+ const parent = build.parentBuildId ? ledger.builds[build.parentBuildId] : null;
197
+ return !parent || parent.status === 'SUCCEEDED';
198
+ });
199
+ if (!candidate)
200
+ return null;
201
+ const attemptId = `attempt-${(0, crypto_1.randomUUID)()}`;
202
+ const fence = guard.fence + 1;
203
+ const leaseUntil = now + this.leaseMs;
204
+ const attempt = {
205
+ attemptId,
206
+ buildId: candidate.buildId,
207
+ branchId: branch.branchId,
208
+ fence,
209
+ status: 'RUNNING',
210
+ leaseUntil,
211
+ workerCapability: String(input.workerCapability || 'unknown'),
212
+ startedAt: new Date(now).toISOString(),
213
+ };
214
+ ledger.attempts[attemptId] = attempt;
215
+ ledger.guards[branch.branchId] = {
216
+ branchId: branch.branchId,
217
+ activeBuildId: candidate.buildId,
218
+ activeAttemptId: attemptId,
219
+ fence,
220
+ leaseUntil,
221
+ workerCapability: attempt.workerCapability,
222
+ };
223
+ candidate.status = 'RUNNING';
224
+ candidate.updatedAt = attempt.startedAt;
225
+ candidate.waitingReason = undefined;
226
+ branch.lifecycleRevision += 1;
227
+ branch.updatedAt = attempt.startedAt;
228
+ this.appendEvent(ledger, {
229
+ type: 'AttemptStarted',
230
+ workspaceId: this.workspaceId,
231
+ rootId: branch.rootId,
232
+ branchId: branch.branchId,
233
+ buildId: candidate.buildId,
234
+ attemptId,
235
+ fence,
236
+ payload: { parentBuildId: candidate.parentBuildId, workerCapability: attempt.workerCapability },
237
+ });
238
+ return {
239
+ workspaceId: this.workspaceId,
240
+ rootId: branch.rootId,
241
+ branchId: branch.branchId,
242
+ buildId: candidate.buildId,
243
+ attemptId,
244
+ fence,
245
+ parentBuildId: candidate.parentBuildId,
246
+ baseContextSnapshotId: candidate.recipeSnapshotId,
247
+ environmentSnapshotId: candidate.recipeSnapshotId,
248
+ workerCapability: attempt.workerCapability,
249
+ };
250
+ });
251
+ }
252
+ updateQueuedBuild(input) {
253
+ return this.mutate(ledger => {
254
+ const build = ledger.builds[input.buildId];
255
+ if (!build)
256
+ throw new contracts_1.ContinuationError('UNKNOWN_BUILD', `Build ${input.buildId} does not exist`);
257
+ if (build.workspaceId !== this.workspaceId || (input.rootId && build.rootId !== input.rootId)) {
258
+ throw new contracts_1.ContinuationError('ROUTE_MISMATCH', `Build ${input.buildId} does not belong to workspace/root ${this.workspaceId}/${input.rootId}`);
259
+ }
260
+ const branch = ledger.branches[build.branchId];
261
+ if (!branch)
262
+ throw new contracts_1.ContinuationError('UNKNOWN_BRANCH', `Build ${input.buildId} references missing branch ${build.branchId}`);
263
+ if (build.status !== 'QUEUED')
264
+ throw new contracts_1.ContinuationError('BUILD_NOT_READY', `Build ${build.buildId} is ${build.status}; only QUEUED builds can be edited`);
265
+ build.input = cloneInput(input.input);
266
+ build.inputHash = (0, contracts_1.buildInputHash)(build.input);
267
+ build.updatedAt = new Date().toISOString();
268
+ branch.queueRevision += 1;
269
+ branch.lifecycleRevision += 1;
270
+ branch.updatedAt = build.updatedAt;
271
+ return { ...build, input: cloneInput(build.input) };
272
+ });
273
+ }
274
+ deleteQueuedBuild(input) {
275
+ this.mutate(ledger => {
276
+ const build = ledger.builds[input.buildId];
277
+ if (!build)
278
+ throw new contracts_1.ContinuationError('UNKNOWN_BUILD', `Build ${input.buildId} does not exist`);
279
+ if (build.workspaceId !== this.workspaceId || (input.rootId && build.rootId !== input.rootId)) {
280
+ throw new contracts_1.ContinuationError('ROUTE_MISMATCH', `Build ${input.buildId} does not belong to workspace/root ${this.workspaceId}/${input.rootId}`);
281
+ }
282
+ const branch = ledger.branches[build.branchId];
283
+ if (!branch)
284
+ throw new contracts_1.ContinuationError('UNKNOWN_BRANCH', `Build ${input.buildId} references missing branch ${build.branchId}`);
285
+ if (build.status !== 'QUEUED')
286
+ throw new contracts_1.ContinuationError('BUILD_NOT_READY', `Build ${build.buildId} is ${build.status}; only QUEUED builds can be deleted`);
287
+ if (Object.values(ledger.builds).some(item => item.parentBuildId === build.buildId)) {
288
+ throw new contracts_1.ContinuationError('ANCHOR_NOT_COMMITTED', `Build ${build.buildId} still has queued successors; delete them from the tail first`);
289
+ }
290
+ if (branch.tailBuildId === build.buildId)
291
+ branch.tailBuildId = build.parentBuildId;
292
+ delete ledger.builds[build.buildId];
293
+ branch.queueRevision += 1;
294
+ branch.lifecycleRevision += 1;
295
+ branch.updatedAt = new Date().toISOString();
296
+ });
297
+ }
298
+ reorderQueuedBuilds(input) {
299
+ return this.mutate(ledger => {
300
+ const branch = this.requireBranch(ledger, input);
301
+ const queued = Object.values(ledger.builds)
302
+ .filter(build => build.branchId === branch.branchId && build.status === 'QUEUED')
303
+ .sort((a, b) => a.queueSequence - b.queueSequence);
304
+ // continuation-v1 fixes parentBuildId at admission. Reordering admitted
305
+ // builds would silently change the context chain, so it is rejected.
306
+ // Changing the order requires explicit cancel + new commands.
307
+ throw new contracts_1.ContinuationError('ANCHOR_NOT_COMMITTED', `Branch ${branch.branchId} has ${queued.length} admitted queued build(s); reordering would change their fixed parents. Cancel the suffix and submit it as new commands.`, { branchId: branch.branchId, queuedBuildIds: queued.map(build => build.buildId) });
308
+ });
309
+ }
310
+ heartbeat(scope, now = Date.now()) {
311
+ return this.mutate(ledger => {
312
+ const guard = ledger.guards[scope.branchId];
313
+ const attempt = ledger.attempts[scope.attemptId];
314
+ if (!guard || !attempt
315
+ || guard.activeBuildId !== scope.buildId
316
+ || guard.activeAttemptId !== scope.attemptId
317
+ || guard.fence !== scope.fence
318
+ || attempt.fence !== scope.fence
319
+ || attempt.status !== 'RUNNING'
320
+ || guard.leaseUntil <= now) {
321
+ this.appendEvent(ledger, {
322
+ type: 'StaleWriteRejected',
323
+ workspaceId: this.workspaceId,
324
+ rootId: ledger.builds[scope.buildId]?.rootId || '',
325
+ branchId: scope.branchId,
326
+ buildId: scope.buildId,
327
+ attemptId: scope.attemptId,
328
+ fence: scope.fence,
329
+ payload: { operation: 'heartbeat' },
330
+ });
331
+ return false;
332
+ }
333
+ const leaseUntil = now + this.leaseMs;
334
+ attempt.leaseUntil = leaseUntil;
335
+ guard.leaseUntil = leaseUntil;
336
+ this.appendEvent(ledger, {
337
+ type: 'AttemptHeartbeat',
338
+ workspaceId: this.workspaceId,
339
+ rootId: ledger.builds[scope.buildId]?.rootId || '',
340
+ branchId: scope.branchId,
341
+ buildId: scope.buildId,
342
+ attemptId: scope.attemptId,
343
+ fence: scope.fence,
344
+ payload: { leaseUntil },
345
+ });
346
+ return true;
347
+ });
348
+ }
349
+ commitFinal(input) {
350
+ return this.mutate(ledger => {
351
+ const { scope } = input;
352
+ const branch = this.requireBranch(ledger, scope);
353
+ const build = ledger.builds[scope.buildId];
354
+ const attempt = ledger.attempts[scope.attemptId];
355
+ const guard = ledger.guards[scope.branchId];
356
+ const now = input.now ?? Date.now();
357
+ if (!build || build.branchId !== scope.branchId || build.rootId !== scope.rootId) {
358
+ throw new contracts_1.ContinuationError('UNKNOWN_BUILD', `Build ${scope.buildId} does not belong to branch ${scope.branchId}`);
359
+ }
360
+ if (ledger.finals[build.buildId]) {
361
+ throw new contracts_1.ContinuationError('FINAL_ALREADY_COMMITTED', `Build ${build.buildId} already has an immutable final`, { final: ledger.finals[build.buildId] });
362
+ }
363
+ if (!attempt || !guard
364
+ || guard.activeBuildId !== scope.buildId
365
+ || guard.activeAttemptId !== scope.attemptId
366
+ || guard.fence !== scope.fence
367
+ || attempt.fence !== scope.fence
368
+ || attempt.status !== 'RUNNING'
369
+ || guard.leaseUntil <= now) {
370
+ this.appendEvent(ledger, {
371
+ type: 'StaleWriteRejected',
372
+ workspaceId: this.workspaceId,
373
+ rootId: branch.rootId,
374
+ branchId: branch.branchId,
375
+ buildId: build.buildId,
376
+ attemptId: scope.attemptId,
377
+ fence: scope.fence,
378
+ payload: { operation: 'commit_final' },
379
+ });
380
+ throw new contracts_1.ContinuationError('STALE_EXECUTION', `Attempt ${scope.attemptId} no longer owns branch ${branch.branchId}`);
381
+ }
382
+ if ((branch.headBuildId || null) !== (build.parentBuildId || null)) {
383
+ throw new contracts_1.ContinuationError('ANCHOR_NOT_COMMITTED', `Build ${build.buildId} parent ${build.parentBuildId || 'null'} is not the current head ${branch.headBuildId || 'null'}`);
384
+ }
385
+ const final = {
386
+ finalId: `final-${(0, crypto_1.randomUUID)()}`,
387
+ buildId: build.buildId,
388
+ branchId: branch.branchId,
389
+ finalHash: String(input.finalHash || (0, crypto_1.createHash)('sha256').update(String(input.text || '')).digest('hex').slice(0, 24)),
390
+ stateSnapshotId: String(input.stateSnapshotId || build.recipeSnapshotId),
391
+ text: String(input.text || ''),
392
+ committedAt: new Date(now).toISOString(),
393
+ };
394
+ ledger.finals[build.buildId] = final;
395
+ build.status = 'SUCCEEDED';
396
+ build.finalId = final.finalId;
397
+ build.updatedAt = final.committedAt;
398
+ attempt.status = 'COMPLETED';
399
+ attempt.endedAt = final.committedAt;
400
+ branch.headBuildId = build.buildId;
401
+ branch.lifecycleRevision += 1;
402
+ branch.updatedAt = final.committedAt;
403
+ this.releaseGuard(ledger, branch.branchId, attempt.fence);
404
+ this.appendEvent(ledger, {
405
+ type: 'BuildCommitted',
406
+ workspaceId: this.workspaceId,
407
+ rootId: branch.rootId,
408
+ branchId: branch.branchId,
409
+ buildId: build.buildId,
410
+ attemptId: attempt.attemptId,
411
+ fence: attempt.fence,
412
+ payload: { finalId: final.finalId, finalHash: final.finalHash, headBuildId: branch.headBuildId },
413
+ });
414
+ return final;
415
+ });
416
+ }
417
+ /**
418
+ * Trusted external-owner completion.
419
+ *
420
+ * A Flow is a kernel-owned external run, not a leased model worker. It still
421
+ * has to be a durable build so queued Goal/Next successors have a committed
422
+ * parent. This path validates the parent/head relation transactionally but
423
+ * does not require a BranchGuard attempt; the kernel owns the Flow lifetime.
424
+ * If the build is not the next ready build, it stays QUEUED and the caller
425
+ * must retry after the predecessor commits.
426
+ */
427
+ commitExternalBuild(input) {
428
+ return this.mutate(ledger => {
429
+ const branch = this.requireBranch(ledger, input);
430
+ const build = ledger.builds[input.buildId];
431
+ if (!build || build.branchId !== branch.branchId)
432
+ throw new contracts_1.ContinuationError('UNKNOWN_BUILD', `Build ${input.buildId} is not in branch ${branch.branchId}`);
433
+ if (build.status === 'SUCCEEDED')
434
+ return true;
435
+ if (build.status !== 'QUEUED')
436
+ throw new contracts_1.ContinuationError('BUILD_NOT_READY', `External build ${build.buildId} is ${build.status}`);
437
+ if ((branch.headBuildId || null) !== (build.parentBuildId || null))
438
+ return false;
439
+ const now = input.now ?? Date.now();
440
+ const committedAt = new Date(now).toISOString();
441
+ const final = {
442
+ finalId: `final-${(0, crypto_1.randomUUID)()}`,
443
+ buildId: build.buildId,
444
+ branchId: branch.branchId,
445
+ finalHash: (0, crypto_1.createHash)('sha256').update(String(input.text || '')).digest('hex').slice(0, 24),
446
+ stateSnapshotId: build.recipeSnapshotId,
447
+ text: String(input.text || ''),
448
+ committedAt,
449
+ };
450
+ ledger.finals[build.buildId] = final;
451
+ build.status = 'SUCCEEDED';
452
+ build.finalId = final.finalId;
453
+ build.updatedAt = committedAt;
454
+ branch.headBuildId = build.buildId;
455
+ branch.lifecycleRevision += 1;
456
+ branch.updatedAt = committedAt;
457
+ this.appendEvent(ledger, {
458
+ type: 'BuildCommitted',
459
+ workspaceId: this.workspaceId,
460
+ rootId: branch.rootId,
461
+ branchId: branch.branchId,
462
+ buildId: build.buildId,
463
+ attemptId: null,
464
+ fence: null,
465
+ payload: { finalId: final.finalId, finalHash: final.finalHash, externalOwner: true, headBuildId: branch.headBuildId },
466
+ });
467
+ return true;
468
+ });
469
+ }
470
+ /**
471
+ * Claim a new attempt for an existing external Flow build (suspend/resume).
472
+ * The build identity and fixed parent are preserved; only the attempt/fence
473
+ * advance. This is the external-owner equivalent of retryBuild.
474
+ */
475
+ claimExternalBuild(input) {
476
+ return this.mutate(ledger => {
477
+ const branch = this.requireBranch(ledger, input);
478
+ const build = ledger.builds[input.buildId];
479
+ if (!build || build.branchId !== branch.branchId)
480
+ throw new contracts_1.ContinuationError('UNKNOWN_BUILD', `Build ${input.buildId} is not in branch ${branch.branchId}`);
481
+ if (build.status === 'SUCCEEDED')
482
+ throw new contracts_1.ContinuationError('FINAL_ALREADY_COMMITTED', `Build ${build.buildId} already committed`);
483
+ if (build.status !== 'QUEUED')
484
+ throw new contracts_1.ContinuationError('BUILD_NOT_READY', `External build ${build.buildId} is ${build.status}`);
485
+ if ((branch.headBuildId || null) !== (build.parentBuildId || null)) {
486
+ throw new contracts_1.ContinuationError('ANCHOR_NOT_COMMITTED', `External build ${build.buildId} parent is not the current head`);
487
+ }
488
+ const now = input.now ?? Date.now();
489
+ const guard = this.ensureGuard(ledger, branch.branchId);
490
+ const attemptId = `attempt-${(0, crypto_1.randomUUID)()}`;
491
+ const fence = guard.fence + 1;
492
+ const leaseUntil = now + this.leaseMs;
493
+ const attempt = {
494
+ attemptId,
495
+ buildId: build.buildId,
496
+ branchId: branch.branchId,
497
+ fence,
498
+ status: 'RUNNING',
499
+ leaseUntil,
500
+ workerCapability: String(input.workerCapability || 'flow-external'),
501
+ startedAt: new Date(now).toISOString(),
502
+ };
503
+ ledger.attempts[attemptId] = attempt;
504
+ ledger.guards[branch.branchId] = {
505
+ branchId: branch.branchId,
506
+ activeBuildId: build.buildId,
507
+ activeAttemptId: attemptId,
508
+ fence,
509
+ leaseUntil,
510
+ workerCapability: attempt.workerCapability,
511
+ };
512
+ build.status = 'RUNNING';
513
+ build.updatedAt = attempt.startedAt;
514
+ branch.lifecycleRevision += 1;
515
+ this.appendEvent(ledger, {
516
+ type: 'AttemptStarted',
517
+ workspaceId: this.workspaceId,
518
+ rootId: branch.rootId,
519
+ branchId: branch.branchId,
520
+ buildId: build.buildId,
521
+ attemptId,
522
+ fence,
523
+ payload: { externalOwner: true, workerCapability: attempt.workerCapability },
524
+ });
525
+ return {
526
+ workspaceId: this.workspaceId,
527
+ rootId: branch.rootId,
528
+ branchId: branch.branchId,
529
+ buildId: build.buildId,
530
+ attemptId,
531
+ fence,
532
+ parentBuildId: build.parentBuildId,
533
+ baseContextSnapshotId: build.recipeSnapshotId,
534
+ environmentSnapshotId: build.recipeSnapshotId,
535
+ workerCapability: attempt.workerCapability,
536
+ };
537
+ });
538
+ }
539
+ failBuild(input) {
540
+ this.terminateAttempt(input.scope, 'FAILED', input.reason, input.now);
541
+ }
542
+ cancelBuild(input) {
543
+ this.mutate(ledger => {
544
+ const branch = this.requireBranch(ledger, input);
545
+ const build = ledger.builds[input.buildId];
546
+ if (!build || build.branchId !== branch.branchId)
547
+ throw new contracts_1.ContinuationError('UNKNOWN_BUILD', `Build ${input.buildId} is not in branch ${branch.branchId}`);
548
+ if (build.status === 'SUCCEEDED')
549
+ throw new contracts_1.ContinuationError('FINAL_ALREADY_COMMITTED', `Build ${input.buildId} already committed`);
550
+ const guard = this.ensureGuard(ledger, branch.branchId);
551
+ if (guard.activeBuildId === build.buildId) {
552
+ const attempt = guard.activeAttemptId ? ledger.attempts[guard.activeAttemptId] : undefined;
553
+ if (attempt) {
554
+ attempt.status = 'CANCELLED';
555
+ attempt.endedAt = new Date(input.now ?? Date.now()).toISOString();
556
+ }
557
+ this.releaseGuard(ledger, branch.branchId, guard.fence);
558
+ }
559
+ build.status = 'CANCELLED';
560
+ build.updatedAt = new Date(input.now ?? Date.now()).toISOString();
561
+ branch.lifecycleRevision += 1;
562
+ this.markDependentsBlocked(ledger, branch, build.buildId);
563
+ this.appendEvent(ledger, {
564
+ type: 'BuildCancelled',
565
+ workspaceId: this.workspaceId,
566
+ rootId: branch.rootId,
567
+ branchId: branch.branchId,
568
+ buildId: build.buildId,
569
+ attemptId: null,
570
+ fence: null,
571
+ });
572
+ });
573
+ }
574
+ retryBuild(input) {
575
+ return this.mutate(ledger => {
576
+ const branch = this.requireBranch(ledger, input);
577
+ const build = ledger.builds[input.buildId];
578
+ if (!build || build.branchId !== branch.branchId)
579
+ throw new contracts_1.ContinuationError('UNKNOWN_BUILD', `Build ${input.buildId} is not in branch ${branch.branchId}`);
580
+ if (build.status !== 'FAILED' && build.status !== 'CANCELLED') {
581
+ throw new contracts_1.ContinuationError('BUILD_NOT_READY', `Build ${build.buildId} is ${build.status}; only FAILED/CANCELLED builds can be retried`);
582
+ }
583
+ if (this.hasCommittedDescendant(ledger, build.buildId)) {
584
+ throw new contracts_1.ContinuationError('ANCHOR_NOT_COMMITTED', `Build ${build.buildId} already has a committed descendant`);
585
+ }
586
+ build.status = 'QUEUED';
587
+ build.updatedAt = new Date().toISOString();
588
+ branch.lifecycleRevision += 1;
589
+ return { ...build, input: cloneInput(build.input) };
590
+ });
591
+ }
592
+ /**
593
+ * Idempotent failure recovery for an input that was never accepted.
594
+ *
595
+ * Stop/archive may release the branch guard before the runtime reports the
596
+ * failure. In that case the build is still the same durable row and must be
597
+ * returned to QUEUED for an explicit resume instead of being stranded in
598
+ * RUNNING (or permanently FAILED). A committed build is never requeued.
599
+ */
600
+ requeueBuild(input) {
601
+ return this.mutate(ledger => {
602
+ const branch = this.requireBranch(ledger, input);
603
+ const build = ledger.builds[input.buildId];
604
+ if (!build || build.branchId !== branch.branchId)
605
+ throw new contracts_1.ContinuationError('UNKNOWN_BUILD', `Build ${input.buildId} is not in branch ${branch.branchId}`);
606
+ if (build.status === 'SUCCEEDED')
607
+ throw new contracts_1.ContinuationError('FINAL_ALREADY_COMMITTED', `Build ${build.buildId} already committed and cannot be requeued`);
608
+ const guard = this.ensureGuard(ledger, branch.branchId);
609
+ if (guard.activeBuildId === build.buildId) {
610
+ const attempt = guard.activeAttemptId ? ledger.attempts[guard.activeAttemptId] : undefined;
611
+ if (attempt && attempt.status === 'RUNNING') {
612
+ attempt.status = 'EXPIRED';
613
+ attempt.endedAt = new Date().toISOString();
614
+ }
615
+ this.releaseGuard(ledger, branch.branchId, guard.fence);
616
+ }
617
+ build.status = 'QUEUED';
618
+ build.waitingReason = undefined;
619
+ build.updatedAt = new Date().toISOString();
620
+ branch.lifecycleRevision += 1;
621
+ branch.updatedAt = build.updatedAt;
622
+ return { ...build, input: cloneInput(build.input) };
623
+ });
624
+ }
625
+ forkBranch(input) {
626
+ const hash = (0, crypto_1.createHash)('sha256').update(JSON.stringify(input)).digest('hex');
627
+ return this.mutate(ledger => {
628
+ const key = (0, contracts_1.commandKey)(input.actorId, input.commandId);
629
+ const existing = ledger.commands[key];
630
+ if (existing) {
631
+ if (existing.requestHash !== hash)
632
+ throw new contracts_1.ContinuationError('IDEMPOTENCY_KEY_REUSE', `Command ${input.commandId} was already used with a different fork request`);
633
+ return { ...ledger.branches[existing.branchId] };
634
+ }
635
+ const source = this.requireBranch(ledger, { workspaceId: input.workspaceId, rootId: '', branchId: input.sourceBranchId });
636
+ const anchor = ledger.builds[input.anchorBuildId];
637
+ if (!anchor || anchor.branchId !== source.branchId || anchor.status !== 'SUCCEEDED') {
638
+ throw new contracts_1.ContinuationError('ANCHOR_NOT_COMMITTED', `Anchor ${input.anchorBuildId} is not a committed build of source branch ${source.branchId}`);
639
+ }
640
+ if (!this.isAncestorOfHead(ledger, source, anchor.buildId)) {
641
+ throw new contracts_1.ContinuationError('ANCHOR_NOT_IN_LINEAGE', `Anchor ${anchor.buildId} is not on the source branch lineage`);
642
+ }
643
+ const branch = this.ensureBranchRecord(ledger, {
644
+ rootId: source.rootId,
645
+ branchId: input.newBranchId,
646
+ parentAnchorBuildId: anchor.buildId,
647
+ });
648
+ branch.headBuildId = anchor.buildId;
649
+ branch.tailBuildId = anchor.buildId;
650
+ branch.queueRevision = 0;
651
+ branch.lifecycleRevision += 1;
652
+ const receipt = {
653
+ schemaVersion: 1,
654
+ commandId: input.commandId,
655
+ requestHash: hash,
656
+ workspaceId: this.workspaceId,
657
+ rootId: source.rootId,
658
+ branchId: branch.branchId,
659
+ buildId: anchor.buildId,
660
+ parentBuildId: anchor.buildId,
661
+ queueSequence: 0,
662
+ queueRevision: branch.queueRevision,
663
+ lifecycleRevision: branch.lifecycleRevision,
664
+ acceptedAt: new Date().toISOString(),
665
+ };
666
+ ledger.commands[key] = receipt;
667
+ this.appendEvent(ledger, {
668
+ type: 'BranchForked',
669
+ workspaceId: this.workspaceId,
670
+ rootId: source.rootId,
671
+ branchId: branch.branchId,
672
+ buildId: anchor.buildId,
673
+ attemptId: null,
674
+ fence: null,
675
+ payload: { sourceBranchId: source.branchId, anchorBuildId: anchor.buildId },
676
+ });
677
+ return { ...branch };
678
+ });
679
+ }
680
+ setPaused(input) {
681
+ return this.mutate(ledger => {
682
+ const branch = this.requireBranch(ledger, input);
683
+ branch.paused = input.paused === true;
684
+ branch.lifecycleRevision += 1;
685
+ branch.updatedAt = new Date().toISOString();
686
+ this.appendEvent(ledger, {
687
+ type: branch.paused ? 'BranchPaused' : 'BranchResumed',
688
+ workspaceId: this.workspaceId,
689
+ rootId: branch.rootId,
690
+ branchId: branch.branchId,
691
+ buildId: null,
692
+ attemptId: null,
693
+ fence: null,
694
+ });
695
+ return branch.paused;
696
+ });
697
+ }
698
+ snapshot(input) {
699
+ const ledger = this.readLedger();
700
+ const branch = this.requireBranch(ledger, input);
701
+ return this.snapshotUnlocked(ledger, branch);
702
+ }
703
+ snapshotUnlocked(ledger, branch) {
704
+ const guard = ledger.guards[branch.branchId] || (0, contracts_1.defaultGuard)(branch.branchId);
705
+ const builds = Object.values(ledger.builds)
706
+ .filter(build => build.branchId === branch.branchId)
707
+ .sort((a, b) => a.queueSequence - b.queueSequence)
708
+ .map(build => ({ ...build, input: cloneInput(build.input) }));
709
+ return {
710
+ workspaceId: this.workspaceId,
711
+ rootId: branch.rootId,
712
+ branchId: branch.branchId,
713
+ headBuildId: branch.headBuildId,
714
+ tailBuildId: branch.tailBuildId,
715
+ queueRevision: branch.queueRevision,
716
+ lifecycleRevision: branch.lifecycleRevision,
717
+ paused: branch.paused,
718
+ activeBuildId: guard.activeBuildId,
719
+ guard: { ...guard },
720
+ builds,
721
+ };
722
+ }
723
+ events(afterCursor = 0, limit = 500) {
724
+ return this.readLedger().events
725
+ .filter(event => event.cursor > afterCursor)
726
+ .slice(0, Math.max(1, Math.min(5_000, limit)))
727
+ .map(event => ({ ...event, payload: event.payload ? { ...event.payload } : undefined }));
728
+ }
729
+ recoverExpiredLeases(now = Date.now()) {
730
+ return this.mutate(ledger => {
731
+ let recovered = 0;
732
+ for (const guard of Object.values(ledger.guards)) {
733
+ if (!guard.activeBuildId || guard.leaseUntil > now)
734
+ continue;
735
+ const attempt = guard.activeAttemptId ? ledger.attempts[guard.activeAttemptId] : undefined;
736
+ const build = ledger.builds[guard.activeBuildId];
737
+ if (attempt && attempt.status === 'RUNNING') {
738
+ attempt.status = 'EXPIRED';
739
+ attempt.endedAt = new Date(now).toISOString();
740
+ }
741
+ if (build && build.status === 'RUNNING') {
742
+ build.status = 'QUEUED';
743
+ build.updatedAt = new Date(now).toISOString();
744
+ }
745
+ this.releaseGuard(ledger, guard.branchId, guard.fence);
746
+ recovered += 1;
747
+ }
748
+ return recovered;
749
+ });
750
+ }
751
+ terminateAttempt(scope, status, reason, now = Date.now()) {
752
+ this.mutate(ledger => {
753
+ const branch = this.requireBranch(ledger, scope);
754
+ const build = ledger.builds[scope.buildId];
755
+ const attempt = ledger.attempts[scope.attemptId];
756
+ const guard = ledger.guards[scope.branchId];
757
+ if (!build || !attempt || !guard
758
+ || guard.activeAttemptId !== scope.attemptId
759
+ || guard.fence !== scope.fence
760
+ || attempt.fence !== scope.fence) {
761
+ throw new contracts_1.ContinuationError('STALE_EXECUTION', `Attempt ${scope.attemptId} cannot terminate branch ${scope.branchId}`);
762
+ }
763
+ build.status = status;
764
+ build.updatedAt = new Date(now).toISOString();
765
+ attempt.status = status === 'FAILED' ? 'FAILED' : 'CANCELLED';
766
+ attempt.endedAt = build.updatedAt;
767
+ branch.lifecycleRevision += 1;
768
+ this.releaseGuard(ledger, branch.branchId, attempt.fence);
769
+ this.markDependentsBlocked(ledger, branch, build.buildId);
770
+ this.appendEvent(ledger, {
771
+ type: status === 'FAILED' ? 'BuildFailed' : 'BuildCancelled',
772
+ workspaceId: this.workspaceId,
773
+ rootId: branch.rootId,
774
+ branchId: branch.branchId,
775
+ buildId: build.buildId,
776
+ attemptId: attempt.attemptId,
777
+ fence: attempt.fence,
778
+ payload: { reason: String(reason || '') },
779
+ });
780
+ });
781
+ }
782
+ releaseExpiredGuard(ledger, branch, guard, now) {
783
+ const attempt = guard.activeAttemptId ? ledger.attempts[guard.activeAttemptId] : undefined;
784
+ const build = guard.activeBuildId ? ledger.builds[guard.activeBuildId] : undefined;
785
+ if (attempt && attempt.status === 'RUNNING') {
786
+ attempt.status = 'EXPIRED';
787
+ attempt.endedAt = new Date(now).toISOString();
788
+ }
789
+ if (build && build.status === 'RUNNING') {
790
+ build.status = 'QUEUED';
791
+ build.updatedAt = new Date(now).toISOString();
792
+ }
793
+ this.releaseGuard(ledger, branch.branchId, guard.fence);
794
+ }
795
+ markDependentsBlocked(ledger, branch, failedBuildId) {
796
+ for (const build of Object.values(ledger.builds)) {
797
+ if (build.branchId !== branch.branchId || build.status !== 'QUEUED')
798
+ continue;
799
+ if (build.parentBuildId === failedBuildId)
800
+ build.waitingReason = 'DEPENDENCY_FAILED';
801
+ }
802
+ }
803
+ hasCommittedDescendant(ledger, buildId) {
804
+ return Object.values(ledger.builds).some(build => build.parentBuildId === buildId && build.status === 'SUCCEEDED');
805
+ }
806
+ isAncestorOfHead(ledger, branch, anchorBuildId) {
807
+ let cursor = branch.headBuildId;
808
+ const seen = new Set();
809
+ while (cursor) {
810
+ if (seen.has(cursor))
811
+ return false;
812
+ seen.add(cursor);
813
+ if (cursor === anchorBuildId)
814
+ return true;
815
+ cursor = ledger.builds[cursor]?.parentBuildId || null;
816
+ }
817
+ return false;
818
+ }
819
+ ensureBranchRecord(ledger, input) {
820
+ const existing = ledger.branches[input.branchId];
821
+ if (existing)
822
+ return existing;
823
+ const now = new Date().toISOString();
824
+ const branch = {
825
+ branchId: input.branchId,
826
+ workspaceId: this.workspaceId,
827
+ rootId: input.rootId,
828
+ parentAnchorBuildId: input.parentAnchorBuildId,
829
+ headBuildId: input.parentAnchorBuildId,
830
+ tailBuildId: input.parentAnchorBuildId,
831
+ queueRevision: 0,
832
+ lifecycleRevision: 0,
833
+ paused: false,
834
+ createdAt: now,
835
+ updatedAt: now,
836
+ };
837
+ ledger.branches[branch.branchId] = branch;
838
+ ledger.guards[branch.branchId] = (0, contracts_1.defaultGuard)(branch.branchId);
839
+ if (!ledger.roots[branch.rootId]) {
840
+ ledger.roots[branch.rootId] = { rootId: branch.rootId, workspaceId: this.workspaceId, defaultBranchId: branch.branchId, createdAt: now };
841
+ }
842
+ return branch;
843
+ }
844
+ requireBranch(ledger, target) {
845
+ const branch = ledger.branches[target.branchId];
846
+ if (!branch)
847
+ throw new contracts_1.ContinuationError('UNKNOWN_BRANCH', `Branch ${target.branchId} does not exist`);
848
+ if (branch.workspaceId !== this.workspaceId) {
849
+ throw new contracts_1.ContinuationError('ROUTE_MISMATCH', `Branch ${target.branchId} belongs to workspace ${branch.workspaceId}`);
850
+ }
851
+ if (target.rootId && branch.rootId !== target.rootId) {
852
+ throw new contracts_1.ContinuationError('ROUTE_MISMATCH', `Branch ${target.branchId} belongs to root ${branch.rootId}, not ${target.rootId}`);
853
+ }
854
+ return branch;
855
+ }
856
+ ensureGuard(ledger, branchId) {
857
+ if (!ledger.guards[branchId])
858
+ ledger.guards[branchId] = (0, contracts_1.defaultGuard)(branchId);
859
+ return ledger.guards[branchId];
860
+ }
861
+ /**
862
+ * Release the active attempt without ever moving the branch fence backward.
863
+ * A recovered/completed attempt's fence stays as the monotonic high-water
864
+ * mark; the next claim increments it again.
865
+ */
866
+ releaseGuard(ledger, branchId, fence) {
867
+ ledger.guards[branchId] = {
868
+ ...(0, contracts_1.defaultGuard)(branchId),
869
+ fence: Math.max(0, Math.floor(Number(fence) || 0)),
870
+ };
871
+ }
872
+ appendEvent(ledger, input) {
873
+ const event = {
874
+ ...input,
875
+ eventId: `event-${(0, crypto_1.randomUUID)()}`,
876
+ cursor: ledger.nextEventCursor++,
877
+ at: input.at || new Date().toISOString(),
878
+ };
879
+ ledger.events.push(event);
880
+ if (ledger.events.length > this.eventRetention)
881
+ ledger.events = ledger.events.slice(-this.eventRetention);
882
+ return event;
883
+ }
884
+ mutate(apply) {
885
+ fs.mkdirSync(path.dirname(this.ledgerPath), { recursive: true });
886
+ const lockHandle = this.acquireLock();
887
+ try {
888
+ this.invalidateCache();
889
+ const ledger = this.readLedgerUnlocked();
890
+ const result = apply(ledger);
891
+ this.writeLedgerUnlocked(ledger);
892
+ return result;
893
+ }
894
+ finally {
895
+ try {
896
+ fs.closeSync(lockHandle);
897
+ }
898
+ catch { /* lock already released */ }
899
+ try {
900
+ fs.unlinkSync(this.lockPath);
901
+ }
902
+ catch { /* another process may have removed it */ }
903
+ }
904
+ }
905
+ acquireLock() {
906
+ for (let attempt = 0; attempt < 400; attempt += 1) {
907
+ try {
908
+ return fs.openSync(this.lockPath, 'wx');
909
+ }
910
+ catch (error) {
911
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code || '') : '';
912
+ if (!['EEXIST', 'EPERM', 'EACCES', 'EBUSY'].includes(code))
913
+ throw error;
914
+ if (code === 'EEXIST') {
915
+ try {
916
+ if (Date.now() - fs.statSync(this.lockPath).mtimeMs > 30_000)
917
+ fs.unlinkSync(this.lockPath);
918
+ }
919
+ catch { /* lock may have been released */ }
920
+ }
921
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5 + attempt);
922
+ }
923
+ }
924
+ throw new contracts_1.ContinuationError('EFFECT_UNCERTAIN', `Timed out acquiring continuation ledger lock: ${this.lockPath}`);
925
+ }
926
+ readLedger() {
927
+ this.ensureLedgerExists();
928
+ const stat = fs.statSync(this.ledgerPath, { bigint: true });
929
+ const fingerprint = `${stat.mtimeNs}:${stat.ctimeNs}:${stat.size}`;
930
+ if (this.cached && this.fingerprint === fingerprint)
931
+ return this.cached;
932
+ const ledger = this.readLedgerUnlocked();
933
+ this.cached = ledger;
934
+ this.fingerprint = fingerprint;
935
+ return ledger;
936
+ }
937
+ readLedgerUnlocked() {
938
+ this.ensureLedgerExists();
939
+ try {
940
+ const raw = fs.readFileSync(this.ledgerPath, 'utf-8').replace(/^\uFEFF/, '');
941
+ const parsed = JSON.parse(raw);
942
+ if (!parsed || parsed.version !== 1 || !parsed.branches || !parsed.builds) {
943
+ throw new contracts_1.ContinuationError('EFFECT_UNCERTAIN', `Continuation ledger ${this.ledgerPath} has an unsupported shape`);
944
+ }
945
+ parsed.attempts = parsed.attempts || {};
946
+ parsed.guards = parsed.guards || {};
947
+ parsed.commands = parsed.commands || {};
948
+ parsed.finals = parsed.finals || {};
949
+ parsed.events = Array.isArray(parsed.events) ? parsed.events : [];
950
+ parsed.nextEventCursor = Math.max(1, Number(parsed.nextEventCursor) || (parsed.events.at(-1)?.cursor || 0) + 1);
951
+ parsed.nextQueueSequence = Math.max(1, Number(parsed.nextQueueSequence) || (Object.values(parsed.builds).reduce((max, build) => Math.max(max, build.queueSequence || 0), 0) + 1));
952
+ return parsed;
953
+ }
954
+ catch (error) {
955
+ if (error instanceof contracts_1.ContinuationError)
956
+ throw error;
957
+ throw new contracts_1.ContinuationError('EFFECT_UNCERTAIN', `Continuation ledger ${this.ledgerPath} is unreadable: ${error instanceof Error ? error.message : String(error)}`);
958
+ }
959
+ }
960
+ writeLedgerUnlocked(ledger) {
961
+ const temp = `${this.ledgerPath}.${process.pid}.${(0, crypto_1.randomUUID)()}.tmp`;
962
+ fs.writeFileSync(temp, `${JSON.stringify(ledger, null, 2)}\n`, 'utf-8');
963
+ fs.renameSync(temp, this.ledgerPath);
964
+ this.invalidateCache();
965
+ }
966
+ ensureLedgerExists() {
967
+ if (fs.existsSync(this.ledgerPath))
968
+ return;
969
+ fs.mkdirSync(path.dirname(this.ledgerPath), { recursive: true });
970
+ const ledger = (0, contracts_1.emptyLedger)(this.workspaceId);
971
+ const temp = `${this.ledgerPath}.${process.pid}.${(0, crypto_1.randomUUID)()}.tmp`;
972
+ fs.writeFileSync(temp, `${JSON.stringify(ledger, null, 2)}\n`, 'utf-8');
973
+ fs.renameSync(temp, this.ledgerPath);
974
+ }
975
+ invalidateCache() {
976
+ this.cached = null;
977
+ this.fingerprint = '';
978
+ }
979
+ }
980
+ exports.GuardedContinuationStore = GuardedContinuationStore;
981
+ function cloneInput(input) {
982
+ return {
983
+ ...input,
984
+ images: input.images?.map(image => ({ ...image })),
985
+ attachments: input.attachments?.map(attachment => ({ ...attachment })),
986
+ branchPath: input.branchPath?.slice(),
987
+ };
988
+ }
989
+ //# sourceMappingURL=store.js.map