@osovv/vv-opencode 1.4.1 → 1.4.3-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +17 -5
- package/dist/lib/orchestration.js +8 -3
- package/dist/lib/orchestration.js.map +1 -1
- package/dist/plugins/system-context-injection/index.js +17 -8
- package/dist/plugins/system-context-injection/index.js.map +1 -1
- package/dist/plugins/workflow/checkpoints.d.ts +53 -1
- package/dist/plugins/workflow/checkpoints.js +350 -33
- package/dist/plugins/workflow/checkpoints.js.map +1 -1
- package/dist/plugins/workflow/delegated.d.ts +144 -2
- package/dist/plugins/workflow/delegated.js +542 -12
- package/dist/plugins/workflow/delegated.js.map +1 -1
- package/dist/plugins/workflow/index.js +269 -20
- package/dist/plugins/workflow/index.js.map +1 -1
- package/dist/plugins/workflow/persistence.d.ts +6 -4
- package/dist/plugins/workflow/persistence.js +192 -31
- package/dist/plugins/workflow/persistence.js.map +1 -1
- package/dist/plugins/workflow/repair.d.ts +3 -0
- package/dist/plugins/workflow/repair.js +39 -31
- package/dist/plugins/workflow/repair.js.map +1 -1
- package/dist/plugins/workflow/state.js +16 -3
- package/dist/plugins/workflow/state.js.map +1 -1
- package/dist/plugins/workflow/tooling.d.ts +36 -6
- package/dist/plugins/workflow/tooling.js +137 -20
- package/dist/plugins/workflow/tooling.js.map +1 -1
- package/package.json +1 -1
- package/schemas/vvoc/v3.json +1 -1
- package/templates/agents/investigator.md +3 -0
- package/templates/agents/vv-code-reviewer.md +10 -3
- package/templates/agents/vv-controller.md +28 -6
- package/templates/agents/vv-implementer.md +6 -3
- package/templates/agents/vv-spec-reviewer.md +4 -0
- package/templates/skills/vv-execute/SKILL.md +11 -7
- package/templates/skills/vv-plan/SKILL.md +3 -1
- package/templates/skills/vv-review/SKILL.md +2 -0
- package/templates/skills/vv-spec/SKILL.md +2 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// FILE: src/plugins/workflow/checkpoints.ts
|
|
2
|
-
// VERSION:
|
|
2
|
+
// VERSION: 2.0.0
|
|
3
3
|
// START_MODULE_CONTRACT
|
|
4
|
-
// PURPOSE: Registered delegated plan runs with declared review checkpoints, generation-bound reviewer linkage, fingerprint-verified outcomes, and
|
|
5
|
-
// SCOPE: Atomic plan-run registration binding canonical task ids to stable delegated work items, idempotent re-registration with explicit drift rejection, checkpoint start with prerequisite acceptance and fresh fingerprints through linked review_only work items, reviewer launch/result recording bound to the current generation, verify deriving passed/failed/stale/stopped outcomes from recorded results plus recomputed fingerprints and approval-input hashes, final complete sealing, failed-checkpoint rework authorization, wave-barrier and overlapping-write gates, and read-only run views. No agent dispatch or command execution.
|
|
4
|
+
// PURPOSE: Registered delegated plan runs with declared review checkpoints, generation-bound reviewer linkage, fingerprint-verified outcomes, rework authorization, and bounded checkpoint recovery.
|
|
5
|
+
// SCOPE: Atomic plan-run registration binding canonical task ids to stable delegated work items, idempotent re-registration with explicit drift rejection, checkpoint start with prerequisite acceptance and fresh fingerprints through linked review_only work items, reviewer launch/result recording bound to the current generation, verify deriving passed/failed/stale/stopped outcomes from recorded results plus recomputed fingerprints and approval-input hashes, final complete sealing, failed-checkpoint rework authorization, bounded recovery of stopped or generation-exhausted checkpoints (settling a stopped generation as historical evidence and granting at most one additional generation per autonomous or replay-protected root-user-authorized unit), wave-barrier and overlapping-write gates, and read-only run views exposing generation budgets and supported next actions. No agent dispatch or command execution.
|
|
6
6
|
// DEPENDS: [node:crypto, src/plugins/workflow/checkpoint-io.ts, src/plugins/workflow/delegated.ts, src/plugins/workflow/snapshots.ts, src/plugins/workflow/state.ts]
|
|
7
7
|
// LINKS: [M-WORKFLOW-CHECKPOINTS, M-WORKFLOW-DELEGATED, M-WORKFLOW-SNAPSHOTS, M-WORKFLOW-STATE, M-SPEC-LINT, V-M-WORKFLOW-CHECKPOINTS]
|
|
8
8
|
// ROLE: RUNTIME
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
// END_MODULE_CONTRACT
|
|
11
11
|
//
|
|
12
12
|
// START_MODULE_MAP
|
|
13
|
-
// MAX_CHECKPOINT_REVIEW_ATTEMPTS - Maximum review generations per checkpoint (initial plus one correction).
|
|
13
|
+
// MAX_CHECKPOINT_REVIEW_ATTEMPTS - Maximum ordinary review generations per checkpoint (initial plus one correction).
|
|
14
14
|
// DelegatedReviewerOutcome - One reviewer's recorded outcome inside a generation.
|
|
15
15
|
// DelegatedCheckpointReview - Current in-flight review generation state.
|
|
16
16
|
// DelegatedCheckpointHistoryEntry - One settled generation outcome as historical evidence.
|
|
17
|
-
//
|
|
17
|
+
// DelegatedCheckpointRecoveryRecord - One bounded checkpoint recovery event bound to its settled generation.
|
|
18
|
+
// DelegatedRunCheckpoint - Registered checkpoint runtime state with recovery history.
|
|
18
19
|
// DelegatedRunTask - Canonical task to work-item binding.
|
|
19
20
|
// DelegatedPlanRun - Registered plan-run registry entry.
|
|
20
21
|
// RegisterDelegatedPlanInput - Registration input carrying the loaded approved plan.
|
|
@@ -27,6 +28,12 @@
|
|
|
27
28
|
// VerifyDelegatedCheckpointResult - Derived verify outcome and optional sealed completion.
|
|
28
29
|
// AuthorizeReworkInput - Failed-checkpoint rework authorization request.
|
|
29
30
|
// AuthorizeReworkResult - Rework authorization outcome delegating to the guarded reducer.
|
|
31
|
+
// RecoverDelegatedCheckpointInput - Bounded checkpoint recovery request with optional user authorization.
|
|
32
|
+
// RecoverDelegatedCheckpointResult - Checkpoint recovery outcome with generation budget, or a coded rejection.
|
|
33
|
+
// checkpointRecoveryGrantCount - Number of budget-granting checkpoint recovery entries.
|
|
34
|
+
// checkpointGenerationBudget - Allowed generation count given consumed generations and recovery grants.
|
|
35
|
+
// checkpointAutonomousGrantConsumed - Whether the single autonomous checkpoint grant is already recorded.
|
|
36
|
+
// cloneDelegatedPlanRun - Deep clone of one registered run for staged persistence commits.
|
|
30
37
|
// checkpointBarrierUnsatisfied - Unsatisfied barriers blocking a wave's task launches.
|
|
31
38
|
// findOverlappingInFlightReview - In-flight checkpoint scope overlap detection for declared writes.
|
|
32
39
|
// getDelegatedRunView - Read-only run serialization for tooling output.
|
|
@@ -42,15 +49,17 @@
|
|
|
42
49
|
// verifyDelegatedCheckpointInStore - Store-level verify used by the plugin and tests.
|
|
43
50
|
// authorizeReworkFromFailedCheckpoint - Validate failed-checkpoint rework authorization.
|
|
44
51
|
// authorizeReworkFromFailedCheckpointInStore - Store-level rework authorization.
|
|
52
|
+
// recoverDelegatedCheckpoint - Resume a stopped generation or grant exactly one further generation after exhaustion.
|
|
53
|
+
// recoverDelegatedCheckpointInStore - Store-level guarded checkpoint recovery reducer.
|
|
45
54
|
// END_MODULE_MAP
|
|
46
55
|
//
|
|
47
56
|
// START_CHANGE_SUMMARY
|
|
48
|
-
// LAST_CHANGE: [C-
|
|
57
|
+
// LAST_CHANGE: [C-WORKFLOW-BOUNDED-RECOVERY-R1 - Added bounded checkpoint recovery: stopped generations settle as historical stopped evidence, exhausted checkpoints may grant one further generation per autonomous or root-user-authorized unit, and run views expose generation budgets and next actions.]
|
|
49
58
|
// END_CHANGE_SUMMARY
|
|
50
59
|
import { createHash } from "node:crypto";
|
|
51
60
|
import { readFile } from "node:fs/promises";
|
|
52
61
|
import { contentSha256 } from "./checkpoint-io.js";
|
|
53
|
-
import { currentDelegatedAcceptance, reworkDelegatedWorkItem, validateDelegatedWriteScope, } from "./delegated.js";
|
|
62
|
+
import { currentDelegatedAcceptance, reworkDelegatedWorkItem, validateDelegatedRecoveryInput, validateDelegatedWriteScope, validateRecoveryUserAuthorization, } from "./delegated.js";
|
|
54
63
|
import { captureWorkflowSnapshot } from "./snapshots.js";
|
|
55
64
|
import { cloneRecord, createRecordLookupKey, } from "./state.js";
|
|
56
65
|
// START_BLOCK_REGISTRY_TYPES
|
|
@@ -82,6 +91,10 @@ function cloneCheckpoint(checkpoint) {
|
|
|
82
91
|
scope: [...checkpoint.scope],
|
|
83
92
|
reviewers: [...checkpoint.reviewers],
|
|
84
93
|
history: checkpoint.history.map((entry) => ({ ...entry })),
|
|
94
|
+
recoveryHistory: checkpoint.recoveryHistory.map((recovery) => ({
|
|
95
|
+
...recovery,
|
|
96
|
+
verification: [...recovery.verification],
|
|
97
|
+
})),
|
|
85
98
|
...(checkpoint.currentReview
|
|
86
99
|
? {
|
|
87
100
|
currentReview: {
|
|
@@ -94,6 +107,10 @@ function cloneCheckpoint(checkpoint) {
|
|
|
94
107
|
: {}),
|
|
95
108
|
};
|
|
96
109
|
}
|
|
110
|
+
/** Deep clone of one registered run for staged persistence commits. */
|
|
111
|
+
export function cloneDelegatedPlanRun(run) {
|
|
112
|
+
return cloneRun(run);
|
|
113
|
+
}
|
|
97
114
|
function cloneRun(run) {
|
|
98
115
|
return {
|
|
99
116
|
...run,
|
|
@@ -101,6 +118,19 @@ function cloneRun(run) {
|
|
|
101
118
|
checkpoints: new Map([...run.checkpoints].map(([id, checkpoint]) => [id, cloneCheckpoint(checkpoint)])),
|
|
102
119
|
};
|
|
103
120
|
}
|
|
121
|
+
/** Count of budget-granting checkpoint recovery entries. */
|
|
122
|
+
export function checkpointRecoveryGrantCount(history) {
|
|
123
|
+
return history.filter((entry) => entry.kind === "autonomous_grant" || entry.kind === "user_grant")
|
|
124
|
+
.length;
|
|
125
|
+
}
|
|
126
|
+
/** Allowed generation count: the ordinary maximum plus one per recovery grant. */
|
|
127
|
+
export function checkpointGenerationBudget(checkpoint) {
|
|
128
|
+
return MAX_CHECKPOINT_REVIEW_ATTEMPTS + checkpointRecoveryGrantCount(checkpoint.recoveryHistory);
|
|
129
|
+
}
|
|
130
|
+
/** Whether the single autonomous checkpoint grant is already recorded. */
|
|
131
|
+
export function checkpointAutonomousGrantConsumed(history) {
|
|
132
|
+
return history.some((entry) => entry.kind === "autonomous_grant");
|
|
133
|
+
}
|
|
104
134
|
function waveIndexOf(run, wave) {
|
|
105
135
|
return run.definition.waves.indexOf(wave);
|
|
106
136
|
}
|
|
@@ -220,6 +250,7 @@ export function registerDelegatedPlanInStore(data, input) {
|
|
|
220
250
|
status: "pending",
|
|
221
251
|
attempts: 0,
|
|
222
252
|
history: [],
|
|
253
|
+
recoveryHistory: [],
|
|
223
254
|
});
|
|
224
255
|
}
|
|
225
256
|
const finalCheckpoint = input.plan.definition.checkpoints.find((c) => c.kind === "final");
|
|
@@ -295,6 +326,7 @@ function openInStore(data, input) {
|
|
|
295
326
|
decisions: [],
|
|
296
327
|
acceptances: [],
|
|
297
328
|
reworkHistory: [],
|
|
329
|
+
recoveryHistory: [],
|
|
298
330
|
},
|
|
299
331
|
completedReviewRoundCount: 0,
|
|
300
332
|
specReviewCount: 0,
|
|
@@ -376,11 +408,11 @@ export async function startDelegatedCheckpointInStore(data, input) {
|
|
|
376
408
|
message: `ALREADY_IN_REVIEW: ${input.checkpointId} generation ${checkpoint.currentReview?.generation} is in flight`,
|
|
377
409
|
};
|
|
378
410
|
}
|
|
379
|
-
if (checkpoint.attempts >=
|
|
411
|
+
if (checkpoint.attempts >= checkpointGenerationBudget(checkpoint)) {
|
|
380
412
|
return {
|
|
381
413
|
ok: false,
|
|
382
414
|
errorCode: "ATTEMPTS_EXHAUSTED",
|
|
383
|
-
message: `ATTEMPTS_EXHAUSTED: ${input.checkpointId} consumed ${checkpoint.attempts} review generations; explicit recovery is required`,
|
|
415
|
+
message: `ATTEMPTS_EXHAUSTED: ${input.checkpointId} consumed ${checkpoint.attempts} of ${checkpointGenerationBudget(checkpoint)} allowed review generations; explicit recovery is required`,
|
|
384
416
|
};
|
|
385
417
|
}
|
|
386
418
|
const covered = currentCoveredAttemptIds(data, run, checkpoint);
|
|
@@ -984,6 +1016,255 @@ export function authorizeReworkFromFailedCheckpointInStore(data, input) {
|
|
|
984
1016
|
}
|
|
985
1017
|
return { ok: true, reworkId: reworked.reworkId, grantedAttempts: reworked.grantedAttempts };
|
|
986
1018
|
}
|
|
1019
|
+
/** Timestamp (epoch ms) of the most recent settled evidence in a checkpoint. */
|
|
1020
|
+
function checkpointStopTimeMs(checkpoint) {
|
|
1021
|
+
if (checkpoint.currentReview) {
|
|
1022
|
+
const recorded = Object.values(checkpoint.currentReview.results)
|
|
1023
|
+
.map((result) => (result ? Date.parse(result.recordedAt) : Number.NaN))
|
|
1024
|
+
.filter((value) => Number.isFinite(value));
|
|
1025
|
+
if (recorded.length > 0)
|
|
1026
|
+
return Math.max(...recorded);
|
|
1027
|
+
}
|
|
1028
|
+
const last = checkpoint.history[checkpoint.history.length - 1];
|
|
1029
|
+
if (last) {
|
|
1030
|
+
const parsed = Date.parse(last.completedAt);
|
|
1031
|
+
if (Number.isFinite(parsed))
|
|
1032
|
+
return parsed;
|
|
1033
|
+
}
|
|
1034
|
+
return undefined;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Synchronous eligibility check for checkpoint recovery: a stopped generation
|
|
1038
|
+
* (NEEDS_CONTEXT settled nothing) or a generation-exhausted checkpoint. Live
|
|
1039
|
+
* reviews, pending checkpoints, and ordinary-budget failures are not
|
|
1040
|
+
* recoverable targets.
|
|
1041
|
+
*/
|
|
1042
|
+
function precheckCheckpointRecovery(data, input) {
|
|
1043
|
+
const run = findRun(data, input.runId);
|
|
1044
|
+
if (!run) {
|
|
1045
|
+
return { ok: false, errorCode: "RUN_NOT_FOUND", message: `RUN_NOT_FOUND: ${input.runId}` };
|
|
1046
|
+
}
|
|
1047
|
+
if (run.sessionId !== input.sessionId) {
|
|
1048
|
+
return {
|
|
1049
|
+
ok: false,
|
|
1050
|
+
errorCode: "SESSION_MISMATCH",
|
|
1051
|
+
message: `SESSION_MISMATCH: run ${input.runId} belongs to session ${run.sessionId}`,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
if (run.status === "sealed") {
|
|
1055
|
+
return {
|
|
1056
|
+
ok: false,
|
|
1057
|
+
errorCode: "RUN_SEALED",
|
|
1058
|
+
message: `RUN_SEALED: run ${input.runId} is complete; recovery requires a new change`,
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
const checkpoint = run.checkpoints.get(input.checkpointId);
|
|
1062
|
+
if (!checkpoint) {
|
|
1063
|
+
return {
|
|
1064
|
+
ok: false,
|
|
1065
|
+
errorCode: "CHECKPOINT_NOT_FOUND",
|
|
1066
|
+
message: `CHECKPOINT_NOT_FOUND: ${input.checkpointId}`,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
if (checkpoint.status === "passed") {
|
|
1070
|
+
return {
|
|
1071
|
+
ok: false,
|
|
1072
|
+
errorCode: "ALREADY_PASSED",
|
|
1073
|
+
message: `ALREADY_PASSED: ${input.checkpointId} passed at generation ${checkpoint.attempts}`,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
const stopped = checkpoint.status === "in_review" &&
|
|
1077
|
+
checkpoint.lastOutcome === "stopped" &&
|
|
1078
|
+
checkpoint.currentReview !== undefined;
|
|
1079
|
+
const budget = checkpointGenerationBudget(checkpoint);
|
|
1080
|
+
const exhausted = checkpoint.status === "failed" && checkpoint.attempts >= budget;
|
|
1081
|
+
if (!stopped && !exhausted) {
|
|
1082
|
+
if (checkpoint.status === "in_review") {
|
|
1083
|
+
return {
|
|
1084
|
+
ok: false,
|
|
1085
|
+
errorCode: "INVALID_TARGET_STATE",
|
|
1086
|
+
message: `INVALID_TARGET_STATE: ${input.checkpointId} generation ${checkpoint.currentReview?.generation} is live; recovery never interrupts in-flight review work`,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
if (checkpoint.status === "failed") {
|
|
1090
|
+
return {
|
|
1091
|
+
ok: false,
|
|
1092
|
+
errorCode: "INVALID_TARGET_STATE",
|
|
1093
|
+
message: `INVALID_TARGET_STATE: ${input.checkpointId} still has ${budget - checkpoint.attempts} ordinary generation(s) available; start the next generation instead of recovering`,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
ok: false,
|
|
1098
|
+
errorCode: "INVALID_TARGET_STATE",
|
|
1099
|
+
message: `INVALID_TARGET_STATE: ${input.checkpointId} is ${checkpoint.status} and has nothing to recover`,
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
if (checkpoint.recoveryHistory.some((entry) => entry.recoveryId === input.recoveryId)) {
|
|
1103
|
+
return {
|
|
1104
|
+
ok: false,
|
|
1105
|
+
errorCode: "DUPLICATE_RECOVERY_ID",
|
|
1106
|
+
message: `DUPLICATE_RECOVERY_ID: recoveryId ${input.recoveryId} is already recorded for ${input.checkpointId}`,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
return {
|
|
1110
|
+
ok: true,
|
|
1111
|
+
checkpoint,
|
|
1112
|
+
stopped,
|
|
1113
|
+
needsGrant: exhausted || checkpoint.attempts >= budget,
|
|
1114
|
+
autonomousAvailable: !checkpointAutonomousGrantConsumed(checkpoint.recoveryHistory),
|
|
1115
|
+
stopTimeMs: checkpointStopTimeMs(checkpoint),
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
export async function recoverDelegatedCheckpoint(store, input) {
|
|
1119
|
+
return recoverDelegatedCheckpointInStore(store.getStoreData(), input);
|
|
1120
|
+
}
|
|
1121
|
+
export async function recoverDelegatedCheckpointInStore(data, input) {
|
|
1122
|
+
const validation = validateDelegatedRecoveryInput({
|
|
1123
|
+
diagnosis: input.diagnosis,
|
|
1124
|
+
changedCondition: input.changedCondition,
|
|
1125
|
+
verification: input.verification,
|
|
1126
|
+
recoveryId: input.recoveryId,
|
|
1127
|
+
});
|
|
1128
|
+
if (!validation.ok) {
|
|
1129
|
+
return {
|
|
1130
|
+
ok: false,
|
|
1131
|
+
errorCode: "INVALID_INPUT",
|
|
1132
|
+
message: `INVALID_INPUT: ${validation.message}`,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
const precheck = precheckCheckpointRecovery(data, input);
|
|
1136
|
+
if (!precheck.ok) {
|
|
1137
|
+
return { ok: false, errorCode: precheck.errorCode, message: precheck.message };
|
|
1138
|
+
}
|
|
1139
|
+
const commitRecovery = (kind, validatedMessageTimeMs) => {
|
|
1140
|
+
// Re-run the synchronous eligibility check immediately before committing
|
|
1141
|
+
// so an authorization await can never commit against a changed target.
|
|
1142
|
+
const fresh = precheckCheckpointRecovery(data, input);
|
|
1143
|
+
if (!fresh.ok) {
|
|
1144
|
+
return { ok: false, errorCode: fresh.errorCode, message: fresh.message };
|
|
1145
|
+
}
|
|
1146
|
+
// The authorization message was timed against the pre-await stop; if the
|
|
1147
|
+
// target moved to a newer generation during the await, that timing no
|
|
1148
|
+
// longer authorizes this recovery.
|
|
1149
|
+
if (validatedMessageTimeMs !== undefined &&
|
|
1150
|
+
fresh.stopTimeMs !== undefined &&
|
|
1151
|
+
validatedMessageTimeMs < fresh.stopTimeMs) {
|
|
1152
|
+
return {
|
|
1153
|
+
ok: false,
|
|
1154
|
+
errorCode: "AUTHORIZATION_STALE",
|
|
1155
|
+
message: `AUTHORIZATION_STALE: message ${String(input.userMessageId)} predates the stop it must authorize`,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
const now = toIsoNow();
|
|
1159
|
+
const stoppedReview = fresh.stopped ? fresh.checkpoint.currentReview : undefined;
|
|
1160
|
+
const recovery = {
|
|
1161
|
+
recoveryId: input.recoveryId,
|
|
1162
|
+
targetGeneration: stoppedReview?.generation ?? fresh.checkpoint.attempts,
|
|
1163
|
+
kind,
|
|
1164
|
+
diagnosis: input.diagnosis.trim(),
|
|
1165
|
+
changedCondition: input.changedCondition.trim(),
|
|
1166
|
+
verification: input.verification.map((reference) => reference.trim()),
|
|
1167
|
+
recoveredAt: now,
|
|
1168
|
+
...(kind === "user_grant" && input.userMessageId
|
|
1169
|
+
? { userMessageId: input.userMessageId }
|
|
1170
|
+
: {}),
|
|
1171
|
+
};
|
|
1172
|
+
let updated;
|
|
1173
|
+
if (stoppedReview) {
|
|
1174
|
+
// Settle the stopped generation as historical stopped evidence. The
|
|
1175
|
+
// FAIL-less stop is not converted into a failure: rework stays locked
|
|
1176
|
+
// until a real failed generation exists.
|
|
1177
|
+
updated = {
|
|
1178
|
+
...fresh.checkpoint,
|
|
1179
|
+
status: "failed",
|
|
1180
|
+
lastOutcome: "stopped",
|
|
1181
|
+
currentReview: undefined,
|
|
1182
|
+
history: [
|
|
1183
|
+
...fresh.checkpoint.history,
|
|
1184
|
+
{
|
|
1185
|
+
generation: stoppedReview.generation,
|
|
1186
|
+
outcome: "stopped",
|
|
1187
|
+
fingerprint: stoppedReview.startFingerprint,
|
|
1188
|
+
completedAt: now,
|
|
1189
|
+
},
|
|
1190
|
+
],
|
|
1191
|
+
recoveryHistory: [...fresh.checkpoint.recoveryHistory, recovery],
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
else {
|
|
1195
|
+
updated = {
|
|
1196
|
+
...fresh.checkpoint,
|
|
1197
|
+
recoveryHistory: [...fresh.checkpoint.recoveryHistory, recovery],
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
findRun(data, input.runId).checkpoints.set(input.checkpointId, updated);
|
|
1201
|
+
const generationBudget = checkpointGenerationBudget(updated);
|
|
1202
|
+
return {
|
|
1203
|
+
ok: true,
|
|
1204
|
+
checkpoint: cloneCheckpoint(updated),
|
|
1205
|
+
recoveryId: input.recoveryId,
|
|
1206
|
+
kind,
|
|
1207
|
+
generationBudget,
|
|
1208
|
+
...(stoppedReview ? { settledStoppedGeneration: stoppedReview.generation } : {}),
|
|
1209
|
+
};
|
|
1210
|
+
};
|
|
1211
|
+
if (!precheck.needsGrant) {
|
|
1212
|
+
return commitRecovery("resume");
|
|
1213
|
+
}
|
|
1214
|
+
if (input.userMessageId !== undefined) {
|
|
1215
|
+
const userMessageId = input.userMessageId.trim();
|
|
1216
|
+
if (!userMessageId) {
|
|
1217
|
+
return {
|
|
1218
|
+
ok: false,
|
|
1219
|
+
errorCode: "INVALID_INPUT",
|
|
1220
|
+
message: "INVALID_INPUT: userMessageId must be a non-empty string when provided",
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
if (precheck.checkpoint.recoveryHistory.some((entry) => entry.userMessageId === userMessageId)) {
|
|
1224
|
+
return {
|
|
1225
|
+
ok: false,
|
|
1226
|
+
errorCode: "AUTHORIZATION_REUSED",
|
|
1227
|
+
message: `AUTHORIZATION_REUSED: message ${userMessageId} already authorized recovery of ${input.checkpointId}; each message grants at most one unit per target`,
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
if (typeof input.lookupUserMessage !== "function") {
|
|
1231
|
+
return {
|
|
1232
|
+
ok: false,
|
|
1233
|
+
errorCode: "AUTHORIZATION_LOOKUP_FAILED",
|
|
1234
|
+
message: "AUTHORIZATION_LOOKUP_FAILED: user-authorized recovery requires a read-only message lookup bound to the plugin context",
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
const authorization = await validateRecoveryUserAuthorization({
|
|
1238
|
+
owningSessionId: input.sessionId,
|
|
1239
|
+
userMessageId,
|
|
1240
|
+
requireAfterMs: precheck.stopTimeMs,
|
|
1241
|
+
lookup: input.lookupUserMessage,
|
|
1242
|
+
});
|
|
1243
|
+
if (!authorization.ok) {
|
|
1244
|
+
return { ok: false, errorCode: authorization.errorCode, message: authorization.message };
|
|
1245
|
+
}
|
|
1246
|
+
// The await above is an async boundary: re-verify non-reuse before commit
|
|
1247
|
+
// (the precheck inside commitRecovery covers the rest of the state).
|
|
1248
|
+
const freshCheckpoint = findRun(data, input.runId)?.checkpoints.get(input.checkpointId);
|
|
1249
|
+
if (freshCheckpoint?.recoveryHistory.some((entry) => entry.userMessageId === userMessageId)) {
|
|
1250
|
+
return {
|
|
1251
|
+
ok: false,
|
|
1252
|
+
errorCode: "AUTHORIZATION_REUSED",
|
|
1253
|
+
message: `AUTHORIZATION_REUSED: message ${userMessageId} already authorized recovery of ${input.checkpointId}`,
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
return commitRecovery("user_grant", authorization.timeCreatedMs);
|
|
1257
|
+
}
|
|
1258
|
+
if (precheck.autonomousAvailable) {
|
|
1259
|
+
return commitRecovery("autonomous_grant");
|
|
1260
|
+
}
|
|
1261
|
+
return {
|
|
1262
|
+
ok: false,
|
|
1263
|
+
errorCode: "AUTONOMOUS_GRANT_EXHAUSTED",
|
|
1264
|
+
message: `AUTONOMOUS_GRANT_EXHAUSTED: the single autonomous recovery grant for ${input.checkpointId} is consumed; one further generation requires a fresh root-user message referenced by userMessageId`,
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
// END_BLOCK_CHECKPOINT_RECOVERY
|
|
987
1268
|
// START_BLOCK_BARRIER_GATES
|
|
988
1269
|
/** Unsatisfied checkpoint barriers blocking task launches for the given wave, in declaration order. */
|
|
989
1270
|
export function checkpointBarrierUnsatisfied(data, runId, wave) {
|
|
@@ -1018,8 +1299,37 @@ export function findOverlappingInFlightReview(data, runId, writeScope) {
|
|
|
1018
1299
|
}
|
|
1019
1300
|
return undefined;
|
|
1020
1301
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1302
|
+
function checkpointNextAction(checkpoint) {
|
|
1303
|
+
const generationBudget = checkpointGenerationBudget(checkpoint);
|
|
1304
|
+
const remainingGenerations = Math.max(0, generationBudget - checkpoint.attempts);
|
|
1305
|
+
if (checkpoint.status === "passed") {
|
|
1306
|
+
return { generationBudget, remainingGenerations, nextAction: "passed" };
|
|
1307
|
+
}
|
|
1308
|
+
if (checkpoint.status === "in_review") {
|
|
1309
|
+
if (checkpoint.lastOutcome === "stopped") {
|
|
1310
|
+
// The linked review item is hard-stopped: re-running verify is a no-op,
|
|
1311
|
+
// so the supported action is bounded recovery, never collection.
|
|
1312
|
+
return { generationBudget, remainingGenerations, nextAction: "recover" };
|
|
1313
|
+
}
|
|
1314
|
+
return { generationBudget, remainingGenerations, nextAction: "collect_and_verify" };
|
|
1315
|
+
}
|
|
1316
|
+
if (checkpoint.status === "pending") {
|
|
1317
|
+
return { generationBudget, remainingGenerations, nextAction: "start" };
|
|
1318
|
+
}
|
|
1319
|
+
// Failed: suggest an ordinary next generation while budget remains, then
|
|
1320
|
+
// bounded recovery, then user-authorized recovery after the autonomous
|
|
1321
|
+
// grant is consumed. The suggestion never proposes a start the same state
|
|
1322
|
+
// would immediately reject.
|
|
1323
|
+
if (remainingGenerations > 0) {
|
|
1324
|
+
return { generationBudget, remainingGenerations, nextAction: "start_next_generation" };
|
|
1325
|
+
}
|
|
1326
|
+
const autonomousAvailable = !checkpointAutonomousGrantConsumed(checkpoint.recoveryHistory);
|
|
1327
|
+
return {
|
|
1328
|
+
generationBudget,
|
|
1329
|
+
remainingGenerations,
|
|
1330
|
+
nextAction: autonomousAvailable ? "recover" : "recover_with_user_authorization",
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1023
1333
|
/** Read-only run serialization for tooling output and persistence consumers. */
|
|
1024
1334
|
export function getDelegatedRunView(data, runId) {
|
|
1025
1335
|
const run = findRun(data, runId);
|
|
@@ -1039,28 +1349,35 @@ export function getDelegatedRunView(data, runId) {
|
|
|
1039
1349
|
taskId: task.taskId,
|
|
1040
1350
|
workItemId: task.workItemId,
|
|
1041
1351
|
})),
|
|
1042
|
-
checkpoints: [...run.checkpoints.values()].map((checkpoint) =>
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
? {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1352
|
+
checkpoints: [...run.checkpoints.values()].map((checkpoint) => {
|
|
1353
|
+
const progress = checkpointNextAction(checkpoint);
|
|
1354
|
+
return {
|
|
1355
|
+
checkpointId: checkpoint.checkpointId,
|
|
1356
|
+
kind: checkpoint.kind,
|
|
1357
|
+
afterWave: checkpoint.afterWave,
|
|
1358
|
+
covers: checkpoint.covers,
|
|
1359
|
+
scope: checkpoint.scope,
|
|
1360
|
+
reviewers: checkpoint.reviewers,
|
|
1361
|
+
status: checkpoint.status,
|
|
1362
|
+
attempts: checkpoint.attempts,
|
|
1363
|
+
...(checkpoint.lastOutcome ? { lastOutcome: checkpoint.lastOutcome } : {}),
|
|
1364
|
+
generationBudget: progress.generationBudget,
|
|
1365
|
+
remainingGenerations: progress.remainingGenerations,
|
|
1366
|
+
recoveryCount: checkpointRecoveryGrantCount(checkpoint.recoveryHistory),
|
|
1367
|
+
nextAction: progress.nextAction,
|
|
1368
|
+
...(checkpoint.currentReview
|
|
1369
|
+
? {
|
|
1370
|
+
currentReview: {
|
|
1371
|
+
reviewWorkItemId: checkpoint.currentReview.reviewWorkItemId,
|
|
1372
|
+
generation: checkpoint.currentReview.generation,
|
|
1373
|
+
coveredAttemptIds: checkpoint.currentReview.coveredAttemptIds,
|
|
1374
|
+
recordedReviewers: Object.keys(checkpoint.currentReview.results),
|
|
1375
|
+
},
|
|
1376
|
+
}
|
|
1377
|
+
: {}),
|
|
1378
|
+
history: checkpoint.history,
|
|
1379
|
+
};
|
|
1380
|
+
}),
|
|
1064
1381
|
};
|
|
1065
1382
|
}
|
|
1066
1383
|
// END_BLOCK_RUN_VIEW
|