@osovv/vv-opencode 1.4.2 → 1.4.3-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +20 -6
- package/dist/lib/orchestration.js +12 -3
- package/dist/lib/orchestration.js.map +1 -1
- package/dist/lib/spec-lint.d.ts +2 -64
- package/dist/lib/spec-lint.js +47 -45
- package/dist/lib/spec-lint.js.map +1 -1
- package/dist/lib/workflow-contract.d.ts +277 -0
- package/dist/lib/workflow-contract.js +636 -0
- package/dist/lib/workflow-contract.js.map +1 -0
- package/dist/plugins/system-context-injection/index.js +15 -7
- package/dist/plugins/system-context-injection/index.js.map +1 -1
- package/dist/plugins/workflow/authority.d.ts +99 -0
- package/dist/plugins/workflow/authority.js +435 -0
- package/dist/plugins/workflow/authority.js.map +1 -0
- package/dist/plugins/workflow/checkpoint-io.d.ts +10 -1
- package/dist/plugins/workflow/checkpoint-io.js +22 -3
- package/dist/plugins/workflow/checkpoint-io.js.map +1 -1
- package/dist/plugins/workflow/checkpoints.d.ts +58 -1
- package/dist/plugins/workflow/checkpoints.js +362 -34
- package/dist/plugins/workflow/checkpoints.js.map +1 -1
- package/dist/plugins/workflow/delegated.d.ts +150 -2
- package/dist/plugins/workflow/delegated.js +551 -15
- package/dist/plugins/workflow/delegated.js.map +1 -1
- package/dist/plugins/workflow/execution.d.ts +361 -0
- package/dist/plugins/workflow/execution.js +1807 -0
- package/dist/plugins/workflow/execution.js.map +1 -0
- package/dist/plugins/workflow/index.js +539 -80
- package/dist/plugins/workflow/index.js.map +1 -1
- package/dist/plugins/workflow/persistence.d.ts +18 -4
- package/dist/plugins/workflow/persistence.js +439 -42
- package/dist/plugins/workflow/persistence.js.map +1 -1
- package/dist/plugins/workflow/repair.d.ts +2 -0
- package/dist/plugins/workflow/repair.js +16 -8
- package/dist/plugins/workflow/repair.js.map +1 -1
- package/dist/plugins/workflow/snapshots.js +3 -3
- package/dist/plugins/workflow/snapshots.js.map +1 -1
- package/dist/plugins/workflow/state.d.ts +13 -0
- package/dist/plugins/workflow/state.js +37 -7
- package/dist/plugins/workflow/state.js.map +1 -1
- package/dist/plugins/workflow/system-instruction.md +15 -1
- package/dist/plugins/workflow/tooling.d.ts +81 -6
- package/dist/plugins/workflow/tooling.js +956 -23
- package/dist/plugins/workflow/tooling.js.map +1 -1
- package/dist/plugins/workflow/transactions.d.ts +54 -0
- package/dist/plugins/workflow/transactions.js +147 -0
- package/dist/plugins/workflow/transactions.js.map +1 -0
- package/package.json +1 -1
- package/schemas/vvoc/v3.json +1 -1
- package/templates/agents/vv-code-reviewer.md +1 -0
- package/templates/agents/vv-controller.md +26 -13
- package/templates/agents/vv-implementer.md +1 -0
- package/templates/agents/vv-spec-reviewer.md +1 -0
- package/templates/skills/vv-execute/SKILL.md +6 -5
- package/templates/skills/vv-plan/SKILL.md +1 -1
- package/templates/skills/vv-review/SKILL.md +1 -0
- package/templates/skills/vv-spec/SKILL.md +2 -2
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
// FILE: src/plugins/workflow/persistence.ts
|
|
2
|
-
// VERSION: 0.
|
|
2
|
+
// VERSION: 0.4.0
|
|
3
3
|
// START_MODULE_CONTRACT
|
|
4
4
|
// PURPOSE: Hydrate and snapshot work-item workflow state from/to per-session JSON
|
|
5
5
|
// files under $XDG_DATA_HOME/vvoc/workflow/<sessionId>/workflow-state.json.
|
|
6
6
|
// SCOPE: Read/write WorkItemStoreData (nextId, records, keyIndexBySession,
|
|
7
|
-
// planRuns) as serializable JSON. Version
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
7
|
+
// planRuns, executions, messageClaims) as serializable JSON. Version 4
|
|
8
|
+
// snapshots additionally persist the common execution registry and
|
|
9
|
+
// session-wide root-message claims. Version 3 snapshots persist
|
|
10
|
+
// delegated and checkpoint recovery histories with replay-protected
|
|
11
|
+
// authorization references, recovery-aware attempt and generation budgets,
|
|
12
|
+
// and report_rejected attempts carrying bounded rejected-report evidence
|
|
13
|
+
// through an atomic temporary-file replacement. Version 2 files hydrate
|
|
14
|
+
// conservatively with empty recovery histories and unchanged budgets;
|
|
15
|
+
// version 1 files hydrate as legacy records with an empty plan-run
|
|
16
|
+
// registry. Existing native plan runs materialize a compatibility
|
|
17
|
+
// execution registry entry without inventing authority. Neither ever
|
|
18
|
+
// synthesizes acceptance, approval, or recovery.
|
|
13
19
|
// Strict validation rejects malformed or contradictory new state instead of
|
|
14
20
|
// silently restarting a run. A checked loader distinguishes missing, valid,
|
|
15
21
|
// and invalid state and surfaces I/O failures.
|
|
16
22
|
// DEPENDS: [node:fs, node:fs/promises, node:path, src/lib/vvoc-paths.ts,
|
|
23
|
+
// src/lib/workflow-contract.ts,
|
|
17
24
|
// src/plugins/workflow/checkpoints.ts (types), src/plugins/workflow/delegated.ts,
|
|
18
|
-
// src/plugins/workflow/state.ts]
|
|
19
|
-
// LINKS: M-WORKFLOW-PERSISTENCE, M-CONFIG-LAYERS, M-WORKFLOW-STATE, M-WORKFLOW-DELEGATED, M-WORKFLOW-CHECKPOINTS, V-M-WORKFLOW-PERSISTENCE
|
|
25
|
+
// src/plugins/workflow/execution.ts, src/plugins/workflow/state.ts]
|
|
26
|
+
// LINKS: M-WORKFLOW-PERSISTENCE, M-CONFIG-LAYERS, M-WORKFLOW-STATE, M-WORKFLOW-DELEGATED, M-WORKFLOW-CHECKPOINTS, M-WORKFLOW-EXECUTION, M-WORKFLOW-CONTRACT, V-M-WORKFLOW-PERSISTENCE
|
|
20
27
|
// ROLE: RUNTIME
|
|
21
28
|
// MAP_MODE: EXPORTS
|
|
22
29
|
// END_MODULE_CONTRACT
|
|
@@ -25,6 +32,7 @@
|
|
|
25
32
|
// PERSISTED_WORKFLOW_STATE_VERSION - Current persisted snapshot version.
|
|
26
33
|
// PersistedWorkflowState - JSON-serializable shape of a per-session workflow state.
|
|
27
34
|
// SerializedDelegatedPlanRun - JSON form of one registered plan run.
|
|
35
|
+
// SerializedWorkflowExecution - JSON form of one common execution registry entry.
|
|
28
36
|
// HydratedWorkflowStateResult - Missing/valid/invalid triage returned by the checked loader.
|
|
29
37
|
// SnapshotWorkflowStateResult - Write outcome returned by the checked snapshot path.
|
|
30
38
|
// getWorkflowSessionDir - Resolve per-session directory path.
|
|
@@ -36,16 +44,20 @@
|
|
|
36
44
|
// END_MODULE_MAP
|
|
37
45
|
//
|
|
38
46
|
// START_CHANGE_SUMMARY
|
|
39
|
-
// LAST_CHANGE: [
|
|
47
|
+
// LAST_CHANGE: [C-WORKFLOW-PLAN-INDEPENDENCE - Version 4 persists the common execution registry and session-wide message claims while explicitly migrating v1/v2/v3 without clearing recovery, rejected-report, or stopped-generation history.]
|
|
40
48
|
// END_CHANGE_SUMMARY
|
|
41
49
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
42
50
|
import { rm } from "node:fs/promises";
|
|
43
51
|
import { join } from "node:path";
|
|
44
52
|
import { getGlobalVvocDataDir } from "../../lib/vvoc-paths.js";
|
|
45
|
-
import { normalizeDeclaredScopePath } from "../../lib/
|
|
46
|
-
import {
|
|
53
|
+
import { normalizeDeclaredScopePath } from "../../lib/workflow-contract.js";
|
|
54
|
+
import { ensureNativeExecutions } from "./execution.js";
|
|
55
|
+
import { DELEGATED_BASE_ATTEMPTS, DELEGATED_EVIDENCE_MAX_CHARS, DELEGATED_EVIDENCE_MAX_REFS, DELEGATED_RATIONALE_MAX_CHARS, DELEGATED_RECOVERY_CODE_MAX_CHARS, delegatedRecoveryGrantCount, } from "./delegated.js";
|
|
56
|
+
import { MAX_CHECKPOINT_REVIEW_ATTEMPTS, checkpointRecoveryGrantCount } from "./checkpoints.js";
|
|
47
57
|
// START_BLOCK_SERIALIZATION_TYPES
|
|
48
|
-
export const PERSISTED_WORKFLOW_STATE_VERSION =
|
|
58
|
+
export const PERSISTED_WORKFLOW_STATE_VERSION = 4;
|
|
59
|
+
/** Version at which delegated/checkpoint recovery histories and report rejections were introduced. */
|
|
60
|
+
const RECOVERY_STATE_VERSION = 3;
|
|
49
61
|
// END_BLOCK_SERIALIZATION_TYPES
|
|
50
62
|
const VALID_STATES = new Set([
|
|
51
63
|
"open",
|
|
@@ -63,7 +75,7 @@ const DELEGATED_RESULT_STATUSES = new Set([
|
|
|
63
75
|
"NEEDS_CONTEXT",
|
|
64
76
|
"BLOCKED",
|
|
65
77
|
]);
|
|
66
|
-
const CHECKPOINT_OUTCOMES = new Set(["passed", "failed", "stale"]);
|
|
78
|
+
const CHECKPOINT_OUTCOMES = new Set(["passed", "failed", "stale", "stopped"]);
|
|
67
79
|
const LAST_OUTCOMES = new Set([
|
|
68
80
|
"passed",
|
|
69
81
|
"failed",
|
|
@@ -71,6 +83,17 @@ const LAST_OUTCOMES = new Set([
|
|
|
71
83
|
"stopped",
|
|
72
84
|
"incomplete",
|
|
73
85
|
]);
|
|
86
|
+
const RECOVERY_KINDS = new Set([
|
|
87
|
+
"resume",
|
|
88
|
+
"autonomous_grant",
|
|
89
|
+
"user_grant",
|
|
90
|
+
"advance_grant",
|
|
91
|
+
]);
|
|
92
|
+
const TERMINAL_ATTEMPT_STATUSES = new Set([
|
|
93
|
+
"completed",
|
|
94
|
+
"failed",
|
|
95
|
+
"report_rejected",
|
|
96
|
+
]);
|
|
74
97
|
function isWorkItemMode(value) {
|
|
75
98
|
return value === "implementation" || value === "review_only" || value === "delegated";
|
|
76
99
|
}
|
|
@@ -167,7 +190,7 @@ function validateBoundedEvidence(value, label, errors) {
|
|
|
167
190
|
return true;
|
|
168
191
|
}
|
|
169
192
|
/** Validate the delegated-mode record extension; returns every contradiction found. */
|
|
170
|
-
function validateDelegatedState(record, sessionId, errors) {
|
|
193
|
+
function validateDelegatedState(record, sessionId, version, errors) {
|
|
171
194
|
const delegated = record.delegated;
|
|
172
195
|
if (!delegated || typeof delegated !== "object") {
|
|
173
196
|
errors.push(`${record.workItemId}: delegated records require a delegated state object`);
|
|
@@ -199,7 +222,8 @@ function validateDelegatedState(record, sessionId, errors) {
|
|
|
199
222
|
inFlight += 1;
|
|
200
223
|
if (attempt.resultStatus !== undefined ||
|
|
201
224
|
attempt.completedAt !== undefined ||
|
|
202
|
-
attempt.failureExcerpt !== undefined
|
|
225
|
+
attempt.failureExcerpt !== undefined ||
|
|
226
|
+
attempt.reportRejection !== undefined) {
|
|
203
227
|
errors.push(`${record.workItemId}: in-flight attempt ${attempt.attempt} must not carry a result`);
|
|
204
228
|
}
|
|
205
229
|
}
|
|
@@ -210,22 +234,43 @@ function validateDelegatedState(record, sessionId, errors) {
|
|
|
210
234
|
if (typeof attempt.completedAt !== "string") {
|
|
211
235
|
errors.push(`${record.workItemId}: completed attempt ${attempt.attempt} requires completedAt`);
|
|
212
236
|
}
|
|
213
|
-
if (attempt.failureExcerpt !== undefined) {
|
|
214
|
-
errors.push(`${record.workItemId}: completed attempt ${attempt.attempt} must not carry
|
|
237
|
+
if (attempt.failureExcerpt !== undefined || attempt.reportRejection !== undefined) {
|
|
238
|
+
errors.push(`${record.workItemId}: completed attempt ${attempt.attempt} must not carry failure or rejection evidence`);
|
|
215
239
|
}
|
|
216
240
|
}
|
|
217
241
|
else if (attempt.status === "failed") {
|
|
218
242
|
if (typeof attempt.completedAt !== "string") {
|
|
219
243
|
errors.push(`${record.workItemId}: failed attempt ${attempt.attempt} requires completedAt`);
|
|
220
244
|
}
|
|
221
|
-
if (attempt.resultStatus !== undefined) {
|
|
222
|
-
errors.push(`${record.workItemId}: failed attempt ${attempt.attempt} must not carry a
|
|
245
|
+
if (attempt.resultStatus !== undefined || attempt.reportRejection !== undefined) {
|
|
246
|
+
errors.push(`${record.workItemId}: failed attempt ${attempt.attempt} must not carry a result or rejection`);
|
|
223
247
|
}
|
|
224
248
|
if (attempt.failureExcerpt === undefined ||
|
|
225
249
|
!isWorkflowResultExcerpt(attempt.failureExcerpt)) {
|
|
226
250
|
errors.push(`${record.workItemId}: failed attempt ${attempt.attempt} requires a bounded failure excerpt`);
|
|
227
251
|
}
|
|
228
252
|
}
|
|
253
|
+
else if (attempt.status === "report_rejected") {
|
|
254
|
+
if (version < RECOVERY_STATE_VERSION) {
|
|
255
|
+
errors.push(`${record.workItemId}: report_rejected attempts require persisted version ${RECOVERY_STATE_VERSION}`);
|
|
256
|
+
}
|
|
257
|
+
if (typeof attempt.completedAt !== "string") {
|
|
258
|
+
errors.push(`${record.workItemId}: report_rejected attempt ${attempt.attempt} requires completedAt`);
|
|
259
|
+
}
|
|
260
|
+
if (attempt.resultStatus !== undefined || attempt.failureExcerpt !== undefined) {
|
|
261
|
+
errors.push(`${record.workItemId}: report_rejected attempt ${attempt.attempt} must not carry a synthesized result`);
|
|
262
|
+
}
|
|
263
|
+
const rejection = attempt.reportRejection;
|
|
264
|
+
if (!rejection ||
|
|
265
|
+
typeof rejection !== "object" ||
|
|
266
|
+
typeof rejection.protocolErrorCode !== "string" ||
|
|
267
|
+
rejection.protocolErrorCode.trim() === "" ||
|
|
268
|
+
rejection.protocolErrorCode.length > DELEGATED_RECOVERY_CODE_MAX_CHARS ||
|
|
269
|
+
typeof rejection.rejectedAt !== "string" ||
|
|
270
|
+
!isWorkflowResultExcerpt(rejection.excerpt)) {
|
|
271
|
+
errors.push(`${record.workItemId}: report_rejected attempt ${attempt.attempt} requires a bounded rejection record`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
229
274
|
else {
|
|
230
275
|
errors.push(`${record.workItemId}: attempt ${attempt.attempt} has invalid status ${attempt.status}`);
|
|
231
276
|
}
|
|
@@ -240,8 +285,63 @@ function validateDelegatedState(record, sessionId, errors) {
|
|
|
240
285
|
if (delegated.reworkHistory.some((rework) => !rework.reworkId || !rework.authorizedByCheckpoint)) {
|
|
241
286
|
errors.push(`${record.workItemId}: rework history entries require ids and checkpoint authorization`);
|
|
242
287
|
}
|
|
243
|
-
|
|
244
|
-
|
|
288
|
+
const recoveryHistory = delegated.recoveryHistory;
|
|
289
|
+
if (!Array.isArray(recoveryHistory)) {
|
|
290
|
+
if (version >= RECOVERY_STATE_VERSION) {
|
|
291
|
+
errors.push(`${record.workItemId}: version 3 delegated records require a recovery history`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else if (recoveryHistory.length > 0 && version < RECOVERY_STATE_VERSION) {
|
|
295
|
+
errors.push(`${record.workItemId}: recovery history requires persisted version ${RECOVERY_STATE_VERSION}`);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const recoveryIds = new Set();
|
|
299
|
+
const userMessageIds = new Set();
|
|
300
|
+
let autonomousGrants = 0;
|
|
301
|
+
for (const recovery of recoveryHistory) {
|
|
302
|
+
if (!recovery.recoveryId || recoveryIds.has(recovery.recoveryId)) {
|
|
303
|
+
errors.push(`${record.workItemId}: recovery entries require unique non-empty recoveryId values`);
|
|
304
|
+
}
|
|
305
|
+
recoveryIds.add(recovery.recoveryId ?? "");
|
|
306
|
+
if (!RECOVERY_KINDS.has(recovery.kind)) {
|
|
307
|
+
errors.push(`${record.workItemId}: recovery ${recovery.recoveryId} has an invalid kind`);
|
|
308
|
+
}
|
|
309
|
+
if (recovery.kind === "autonomous_grant") {
|
|
310
|
+
autonomousGrants += 1;
|
|
311
|
+
}
|
|
312
|
+
if (recovery.kind === "user_grant") {
|
|
313
|
+
if (typeof recovery.userMessageId !== "string" || recovery.userMessageId === "") {
|
|
314
|
+
errors.push(`${record.workItemId}: user-grant recovery ${recovery.recoveryId} requires its authorizing message id`);
|
|
315
|
+
}
|
|
316
|
+
else if (userMessageIds.has(recovery.userMessageId)) {
|
|
317
|
+
errors.push(`${record.workItemId}: message ${recovery.userMessageId} authorized more than one recovery unit`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
userMessageIds.add(recovery.userMessageId);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else if (recovery.userMessageId !== undefined) {
|
|
324
|
+
errors.push(`${record.workItemId}: non-user recovery ${recovery.recoveryId} must not persist an authorization reference`);
|
|
325
|
+
}
|
|
326
|
+
if (!Number.isInteger(recovery.targetAttempt) ||
|
|
327
|
+
recovery.targetAttempt < 1 ||
|
|
328
|
+
recovery.targetAttempt > attempts.length ||
|
|
329
|
+
!TERMINAL_ATTEMPT_STATUSES.has(attempts[recovery.targetAttempt - 1]?.status ?? "")) {
|
|
330
|
+
errors.push(`${record.workItemId}: recovery ${recovery.recoveryId} targets a non-terminal attempt`);
|
|
331
|
+
}
|
|
332
|
+
validateDelegatedText(recovery.diagnosis, DELEGATED_RATIONALE_MAX_CHARS, `${record.workItemId}: recovery ${recovery.recoveryId} diagnosis`, errors);
|
|
333
|
+
validateDelegatedText(recovery.changedCondition, DELEGATED_RATIONALE_MAX_CHARS, `${record.workItemId}: recovery ${recovery.recoveryId} changedCondition`, errors);
|
|
334
|
+
validateBoundedEvidence(recovery.verification, `${record.workItemId}: recovery ${recovery.recoveryId} verification`, errors);
|
|
335
|
+
}
|
|
336
|
+
if (autonomousGrants > 1) {
|
|
337
|
+
errors.push(`${record.workItemId}: at most one autonomous recovery grant may be recorded per item`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (attempts.length >
|
|
341
|
+
DELEGATED_BASE_ATTEMPTS +
|
|
342
|
+
delegated.reworkHistory.length +
|
|
343
|
+
delegatedRecoveryGrantCount(delegated.recoveryHistory ?? [])) {
|
|
344
|
+
errors.push(`${record.workItemId}: attempts exceed the base budget plus authorized rework and recovery grants`);
|
|
245
345
|
}
|
|
246
346
|
const decisions = delegated.decisions;
|
|
247
347
|
if (!Array.isArray(decisions)) {
|
|
@@ -310,7 +410,7 @@ function validateDelegatedState(record, sessionId, errors) {
|
|
|
310
410
|
errors.push(`${record.workItemId}: record session mismatch`);
|
|
311
411
|
}
|
|
312
412
|
}
|
|
313
|
-
function isWorkItemRecord(value, sessionId, errors) {
|
|
413
|
+
function isWorkItemRecord(value, sessionId, version, errors) {
|
|
314
414
|
if (!value || typeof value !== "object")
|
|
315
415
|
return false;
|
|
316
416
|
const record = value;
|
|
@@ -333,7 +433,7 @@ function isWorkItemRecord(value, sessionId, errors) {
|
|
|
333
433
|
return false;
|
|
334
434
|
if (record.mode === "delegated") {
|
|
335
435
|
const recordErrors = [];
|
|
336
|
-
validateDelegatedState(record, sessionId, recordErrors);
|
|
436
|
+
validateDelegatedState(record, sessionId, version, recordErrors);
|
|
337
437
|
errors.push(...recordErrors);
|
|
338
438
|
return recordErrors.length === 0;
|
|
339
439
|
}
|
|
@@ -347,7 +447,7 @@ function isWorkItemRecord(value, sessionId, errors) {
|
|
|
347
447
|
}
|
|
348
448
|
return true;
|
|
349
449
|
}
|
|
350
|
-
function validatePlanRun(run, recordsById, sessionId, errors) {
|
|
450
|
+
function validatePlanRun(run, recordsById, sessionId, version, errors) {
|
|
351
451
|
if (!run || typeof run !== "object") {
|
|
352
452
|
errors.push("plan run entries must be objects");
|
|
353
453
|
return false;
|
|
@@ -429,10 +529,68 @@ function validatePlanRun(run, recordsById, sessionId, errors) {
|
|
|
429
529
|
if (!CHECKPOINT_OUTCOMES.has(entry.outcome)) {
|
|
430
530
|
errors.push(`checkpoint ${checkpoint.checkpointId} history has invalid outcome ${entry.outcome}`);
|
|
431
531
|
}
|
|
532
|
+
if (entry.outcome === "stopped" && version < RECOVERY_STATE_VERSION) {
|
|
533
|
+
errors.push(`checkpoint ${checkpoint.checkpointId} stopped history requires persisted version ${RECOVERY_STATE_VERSION}`);
|
|
534
|
+
}
|
|
432
535
|
if (typeof entry.generation !== "number" || typeof entry.fingerprint !== "string") {
|
|
433
536
|
errors.push(`checkpoint ${checkpoint.checkpointId} history entry is malformed`);
|
|
434
537
|
}
|
|
435
538
|
}
|
|
539
|
+
const recoveryHistory = checkpoint.recoveryHistory;
|
|
540
|
+
if (!Array.isArray(recoveryHistory)) {
|
|
541
|
+
if (version >= RECOVERY_STATE_VERSION) {
|
|
542
|
+
errors.push(`checkpoint ${checkpoint.checkpointId} requires a recovery history at persisted version ${RECOVERY_STATE_VERSION}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else if (recoveryHistory.length > 0 && version < RECOVERY_STATE_VERSION) {
|
|
546
|
+
errors.push(`checkpoint ${checkpoint.checkpointId} recovery history requires persisted version ${RECOVERY_STATE_VERSION}`);
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
const recoveryIds = new Set();
|
|
550
|
+
const userMessageIds = new Set();
|
|
551
|
+
let autonomousGrants = 0;
|
|
552
|
+
for (const recovery of recoveryHistory) {
|
|
553
|
+
if (!recovery.recoveryId || recoveryIds.has(recovery.recoveryId)) {
|
|
554
|
+
errors.push(`checkpoint ${checkpoint.checkpointId} recovery entries require unique non-empty recoveryId values`);
|
|
555
|
+
}
|
|
556
|
+
recoveryIds.add(recovery.recoveryId ?? "");
|
|
557
|
+
if (!RECOVERY_KINDS.has(recovery.kind)) {
|
|
558
|
+
errors.push(`checkpoint ${checkpoint.checkpointId} recovery ${recovery.recoveryId} has an invalid kind`);
|
|
559
|
+
}
|
|
560
|
+
if (recovery.kind === "autonomous_grant") {
|
|
561
|
+
autonomousGrants += 1;
|
|
562
|
+
}
|
|
563
|
+
if (recovery.kind === "user_grant") {
|
|
564
|
+
if (typeof recovery.userMessageId !== "string" || recovery.userMessageId === "") {
|
|
565
|
+
errors.push(`checkpoint ${checkpoint.checkpointId} user-grant recovery ${recovery.recoveryId} requires its authorizing message id`);
|
|
566
|
+
}
|
|
567
|
+
else if (userMessageIds.has(recovery.userMessageId)) {
|
|
568
|
+
errors.push(`checkpoint ${checkpoint.checkpointId} message ${recovery.userMessageId} authorized more than one recovery unit`);
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
userMessageIds.add(recovery.userMessageId);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
else if (recovery.userMessageId !== undefined) {
|
|
575
|
+
errors.push(`checkpoint ${checkpoint.checkpointId} non-user recovery ${recovery.recoveryId} must not persist an authorization reference`);
|
|
576
|
+
}
|
|
577
|
+
if (!Number.isInteger(recovery.targetGeneration) ||
|
|
578
|
+
recovery.targetGeneration < 1 ||
|
|
579
|
+
recovery.targetGeneration > checkpoint.attempts) {
|
|
580
|
+
errors.push(`checkpoint ${checkpoint.checkpointId} recovery ${recovery.recoveryId} targets an unknown generation`);
|
|
581
|
+
}
|
|
582
|
+
validateDelegatedText(recovery.diagnosis, DELEGATED_RATIONALE_MAX_CHARS, `checkpoint ${checkpoint.checkpointId} recovery ${recovery.recoveryId} diagnosis`, errors);
|
|
583
|
+
validateDelegatedText(recovery.changedCondition, DELEGATED_RATIONALE_MAX_CHARS, `checkpoint ${checkpoint.checkpointId} recovery ${recovery.recoveryId} changedCondition`, errors);
|
|
584
|
+
validateBoundedEvidence(recovery.verification, `checkpoint ${checkpoint.checkpointId} recovery ${recovery.recoveryId} verification`, errors);
|
|
585
|
+
}
|
|
586
|
+
if (autonomousGrants > 1) {
|
|
587
|
+
errors.push(`checkpoint ${checkpoint.checkpointId} has more than one autonomous recovery grant`);
|
|
588
|
+
}
|
|
589
|
+
if (checkpoint.attempts >
|
|
590
|
+
MAX_CHECKPOINT_REVIEW_ATTEMPTS + checkpointRecoveryGrantCount(recoveryHistory)) {
|
|
591
|
+
errors.push(`checkpoint ${checkpoint.checkpointId} generations exceed the ordinary budget plus recovery grants`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
436
594
|
const review = checkpoint.currentReview;
|
|
437
595
|
if (checkpoint.status === "in_review") {
|
|
438
596
|
if (!review ||
|
|
@@ -458,7 +616,7 @@ function validatePlanRun(run, recordsById, sessionId, errors) {
|
|
|
458
616
|
errors.push(`checkpoint ${checkpoint.checkpointId} passed without a passing history entry`);
|
|
459
617
|
}
|
|
460
618
|
if (checkpoint.status === "failed" &&
|
|
461
|
-
!["failed", "stale"].includes(history[history.length - 1]?.outcome ?? "")) {
|
|
619
|
+
!["failed", "stale", "stopped"].includes(history[history.length - 1]?.outcome ?? "")) {
|
|
462
620
|
errors.push(`checkpoint ${checkpoint.checkpointId} failed without a failing history entry`);
|
|
463
621
|
}
|
|
464
622
|
if (checkpoint.attempts !== history.length + (checkpoint.status === "in_review" ? 1 : 0)) {
|
|
@@ -481,6 +639,181 @@ function serializePlanRun(run) {
|
|
|
481
639
|
checkpoints: [...run.checkpoints.values()],
|
|
482
640
|
};
|
|
483
641
|
}
|
|
642
|
+
// START_BLOCK_EXECUTION_SERIALIZATION
|
|
643
|
+
function serializeWorkflowExecution(execution) {
|
|
644
|
+
return {
|
|
645
|
+
...execution,
|
|
646
|
+
tasks: [...execution.tasks.entries()],
|
|
647
|
+
checkpoints: [...execution.checkpoints.entries()],
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function isNonEmptyString(value) {
|
|
651
|
+
return typeof value === "string" && value.trim() !== "";
|
|
652
|
+
}
|
|
653
|
+
function validateWorkflowExecution(candidate, sessionId, errors) {
|
|
654
|
+
const before = errors.length;
|
|
655
|
+
if (!isNonEmptyString(candidate.runId))
|
|
656
|
+
errors.push("execution requires a runId");
|
|
657
|
+
if (candidate.sessionId !== sessionId) {
|
|
658
|
+
errors.push(`execution ${candidate.runId} belongs to another session`);
|
|
659
|
+
}
|
|
660
|
+
if (!isNonEmptyString(candidate.workspaceRoot)) {
|
|
661
|
+
errors.push(`execution ${candidate.runId} requires a workspaceRoot`);
|
|
662
|
+
}
|
|
663
|
+
if (!isNonEmptyString(candidate.executionKey)) {
|
|
664
|
+
errors.push(`execution ${candidate.runId} requires an executionKey`);
|
|
665
|
+
}
|
|
666
|
+
if (!isNonEmptyString(candidate.goal)) {
|
|
667
|
+
errors.push(`execution ${candidate.runId} requires a goal`);
|
|
668
|
+
}
|
|
669
|
+
if (candidate.state !== "preparing" &&
|
|
670
|
+
candidate.state !== "active" &&
|
|
671
|
+
candidate.state !== "sealed") {
|
|
672
|
+
errors.push(`execution ${candidate.runId} has invalid state ${String(candidate.state)}`);
|
|
673
|
+
}
|
|
674
|
+
if (!Number.isInteger(candidate.revision) || candidate.revision < 1) {
|
|
675
|
+
errors.push(`execution ${candidate.runId} requires a positive revision`);
|
|
676
|
+
}
|
|
677
|
+
if (!candidate.source || typeof candidate.source !== "object") {
|
|
678
|
+
errors.push(`execution ${candidate.runId} requires a source`);
|
|
679
|
+
}
|
|
680
|
+
else if (candidate.source.kind !== "native-package" &&
|
|
681
|
+
candidate.source.kind !== "provided-plan" &&
|
|
682
|
+
candidate.source.kind !== "conversation-scoped") {
|
|
683
|
+
errors.push(`execution ${candidate.runId} has an invalid source kind`);
|
|
684
|
+
}
|
|
685
|
+
if (!candidate.boundary ||
|
|
686
|
+
!Array.isArray(candidate.boundary.files) ||
|
|
687
|
+
!Array.isArray(candidate.boundary.directories)) {
|
|
688
|
+
errors.push(`execution ${candidate.runId} requires a boundary with files and directories`);
|
|
689
|
+
}
|
|
690
|
+
if (!Array.isArray(candidate.tasks) || candidate.tasks.length === 0) {
|
|
691
|
+
errors.push(`execution ${candidate.runId} requires at least one task binding`);
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
const ids = new Set();
|
|
695
|
+
for (const [taskId, binding] of candidate.tasks) {
|
|
696
|
+
if (ids.has(taskId))
|
|
697
|
+
errors.push(`execution ${candidate.runId} repeats task ${taskId}`);
|
|
698
|
+
ids.add(taskId);
|
|
699
|
+
if (!binding || binding.taskId !== taskId) {
|
|
700
|
+
errors.push(`execution ${candidate.runId} task ${taskId} binding mismatch`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (!Array.isArray(candidate.checkpoints)) {
|
|
705
|
+
errors.push(`execution ${candidate.runId} requires a checkpoints array`);
|
|
706
|
+
}
|
|
707
|
+
if (!Array.isArray(candidate.lineage)) {
|
|
708
|
+
errors.push(`execution ${candidate.runId} requires a lineage array`);
|
|
709
|
+
}
|
|
710
|
+
if (!Array.isArray(candidate.authority) || !Array.isArray(candidate.stageApprovals)) {
|
|
711
|
+
errors.push(`execution ${candidate.runId} requires authority and stageApprovals arrays`);
|
|
712
|
+
}
|
|
713
|
+
if (!Array.isArray(candidate.reserveDebits)) {
|
|
714
|
+
errors.push(`execution ${candidate.runId} requires a reserveDebits array`);
|
|
715
|
+
}
|
|
716
|
+
// Deep authority/debit/approval consistency: cross-record totals, exact
|
|
717
|
+
// bindings, and replay-unique identities within one execution.
|
|
718
|
+
const authorityIds = new Set();
|
|
719
|
+
let grantedUnits = 0;
|
|
720
|
+
for (const authority of candidate.authority) {
|
|
721
|
+
if (!authority || typeof authority !== "object") {
|
|
722
|
+
errors.push(`execution ${candidate.runId} has a malformed authority record`);
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const authorityId = authority.authorityId;
|
|
726
|
+
if (!isNonEmptyString(authorityId) || authorityIds.has(authorityId)) {
|
|
727
|
+
errors.push(`execution ${candidate.runId} requires unique authority ids`);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
authorityIds.add(authorityId);
|
|
731
|
+
if (authority.runId !== candidate.runId) {
|
|
732
|
+
errors.push(`authority ${authorityId} does not belong to execution ${candidate.runId}`);
|
|
733
|
+
}
|
|
734
|
+
if (!isNonEmptyString(authority.grantedByMessageId)) {
|
|
735
|
+
errors.push(`authority ${authorityId} lacks its granting message identity`);
|
|
736
|
+
}
|
|
737
|
+
if (!Number.isInteger(authority.initialUnits) || authority.initialUnits < 1) {
|
|
738
|
+
errors.push(`authority ${authorityId} requires a positive initial reserve`);
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
grantedUnits += authority.initialUnits;
|
|
742
|
+
}
|
|
743
|
+
if (!Array.isArray(authority.extensions) || !Array.isArray(authority.revocations)) {
|
|
744
|
+
errors.push(`authority ${authorityId} requires extension and revocation arrays`);
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
for (const extension of authority.extensions) {
|
|
748
|
+
if (!Number.isInteger(extension.units) || extension.units < 1) {
|
|
749
|
+
errors.push(`authority ${authorityId} has an invalid extension`);
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
grantedUnits += extension.units;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const debitIds = new Set();
|
|
758
|
+
let consumedUnits = 0;
|
|
759
|
+
for (const debit of candidate.reserveDebits) {
|
|
760
|
+
if (!debit || typeof debit !== "object") {
|
|
761
|
+
errors.push(`execution ${candidate.runId} has a malformed reserve debit`);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const recoveryId = debit.recoveryId;
|
|
765
|
+
if (!isNonEmptyString(recoveryId) || debitIds.has(recoveryId)) {
|
|
766
|
+
errors.push(`execution ${candidate.runId} requires unique reserve debit ids`);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
debitIds.add(recoveryId);
|
|
770
|
+
if (!authorityIds.has(debit.authorityId)) {
|
|
771
|
+
errors.push(`reserve debit ${recoveryId} references an unknown authority`);
|
|
772
|
+
}
|
|
773
|
+
if (!Number.isInteger(debit.units) || debit.units < 1) {
|
|
774
|
+
errors.push(`reserve debit ${recoveryId} requires a positive unit count`);
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
consumedUnits += debit.units;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (consumedUnits > grantedUnits) {
|
|
781
|
+
errors.push(`execution ${candidate.runId} consumed more reserve units than were granted`);
|
|
782
|
+
}
|
|
783
|
+
const approvalIds = new Set();
|
|
784
|
+
for (const approval of candidate.stageApprovals) {
|
|
785
|
+
if (!approval || typeof approval !== "object") {
|
|
786
|
+
errors.push(`execution ${candidate.runId} has a malformed stage approval`);
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (!isNonEmptyString(approval.approvalId) || approvalIds.has(approval.approvalId)) {
|
|
790
|
+
errors.push(`execution ${candidate.runId} requires unique stage approval ids`);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
approvalIds.add(approval.approvalId);
|
|
794
|
+
if (!authorityIds.has(approval.authorityId)) {
|
|
795
|
+
errors.push(`stage approval ${approval.approvalId} references an unknown authority`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (Array.isArray(candidate.checkpoints)) {
|
|
799
|
+
for (const [checkpointId, binding] of candidate.checkpoints) {
|
|
800
|
+
if (!binding || typeof binding !== "object") {
|
|
801
|
+
errors.push(`execution ${candidate.runId} checkpoint ${checkpointId} is malformed`);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (binding.status === "passed") {
|
|
805
|
+
if (!Number.isInteger(binding.attempts) || binding.attempts < 1) {
|
|
806
|
+
errors.push(`passed checkpoint ${checkpointId} requires a completed generation`);
|
|
807
|
+
}
|
|
808
|
+
if (!Number.isInteger(binding.passedRevision) || binding.passedRevision < 1) {
|
|
809
|
+
errors.push(`passed checkpoint ${checkpointId} requires a passing revision`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return errors.length === before;
|
|
815
|
+
}
|
|
816
|
+
// END_BLOCK_EXECUTION_SERIALIZATION
|
|
484
817
|
/**
|
|
485
818
|
* Resolve the per-session workflow data directory.
|
|
486
819
|
* Path: $XDG_DATA_HOME/vvoc/workflow/<sessionId>/
|
|
@@ -526,7 +859,10 @@ export function hydrateWorkflowStateChecked(sessionId) {
|
|
|
526
859
|
errors: [`workflow state is not valid JSON: ${error.message}`],
|
|
527
860
|
};
|
|
528
861
|
}
|
|
529
|
-
if (parsed.version !== 1 &&
|
|
862
|
+
if (parsed.version !== 1 &&
|
|
863
|
+
parsed.version !== 2 &&
|
|
864
|
+
parsed.version !== RECOVERY_STATE_VERSION &&
|
|
865
|
+
parsed.version !== PERSISTED_WORKFLOW_STATE_VERSION) {
|
|
530
866
|
return {
|
|
531
867
|
status: "invalid",
|
|
532
868
|
errors: [`unsupported persisted version ${String(parsed.version)}`],
|
|
@@ -535,11 +871,12 @@ export function hydrateWorkflowStateChecked(sessionId) {
|
|
|
535
871
|
if (!Array.isArray(parsed.records)) {
|
|
536
872
|
return { status: "invalid", errors: ["persisted records must be an array"] };
|
|
537
873
|
}
|
|
874
|
+
const version = parsed.version;
|
|
538
875
|
const errors = [];
|
|
539
876
|
const records = new Map();
|
|
540
877
|
for (const record of parsed.records) {
|
|
541
878
|
const recordErrors = [];
|
|
542
|
-
if (!isWorkItemRecord(record, sessionId, recordErrors)) {
|
|
879
|
+
if (!isWorkItemRecord(record, sessionId, version, recordErrors)) {
|
|
543
880
|
errors.push(...(recordErrors.length > 0
|
|
544
881
|
? recordErrors
|
|
545
882
|
: [
|
|
@@ -547,6 +884,12 @@ export function hydrateWorkflowStateChecked(sessionId) {
|
|
|
547
884
|
]));
|
|
548
885
|
continue;
|
|
549
886
|
}
|
|
887
|
+
// Versions 1 and 2 never carried recovery histories: hydrate them
|
|
888
|
+
// conservatively with empty histories rather than inferring grants, so
|
|
889
|
+
// their original budgets stay unchanged.
|
|
890
|
+
if (record.mode === "delegated" && record.delegated && version < RECOVERY_STATE_VERSION) {
|
|
891
|
+
record.delegated = { ...record.delegated, recoveryHistory: [] };
|
|
892
|
+
}
|
|
550
893
|
records.set(`${sessionId}::${record.workItemId}`, record);
|
|
551
894
|
}
|
|
552
895
|
const keyIndex = new Map();
|
|
@@ -556,37 +899,76 @@ export function hydrateWorkflowStateChecked(sessionId) {
|
|
|
556
899
|
const keyIndexBySession = new Map();
|
|
557
900
|
keyIndexBySession.set(sessionId, keyIndex);
|
|
558
901
|
const planRuns = new Map();
|
|
559
|
-
if (
|
|
902
|
+
if (version >= 2) {
|
|
560
903
|
if (!Array.isArray(parsed.planRuns)) {
|
|
561
904
|
return { status: "invalid", errors: ["version 2 state requires a planRuns array"] };
|
|
562
905
|
}
|
|
563
906
|
const recordsByBareId = new Map([...records.values()].map((record) => [record.workItemId, record]));
|
|
564
907
|
for (const serialized of parsed.planRuns) {
|
|
565
908
|
const runErrors = [];
|
|
566
|
-
if (!validatePlanRun(serialized, recordsByBareId, sessionId, runErrors)) {
|
|
909
|
+
if (!validatePlanRun(serialized, recordsByBareId, sessionId, version, runErrors)) {
|
|
567
910
|
errors.push(...runErrors);
|
|
568
911
|
continue;
|
|
569
912
|
}
|
|
570
913
|
const run = {
|
|
571
914
|
...serialized,
|
|
572
915
|
tasks: new Map(serialized.tasks.map((task) => [task.taskId, task])),
|
|
573
|
-
checkpoints: new Map(serialized.checkpoints.map((checkpoint) =>
|
|
916
|
+
checkpoints: new Map(serialized.checkpoints.map((checkpoint) => {
|
|
917
|
+
const hydrated = version < RECOVERY_STATE_VERSION
|
|
918
|
+
? { ...checkpoint, recoveryHistory: checkpoint.recoveryHistory ?? [] }
|
|
919
|
+
: checkpoint;
|
|
920
|
+
return [hydrated.checkpointId, hydrated];
|
|
921
|
+
})),
|
|
574
922
|
};
|
|
575
923
|
planRuns.set(run.runId, run);
|
|
576
924
|
}
|
|
577
925
|
}
|
|
926
|
+
const executions = new Map();
|
|
927
|
+
const messageClaims = new Map();
|
|
928
|
+
if (version >= PERSISTED_WORKFLOW_STATE_VERSION) {
|
|
929
|
+
if (!Array.isArray(parsed.executions)) {
|
|
930
|
+
return { status: "invalid", errors: ["version 4 state requires an executions array"] };
|
|
931
|
+
}
|
|
932
|
+
for (const serialized of parsed.executions) {
|
|
933
|
+
const executionErrors = [];
|
|
934
|
+
if (!validateWorkflowExecution(serialized, sessionId, executionErrors)) {
|
|
935
|
+
errors.push(...executionErrors);
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
executions.set(serialized.runId, {
|
|
939
|
+
...serialized,
|
|
940
|
+
tasks: new Map(serialized.tasks),
|
|
941
|
+
checkpoints: new Map(serialized.checkpoints),
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
for (const claim of parsed.messageClaims ?? []) {
|
|
945
|
+
if (!isNonEmptyString(claim?.messageId) || !isNonEmptyString(claim?.runId)) {
|
|
946
|
+
errors.push("message claims require messageId and runId");
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (messageClaims.has(claim.messageId)) {
|
|
950
|
+
errors.push(`message ${claim.messageId} is claimed more than once`);
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
messageClaims.set(claim.messageId, claim);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
578
956
|
if (errors.length > 0) {
|
|
579
957
|
return { status: "invalid", errors };
|
|
580
958
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
},
|
|
959
|
+
const data = {
|
|
960
|
+
nextId: typeof parsed.nextId === "number" ? parsed.nextId : records.size + 1,
|
|
961
|
+
records,
|
|
962
|
+
keyIndexBySession,
|
|
963
|
+
planRuns,
|
|
964
|
+
executions,
|
|
965
|
+
messageClaims,
|
|
589
966
|
};
|
|
967
|
+
// Materialize compatibility registry entries for existing native plan runs
|
|
968
|
+
// that predate the common registry; this never invents authority or alters
|
|
969
|
+
// native counters.
|
|
970
|
+
ensureNativeExecutions(data);
|
|
971
|
+
return { status: "valid", data };
|
|
590
972
|
}
|
|
591
973
|
// START_CONTRACT: hydrateWorkflowState
|
|
592
974
|
// PURPOSE: Legacy nullable hydrate kept for compatibility with existing callers.
|
|
@@ -631,16 +1013,31 @@ export function snapshotWorkflowStateChecked(sessionId, data) {
|
|
|
631
1013
|
planRuns.push(serializePlanRun(run));
|
|
632
1014
|
}
|
|
633
1015
|
}
|
|
1016
|
+
const executions = [];
|
|
1017
|
+
const sessionRunIds = new Set();
|
|
1018
|
+
for (const execution of data.executions.values()) {
|
|
1019
|
+
if (execution.sessionId === sessionId) {
|
|
1020
|
+
executions.push(serializeWorkflowExecution(execution));
|
|
1021
|
+
sessionRunIds.add(execution.runId);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const messageClaims = [];
|
|
1025
|
+
for (const claim of data.messageClaims.values()) {
|
|
1026
|
+
if (sessionRunIds.has(claim.runId))
|
|
1027
|
+
messageClaims.push(claim);
|
|
1028
|
+
}
|
|
634
1029
|
const persisted = {
|
|
635
|
-
// Version
|
|
636
|
-
//
|
|
637
|
-
version:
|
|
1030
|
+
// Version 4 adds the common execution registry and session-wide message
|
|
1031
|
+
// claims on top of version 3's recovery histories and report rejections.
|
|
1032
|
+
version: PERSISTED_WORKFLOW_STATE_VERSION,
|
|
638
1033
|
updatedAt: new Date().toISOString(),
|
|
639
1034
|
sessionId,
|
|
640
1035
|
nextId: data.nextId,
|
|
641
1036
|
records,
|
|
642
1037
|
keyIndex,
|
|
643
1038
|
planRuns,
|
|
1039
|
+
executions,
|
|
1040
|
+
messageClaims,
|
|
644
1041
|
};
|
|
645
1042
|
const targetPath = getWorkflowStatePath(sessionId);
|
|
646
1043
|
const temporaryPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|