@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,27 +1,37 @@
|
|
|
1
1
|
// FILE: src/plugins/workflow/delegated.ts
|
|
2
|
-
// VERSION:
|
|
2
|
+
// VERSION: 2.0.0
|
|
3
3
|
// START_MODULE_CONTRACT
|
|
4
|
-
// PURPOSE: Delegated work-item attempt ledger and explicit controller acceptance decisions with a checkpoint-authorized rework path.
|
|
5
|
-
// SCOPE: Delegated attempt allocation bound to host callIDs with per-session callID uniqueness, call-bound result application, call-bound host-terminal launch failure recorded as a distinct failed attempt (bounded evidence, no budget refund, no state advance), bounded controller decisions (accept / request_changes with rationale, evidence, and concerns disposition), acceptance history with rework revocation,
|
|
6
|
-
// DEPENDS: [src/plugins/workflow/protocol.ts (types), src/plugins/workflow/state.ts (types and shared helpers), src/plugins/workflow/transitions.ts]
|
|
7
|
-
// LINKS: [M-WORKFLOW-DELEGATED, M-WORKFLOW-STATE, M-WORKFLOW-TRANSITIONS, M-WORKFLOW-CHECKPOINTS, V-M-WORKFLOW-DELEGATED]
|
|
4
|
+
// PURPOSE: Delegated work-item attempt ledger with bounded recovery, terminal report-rejection settlement, and explicit controller acceptance decisions with a checkpoint-authorized rework path.
|
|
5
|
+
// SCOPE: Delegated attempt allocation bound to host callIDs with per-session callID uniqueness, call-bound result application, call-bound host-terminal launch failure recorded as a distinct failed attempt (bounded evidence, no budget refund, no state advance), bounded controller decisions (accept / request_changes with rationale, evidence, and concerns disposition), acceptance history with rework revocation, the two-attempt budget extended only by authorized rework or one-unit recovery grants, bounded recovery of stopped or exhausted unaccepted items (diagnosis, changed condition, verification references, stable recoveryId, optional root-user message authorization validated through a read-only lookup with replay protection), call-bound settlement of confirmed terminal protocol-invalid reports as report_rejected attempts with bounded diagnostics preserved substantive hard stops, and progress summaries exposing remaining budget and supported next actions. Pure domain transitions over WorkItemStoreData; no tool, session, or filesystem authorization here.
|
|
6
|
+
// DEPENDS: [src/plugins/workflow/protocol.ts (types), src/plugins/workflow/state.ts (types and shared helpers), src/plugins/workflow/transitions.ts, src/lib/workflow-contract.ts]
|
|
7
|
+
// LINKS: [M-WORKFLOW-DELEGATED, M-WORKFLOW-STATE, M-WORKFLOW-TRANSITIONS, M-WORKFLOW-CHECKPOINTS, M-WORKFLOW-CONTRACT, V-M-WORKFLOW-DELEGATED]
|
|
8
8
|
// ROLE: RUNTIME
|
|
9
9
|
// MAP_MODE: EXPORTS
|
|
10
10
|
// END_MODULE_CONTRACT
|
|
11
11
|
//
|
|
12
12
|
// START_MODULE_MAP
|
|
13
13
|
// DELEGATED_BASE_ATTEMPTS - Initial controller-directed implementation attempt budget (initial plus one correction).
|
|
14
|
-
// DELEGATED_RATIONALE_MAX_CHARS - Maximum accepted rationale length.
|
|
15
|
-
// DELEGATED_EVIDENCE_MAX_CHARS - Maximum accepted single evidence
|
|
16
|
-
// DELEGATED_EVIDENCE_MAX_REFS - Maximum accepted evidence
|
|
14
|
+
// DELEGATED_RATIONALE_MAX_CHARS - Maximum accepted rationale, diagnosis, and changed-condition length.
|
|
15
|
+
// DELEGATED_EVIDENCE_MAX_CHARS - Maximum accepted single evidence/verification reference length.
|
|
16
|
+
// DELEGATED_EVIDENCE_MAX_REFS - Maximum accepted evidence/verification reference count.
|
|
17
|
+
// DELEGATED_RECOVERY_CODE_MAX_CHARS - Maximum accepted protocol error code stored in report rejections.
|
|
17
18
|
// DelegatedImplementerStatus - Implementer result statuses recordable on an attempt.
|
|
18
19
|
// DelegatedAttempt - One host-callID-bound implementation attempt.
|
|
19
|
-
// DelegatedAttemptStatus - Attempt lifecycle statuses (in_flight, completed, failed).
|
|
20
|
+
// DelegatedAttemptStatus - Attempt lifecycle statuses (in_flight, completed, failed, report_rejected).
|
|
21
|
+
// DelegatedAttemptReportRejection - Bounded rejected-report record for a terminal protocol-invalid result.
|
|
20
22
|
// DelegatedDecisionRecord - One recorded controller decision bound to an attempt.
|
|
21
23
|
// DelegatedAcceptanceRecord - One acceptance with optional later revocation.
|
|
22
24
|
// DelegatedReworkRecord - One checkpoint-authorized rework event.
|
|
23
|
-
//
|
|
25
|
+
// DelegatedRecoveryKind - One recovery unit kind (resume, autonomous grant, user grant).
|
|
26
|
+
// DelegatedRecoveryRecord - One bounded recovery event bound to its terminal target attempt.
|
|
27
|
+
// DelegatedWorkItemState - Full delegated-mode record extension including recovery history.
|
|
24
28
|
// DelegatedWriteScopeValidation - Result of validating declared write-scope text inputs.
|
|
29
|
+
// DelegatedRecoveryInputValidation - Shape accepted by validateDelegatedRecoveryInput.
|
|
30
|
+
// RecoveryUserMessageSnapshot - Identity and timing metadata of one candidate authorization message.
|
|
31
|
+
// LookupRecoveryUserMessage - Read-only authorization message lookup bound to the plugin SDK.
|
|
32
|
+
// RecoveryAuthorizationResult - Outcome of validating one root-user authorization reference.
|
|
33
|
+
// DelegatedProgressSummary - Inspectable budget, stop, and rejected-report snapshot with next action.
|
|
34
|
+
// DelegatedNextAction - Supported next action suggested by progress summaries.
|
|
25
35
|
// BeginDelegatedLaunchInput - Delegated launch input with session, item, and host callID.
|
|
26
36
|
// BeginDelegatedLaunchResult - Launch allocation outcome.
|
|
27
37
|
// ApplyDelegatedResultInput - Call-bound delegated result payload.
|
|
@@ -30,11 +40,20 @@
|
|
|
30
40
|
// DecideDelegatedWorkItemResult - Controller decision outcome.
|
|
31
41
|
// ReworkDelegatedWorkItemInput - Checkpoint-authorized rework request.
|
|
32
42
|
// ReworkDelegatedWorkItemResult - Authorized rework outcome.
|
|
33
|
-
//
|
|
43
|
+
// RecoverDelegatedWorkItemInput - Bounded recovery request with optional user authorization.
|
|
44
|
+
// RecoverDelegatedWorkItemResult - Recovery outcome with budget and kind, or a coded rejection.
|
|
45
|
+
// ApplyDelegatedReportRejectionInput - Call-bound terminal report-rejection payload.
|
|
46
|
+
// ApplyDelegatedReportRejectionResult - Report-rejection settlement outcome.
|
|
47
|
+
// delegatedAttemptBudget - Allowed attempt count given consumed attempts, rework, and recovery grants.
|
|
48
|
+
// delegatedRecoveryGrantCount - Number of budget-granting recovery entries in a history.
|
|
49
|
+
// delegatedAutonomousGrantConsumed - Whether the single autonomous grant is already recorded.
|
|
34
50
|
// currentDelegatedAcceptance - Currently applicable acceptance for a record.
|
|
51
|
+
// summarizeDelegatedProgress - Budget, terminal-state, and next-action summary for inspection.
|
|
35
52
|
// DelegatedDecisionInputValidation - Shape accepted by validateDelegatedDecisionInput.
|
|
36
53
|
// validateDelegatedWriteScope - Validates declared write-scope text into canonical normalized paths.
|
|
37
54
|
// validateDelegatedDecisionInput - Pure validation of decision text bounds.
|
|
55
|
+
// validateDelegatedRecoveryInput - Pure validation of recovery text bounds shared by tool and domain layers.
|
|
56
|
+
// validateRecoveryUserAuthorization - Validates one authorization reference against its root-session message snapshot.
|
|
38
57
|
// beginDelegatedLaunch - Allocate and bind one implementation attempt to a host callID.
|
|
39
58
|
// beginDelegatedLaunchInStore - Store-level delegated launch allocation.
|
|
40
59
|
// revertInFlightDelegatedLaunches - Reclaim attempts orphaned by a process restart boundary.
|
|
@@ -44,31 +63,48 @@
|
|
|
44
63
|
// ApplyDelegatedLaunchFailureResult - Host-terminal launch-failure outcome.
|
|
45
64
|
// applyDelegatedLaunchFailure - Record a confirmed host-terminal launch failure on its matching in-flight attempt.
|
|
46
65
|
// applyDelegatedLaunchFailureInStore - Store-level failed-attempt transition.
|
|
66
|
+
// applyDelegatedReportRejection - Settle one confirmed terminal protocol-invalid report on its call-bound attempt.
|
|
67
|
+
// applyDelegatedReportRejectionInStore - Store-level report-rejection settlement.
|
|
47
68
|
// decideDelegatedWorkItem - Record an explicit controller accept/request_changes decision.
|
|
48
69
|
// decideDelegatedWorkItemInStore - Store-level controller decision.
|
|
49
70
|
// reworkDelegatedWorkItem - Reopen an accepted item under validated checkpoint-failure authorization.
|
|
50
71
|
// reworkDelegatedWorkItemInStore - Store-level guarded rework reducer.
|
|
72
|
+
// recoverDelegatedWorkItem - Resume or grant exactly one additional attempt for a stopped or exhausted unaccepted item.
|
|
73
|
+
// recoverDelegatedWorkItemInStore - Store-level guarded recovery reducer.
|
|
51
74
|
// END_MODULE_MAP
|
|
52
75
|
//
|
|
53
76
|
// START_CHANGE_SUMMARY
|
|
54
|
-
// LAST_CHANGE: [
|
|
77
|
+
// LAST_CHANGE: [C-WORKFLOW-BOUNDED-RECOVERY-R1 - Added bounded recovery with a single autonomous grant, replay-protected root-user message authorization, terminal report-rejection settlement preserving substantive hard stops, and recovery-aware budgets and progress summaries.]
|
|
55
78
|
// END_CHANGE_SUMMARY
|
|
56
79
|
import { cloneRecord, createRecordLookupKey } from "./state.js";
|
|
57
80
|
import { getAllowedNextAgents } from "./transitions.js";
|
|
58
|
-
import { normalizeDeclaredScopePath } from "../../lib/
|
|
81
|
+
import { normalizeDeclaredScopePath } from "../../lib/workflow-contract.js";
|
|
59
82
|
// START_BLOCK_DELEGATED_TYPES
|
|
60
83
|
export const DELEGATED_BASE_ATTEMPTS = 2;
|
|
61
84
|
export const DELEGATED_RATIONALE_MAX_CHARS = 2000;
|
|
62
85
|
export const DELEGATED_EVIDENCE_MAX_CHARS = 512;
|
|
63
86
|
export const DELEGATED_EVIDENCE_MAX_REFS = 8;
|
|
87
|
+
export const DELEGATED_RECOVERY_CODE_MAX_CHARS = 128;
|
|
64
88
|
// END_BLOCK_DELEGATED_TYPES
|
|
65
89
|
// START_BLOCK_DELEGATED_HELPERS
|
|
66
90
|
function toIsoNow() {
|
|
67
91
|
return new Date().toISOString();
|
|
68
92
|
}
|
|
69
|
-
/**
|
|
93
|
+
/** Count of budget-granting recovery entries in a recovery history. */
|
|
94
|
+
export function delegatedRecoveryGrantCount(history) {
|
|
95
|
+
return history.filter((entry) => entry.kind === "autonomous_grant" ||
|
|
96
|
+
entry.kind === "user_grant" ||
|
|
97
|
+
entry.kind === "advance_grant").length;
|
|
98
|
+
}
|
|
99
|
+
/** Allowed attempt count: base budget plus one per authorized rework or recovery grant. */
|
|
70
100
|
export function delegatedAttemptBudget(state) {
|
|
71
|
-
return DELEGATED_BASE_ATTEMPTS +
|
|
101
|
+
return (DELEGATED_BASE_ATTEMPTS +
|
|
102
|
+
state.reworkHistory.length +
|
|
103
|
+
delegatedRecoveryGrantCount(state.recoveryHistory));
|
|
104
|
+
}
|
|
105
|
+
/** Whether the single autonomous recovery grant is already recorded for this item. */
|
|
106
|
+
export function delegatedAutonomousGrantConsumed(history) {
|
|
107
|
+
return history.some((entry) => entry.kind === "autonomous_grant");
|
|
72
108
|
}
|
|
73
109
|
/** Currently applicable acceptance for a record: the latest unrevoked acceptance. */
|
|
74
110
|
export function currentDelegatedAcceptance(record) {
|
|
@@ -145,6 +181,184 @@ export function validateDelegatedDecisionInput(input) {
|
|
|
145
181
|
}
|
|
146
182
|
return { ok: true };
|
|
147
183
|
}
|
|
184
|
+
/** Pure validation of bounded recovery text shared by the tool layer and domain layer. */
|
|
185
|
+
export function validateDelegatedRecoveryInput(input) {
|
|
186
|
+
const recoveryId = typeof input.recoveryId === "string" ? input.recoveryId.trim() : "";
|
|
187
|
+
if (recoveryId === "") {
|
|
188
|
+
return { ok: false, message: "recoveryId must be a non-empty string" };
|
|
189
|
+
}
|
|
190
|
+
if (recoveryId.length > DELEGATED_EVIDENCE_MAX_CHARS) {
|
|
191
|
+
return { ok: false, message: `recoveryId exceeds ${DELEGATED_EVIDENCE_MAX_CHARS} characters` };
|
|
192
|
+
}
|
|
193
|
+
const diagnosis = typeof input.diagnosis === "string" ? input.diagnosis.trim() : "";
|
|
194
|
+
if (diagnosis === "") {
|
|
195
|
+
return { ok: false, message: "diagnosis must be a non-empty string" };
|
|
196
|
+
}
|
|
197
|
+
if (diagnosis.length > DELEGATED_RATIONALE_MAX_CHARS) {
|
|
198
|
+
return { ok: false, message: `diagnosis exceeds ${DELEGATED_RATIONALE_MAX_CHARS} characters` };
|
|
199
|
+
}
|
|
200
|
+
const changedCondition = typeof input.changedCondition === "string" ? input.changedCondition.trim() : "";
|
|
201
|
+
if (changedCondition === "") {
|
|
202
|
+
return { ok: false, message: "changedCondition must be a non-empty string" };
|
|
203
|
+
}
|
|
204
|
+
if (changedCondition.length > DELEGATED_RATIONALE_MAX_CHARS) {
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
message: `changedCondition exceeds ${DELEGATED_RATIONALE_MAX_CHARS} characters`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (!Array.isArray(input.verification) || input.verification.length === 0) {
|
|
211
|
+
return { ok: false, message: "verification must be a non-empty array of reference strings" };
|
|
212
|
+
}
|
|
213
|
+
if (input.verification.length > DELEGATED_EVIDENCE_MAX_REFS) {
|
|
214
|
+
return { ok: false, message: `verification exceeds ${DELEGATED_EVIDENCE_MAX_REFS} references` };
|
|
215
|
+
}
|
|
216
|
+
for (const reference of input.verification) {
|
|
217
|
+
const normalized = typeof reference === "string" ? reference.trim() : "";
|
|
218
|
+
if (normalized === "") {
|
|
219
|
+
return { ok: false, message: "verification references must be non-empty strings" };
|
|
220
|
+
}
|
|
221
|
+
if (normalized.length > DELEGATED_EVIDENCE_MAX_CHARS) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
message: `verification reference exceeds ${DELEGATED_EVIDENCE_MAX_CHARS} characters`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { ok: true };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Validate one authorization reference against its root-session message
|
|
232
|
+
* snapshot. Provenance only: role, session identity, message identity, and a
|
|
233
|
+
* creation timestamp at or after the relevant stop. This check never
|
|
234
|
+
* interprets natural-language intent.
|
|
235
|
+
*/
|
|
236
|
+
export async function validateRecoveryUserAuthorization(input) {
|
|
237
|
+
let snapshot;
|
|
238
|
+
try {
|
|
239
|
+
snapshot = await input.lookup(input.owningSessionId, input.userMessageId);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
errorCode: "AUTHORIZATION_LOOKUP_FAILED",
|
|
245
|
+
message: `AUTHORIZATION_LOOKUP_FAILED: authorization message lookup failed: ${error.message}`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (!snapshot) {
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
errorCode: "AUTHORIZATION_NOT_FOUND",
|
|
252
|
+
message: `AUTHORIZATION_NOT_FOUND: no message ${input.userMessageId} is observable in session ${input.owningSessionId}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (snapshot.id !== undefined && snapshot.id !== input.userMessageId) {
|
|
256
|
+
return {
|
|
257
|
+
ok: false,
|
|
258
|
+
errorCode: "AUTHORIZATION_ID_MISMATCH",
|
|
259
|
+
message: `AUTHORIZATION_ID_MISMATCH: looked-up message ${snapshot.id} is not the referenced ${input.userMessageId}`,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (snapshot.role !== "user") {
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
errorCode: "AUTHORIZATION_NOT_USER_MESSAGE",
|
|
266
|
+
message: `AUTHORIZATION_NOT_USER_MESSAGE: message ${input.userMessageId} has role ${snapshot.role ?? "(unknown)"} instead of user`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (snapshot.sessionID !== input.owningSessionId) {
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
errorCode: "AUTHORIZATION_SESSION_MISMATCH",
|
|
273
|
+
message: `AUTHORIZATION_SESSION_MISMATCH: message ${input.userMessageId} belongs to session ${snapshot.sessionID ?? "(unknown)"}, not ${input.owningSessionId}`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (typeof snapshot.timeCreatedMs !== "number" || !Number.isFinite(snapshot.timeCreatedMs)) {
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
errorCode: "AUTHORIZATION_NOT_FOUND",
|
|
280
|
+
message: "AUTHORIZATION_NOT_FOUND: authorization message is missing verifiable timing metadata",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (input.requireAfterMs !== undefined && snapshot.timeCreatedMs < input.requireAfterMs) {
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
errorCode: "AUTHORIZATION_STALE",
|
|
287
|
+
message: `AUTHORIZATION_STALE: message ${input.userMessageId} predates the stop it must authorize`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return { ok: true, timeCreatedMs: snapshot.timeCreatedMs };
|
|
291
|
+
}
|
|
292
|
+
/** Terminal timestamp (epoch ms) of one attempt, or undefined when unknown. */
|
|
293
|
+
function terminalAttemptTimeMs(attempt) {
|
|
294
|
+
if (!attempt.completedAt)
|
|
295
|
+
return undefined;
|
|
296
|
+
const parsed = Date.parse(attempt.completedAt);
|
|
297
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Budget, terminal-state, and next-action summary for work-item inspection.
|
|
301
|
+
* The next action never suggests an ordinary launch the same state rejects.
|
|
302
|
+
*/
|
|
303
|
+
export function summarizeDelegatedProgress(record) {
|
|
304
|
+
const delegated = record.delegated;
|
|
305
|
+
if (!delegated) {
|
|
306
|
+
return {
|
|
307
|
+
attemptsConsumed: 0,
|
|
308
|
+
attemptBudget: 0,
|
|
309
|
+
remainingAttempts: 0,
|
|
310
|
+
hasInFlightAttempt: false,
|
|
311
|
+
recoveryGrants: 0,
|
|
312
|
+
autonomousGrantConsumed: false,
|
|
313
|
+
reportRejectedAttempts: 0,
|
|
314
|
+
nextAction: record.state === "closed" ? "closed" : "launch_implementer",
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const attemptsConsumed = delegated.attempts.length;
|
|
318
|
+
const attemptBudget = delegatedAttemptBudget(delegated);
|
|
319
|
+
const remainingAttempts = Math.max(0, attemptBudget - attemptsConsumed);
|
|
320
|
+
const hasInFlightAttempt = delegated.attempts.some((attempt) => attempt.status === "in_flight");
|
|
321
|
+
const recoveryGrants = delegatedRecoveryGrantCount(delegated.recoveryHistory);
|
|
322
|
+
const autonomousGrantConsumed = delegatedAutonomousGrantConsumed(delegated.recoveryHistory);
|
|
323
|
+
const reportRejectedAttempts = delegated.attempts.filter((attempt) => attempt.status === "report_rejected").length;
|
|
324
|
+
let nextAction;
|
|
325
|
+
if (record.state === "closed") {
|
|
326
|
+
nextAction = "closed";
|
|
327
|
+
}
|
|
328
|
+
else if (record.state === "ready_to_close") {
|
|
329
|
+
nextAction = "close";
|
|
330
|
+
}
|
|
331
|
+
else if (record.state === "awaiting_acceptance") {
|
|
332
|
+
nextAction = "decide";
|
|
333
|
+
}
|
|
334
|
+
else if (record.state === "blocked" || record.state === "needs_context") {
|
|
335
|
+
nextAction =
|
|
336
|
+
remainingAttempts > 0 || !autonomousGrantConsumed
|
|
337
|
+
? "recover"
|
|
338
|
+
: "recover_with_user_authorization";
|
|
339
|
+
}
|
|
340
|
+
else if (hasInFlightAttempt) {
|
|
341
|
+
// A live attempt rejects an ordinary launch with ATTEMPT_IN_FLIGHT; the
|
|
342
|
+
// supported action is collecting its result, not launching again.
|
|
343
|
+
nextAction = "await_result";
|
|
344
|
+
}
|
|
345
|
+
else if (remainingAttempts > 0) {
|
|
346
|
+
nextAction = "launch_implementer";
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
nextAction = autonomousGrantConsumed ? "recover_with_user_authorization" : "recover";
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
attemptsConsumed,
|
|
353
|
+
attemptBudget,
|
|
354
|
+
remainingAttempts,
|
|
355
|
+
hasInFlightAttempt,
|
|
356
|
+
recoveryGrants,
|
|
357
|
+
autonomousGrantConsumed,
|
|
358
|
+
reportRejectedAttempts,
|
|
359
|
+
nextAction,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
148
362
|
function findRecord(store, sessionId, workItemId) {
|
|
149
363
|
return store.records.get(createRecordLookupKey(sessionId, workItemId));
|
|
150
364
|
}
|
|
@@ -447,6 +661,106 @@ export function applyDelegatedLaunchFailureInStore(store, input) {
|
|
|
447
661
|
retryAllowed: (updated.delegated?.attempts.length ?? 0) < attemptBudget,
|
|
448
662
|
};
|
|
449
663
|
}
|
|
664
|
+
// START_CONTRACT: applyDelegatedReportRejection
|
|
665
|
+
// PURPOSE: Settle one confirmed terminal protocol-invalid report on its exact call-bound attempt as a completed execution with a rejected report.
|
|
666
|
+
// INPUTS: { store: WorkItemStore - backing store, input: ApplyDelegatedReportRejectionInput - call-bound report rejection with bounded diagnostics }
|
|
667
|
+
// OUTPUTS: { ApplyDelegatedReportRejectionResult - settled attempt and preserved hard-stop state, or a coded rejection without mutation }
|
|
668
|
+
// SIDE_EFFECTS: [Marks the matching in-flight attempt report_rejected and consumes one attempt without fabricating DONE]
|
|
669
|
+
// LINKS: [M-WORKFLOW-DELEGATED, M-WORKFLOW-STATE, M-WORKFLOW-REPAIR]
|
|
670
|
+
// END_CONTRACT: applyDelegatedReportRejection
|
|
671
|
+
export function applyDelegatedReportRejection(store, input) {
|
|
672
|
+
return applyDelegatedReportRejectionInStore(store.getStoreData(), input);
|
|
673
|
+
}
|
|
674
|
+
export function applyDelegatedReportRejectionInStore(store, input) {
|
|
675
|
+
const protocolErrorCode = typeof input.protocolErrorCode === "string" ? input.protocolErrorCode.trim() : "";
|
|
676
|
+
if (!protocolErrorCode || protocolErrorCode.length > DELEGATED_RECOVERY_CODE_MAX_CHARS) {
|
|
677
|
+
return {
|
|
678
|
+
ok: false,
|
|
679
|
+
errorCode: "INVALID_INPUT",
|
|
680
|
+
message: `INVALID_INPUT: protocolErrorCode must be 1 to ${DELEGATED_RECOVERY_CODE_MAX_CHARS} characters`,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
if (input.explicitHardStop !== undefined &&
|
|
684
|
+
input.explicitHardStop !== "BLOCKED" &&
|
|
685
|
+
input.explicitHardStop !== "NEEDS_CONTEXT") {
|
|
686
|
+
return {
|
|
687
|
+
ok: false,
|
|
688
|
+
errorCode: "INVALID_INPUT",
|
|
689
|
+
message: "INVALID_INPUT: explicitHardStop must be BLOCKED, NEEDS_CONTEXT, or omitted",
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
const existing = findRecord(store, input.sessionId, input.workItemId);
|
|
693
|
+
if (!existing) {
|
|
694
|
+
return {
|
|
695
|
+
ok: false,
|
|
696
|
+
errorCode: "WORK_ITEM_NOT_FOUND",
|
|
697
|
+
message: `WORK_ITEM_NOT_FOUND: no work item ${input.workItemId} for session ${input.sessionId}`,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
if (existing.state === "closed") {
|
|
701
|
+
return {
|
|
702
|
+
ok: false,
|
|
703
|
+
errorCode: "WORK_ITEM_ALREADY_CLOSED",
|
|
704
|
+
message: `WORK_ITEM_ALREADY_CLOSED: ${input.workItemId} is already closed`,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
if (existing.mode !== "delegated" || !existing.delegated) {
|
|
708
|
+
return {
|
|
709
|
+
ok: false,
|
|
710
|
+
errorCode: "WRONG_MODE",
|
|
711
|
+
message: `WRONG_MODE: ${input.workItemId} is ${existing.mode}, not delegated`,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const inFlight = existing.delegated.attempts.find((attempt) => attempt.status === "in_flight");
|
|
715
|
+
if (!inFlight || inFlight.callId !== input.callId) {
|
|
716
|
+
return {
|
|
717
|
+
ok: false,
|
|
718
|
+
errorCode: "STALE_CALLBACK",
|
|
719
|
+
message: `STALE_CALLBACK: ${input.callId} does not match the in-flight attempt ${inFlight ? inFlight.callId : "(none)"} for ${input.workItemId}`,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
const now = toIsoNow();
|
|
723
|
+
const nextState = input.explicitHardStop === "BLOCKED"
|
|
724
|
+
? "blocked"
|
|
725
|
+
: input.explicitHardStop === "NEEDS_CONTEXT"
|
|
726
|
+
? "needs_context"
|
|
727
|
+
: "awaiting_implementer";
|
|
728
|
+
const updated = {
|
|
729
|
+
...existing,
|
|
730
|
+
// A confirmed terminal execution settles its attempt; a rejected report
|
|
731
|
+
// never synthesizes DONE, so the item returns to the launch gate (or the
|
|
732
|
+
// observed substantive hard-stop state) with the attempt consumed.
|
|
733
|
+
state: nextState,
|
|
734
|
+
...(input.explicitHardStop ? { resultExcerpt: input.excerpt } : {}),
|
|
735
|
+
delegated: {
|
|
736
|
+
...existing.delegated,
|
|
737
|
+
attempts: existing.delegated.attempts.map((attempt) => attempt.attempt === inFlight.attempt
|
|
738
|
+
? {
|
|
739
|
+
...attempt,
|
|
740
|
+
status: "report_rejected",
|
|
741
|
+
reportRejection: {
|
|
742
|
+
protocolErrorCode,
|
|
743
|
+
excerpt: input.excerpt,
|
|
744
|
+
rejectedAt: now,
|
|
745
|
+
},
|
|
746
|
+
completedAt: now,
|
|
747
|
+
}
|
|
748
|
+
: attempt),
|
|
749
|
+
},
|
|
750
|
+
updatedAt: now,
|
|
751
|
+
};
|
|
752
|
+
store.records.set(createRecordLookupKey(input.sessionId, input.workItemId), updated);
|
|
753
|
+
const delegated = updated.delegated;
|
|
754
|
+
const attemptBudget = delegatedAttemptBudget(delegated);
|
|
755
|
+
return {
|
|
756
|
+
ok: true,
|
|
757
|
+
record: cloneRecord(updated),
|
|
758
|
+
attempt: inFlight.attempt,
|
|
759
|
+
...(input.explicitHardStop ? { observedHardStop: input.explicitHardStop } : {}),
|
|
760
|
+
consumedAttempts: delegated.attempts.length,
|
|
761
|
+
attemptBudget,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
450
764
|
// START_CONTRACT: decideDelegatedWorkItem
|
|
451
765
|
// PURPOSE: Record an explicit controller accept or request_changes decision bound to the current completed attempt.
|
|
452
766
|
// INPUTS: { store: WorkItemStore - backing store, input: DecideDelegatedWorkItemInput - decision payload with attempt identity and bounded text }
|
|
@@ -707,6 +1021,228 @@ export function reworkDelegatedWorkItemInStore(store, input) {
|
|
|
707
1021
|
grantedAttempts: delegatedAttemptBudget(updated.delegated),
|
|
708
1022
|
};
|
|
709
1023
|
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Synchronous eligibility check for delegated recovery. Re-run after any
|
|
1026
|
+
* asynchronous authorization validation so a changed target state can never
|
|
1027
|
+
* be committed on stale evidence.
|
|
1028
|
+
*/
|
|
1029
|
+
function precheckDelegatedRecovery(store, input) {
|
|
1030
|
+
const existing = findRecord(store, input.sessionId, input.workItemId);
|
|
1031
|
+
if (!existing) {
|
|
1032
|
+
return {
|
|
1033
|
+
ok: false,
|
|
1034
|
+
errorCode: "WORK_ITEM_NOT_FOUND",
|
|
1035
|
+
message: `WORK_ITEM_NOT_FOUND: no work item ${input.workItemId} for session ${input.sessionId}`,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
if (existing.state === "closed") {
|
|
1039
|
+
return {
|
|
1040
|
+
ok: false,
|
|
1041
|
+
errorCode: "WORK_ITEM_ALREADY_CLOSED",
|
|
1042
|
+
message: `WORK_ITEM_ALREADY_CLOSED: ${input.workItemId} is already closed`,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
if (existing.mode !== "delegated" || !existing.delegated) {
|
|
1046
|
+
return {
|
|
1047
|
+
ok: false,
|
|
1048
|
+
errorCode: "WRONG_MODE",
|
|
1049
|
+
message: `WRONG_MODE: ${input.workItemId} is ${existing.mode}, not delegated`,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
if (currentDelegatedAcceptance(existing)) {
|
|
1053
|
+
return {
|
|
1054
|
+
ok: false,
|
|
1055
|
+
errorCode: "INVALID_TARGET_STATE",
|
|
1056
|
+
message: `INVALID_TARGET_STATE: ${input.workItemId} is currently accepted; checkpoint-authorized rework is the path for accepted items, not recovery`,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
if (existing.delegated.attempts.some((attempt) => attempt.status === "in_flight")) {
|
|
1060
|
+
return {
|
|
1061
|
+
ok: false,
|
|
1062
|
+
errorCode: "INVALID_TARGET_STATE",
|
|
1063
|
+
message: `INVALID_TARGET_STATE: ${input.workItemId} has a live in-flight attempt; recovery never interrupts live work`,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
const isStop = existing.state === "blocked" || existing.state === "needs_context";
|
|
1067
|
+
const isExhausted = existing.state === "awaiting_implementer" &&
|
|
1068
|
+
existing.delegated.attempts.length >= delegatedAttemptBudget(existing.delegated);
|
|
1069
|
+
if (!isStop && !isExhausted) {
|
|
1070
|
+
return {
|
|
1071
|
+
ok: false,
|
|
1072
|
+
errorCode: "INVALID_TARGET_STATE",
|
|
1073
|
+
message: `INVALID_TARGET_STATE: ${input.workItemId} is ${existing.state} and is neither stopped nor out of implementation budget`,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
const target = existing.delegated.attempts[existing.delegated.attempts.length - 1];
|
|
1077
|
+
if (!target) {
|
|
1078
|
+
return {
|
|
1079
|
+
ok: false,
|
|
1080
|
+
errorCode: "INVALID_TARGET_STATE",
|
|
1081
|
+
message: `INVALID_TARGET_STATE: ${input.workItemId} has no terminal attempt to recover`,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
if (target.status === "in_flight") {
|
|
1085
|
+
return {
|
|
1086
|
+
ok: false,
|
|
1087
|
+
errorCode: "ATTEMPT_NOT_TERMINAL",
|
|
1088
|
+
message: `ATTEMPT_NOT_TERMINAL: attempt ${target.attempt} of ${input.workItemId} is still in flight`,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
if (target.attempt !== input.attempt) {
|
|
1092
|
+
return {
|
|
1093
|
+
ok: false,
|
|
1094
|
+
errorCode: "ATTEMPT_MISMATCH",
|
|
1095
|
+
message: `ATTEMPT_MISMATCH: recovery targets attempt ${input.attempt} but the latest terminal attempt is ${target.attempt}`,
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
if (existing.delegated.recoveryHistory.some((entry) => entry.recoveryId === input.recoveryId)) {
|
|
1099
|
+
return {
|
|
1100
|
+
ok: false,
|
|
1101
|
+
errorCode: "DUPLICATE_RECOVERY_ID",
|
|
1102
|
+
message: `DUPLICATE_RECOVERY_ID: recoveryId ${input.recoveryId} is already recorded for ${input.workItemId}`,
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
const remaining = delegatedAttemptBudget(existing.delegated) - existing.delegated.attempts.length;
|
|
1106
|
+
return {
|
|
1107
|
+
ok: true,
|
|
1108
|
+
record: existing,
|
|
1109
|
+
target,
|
|
1110
|
+
needsGrant: remaining <= 0,
|
|
1111
|
+
autonomousAvailable: !delegatedAutonomousGrantConsumed(existing.delegated.recoveryHistory),
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
// START_CONTRACT: recoverDelegatedWorkItem
|
|
1115
|
+
// PURPOSE: Resume a stopped unaccepted item or grant exactly one additional attempt after exhaustion, preserving every prior outcome and decision.
|
|
1116
|
+
// INPUTS: { store: WorkItemStore - backing store, input: RecoverDelegatedWorkItemInput - bounded recovery payload with optional root-user authorization }
|
|
1117
|
+
// OUTPUTS: { RecoverDelegatedWorkItemResult - recorded recovery with updated budget or a coded rejection without mutation }
|
|
1118
|
+
// SIDE_EFFECTS: [Appends a recovery record, moves a stopped item back to awaiting_implementer, and may extend the attempt budget by exactly one]
|
|
1119
|
+
// LINKS: [M-WORKFLOW-DELEGATED, M-WORKFLOW-CHECKPOINTS, validateRecoveryUserAuthorization]
|
|
1120
|
+
// END_CONTRACT: recoverDelegatedWorkItem
|
|
1121
|
+
export async function recoverDelegatedWorkItem(store, input) {
|
|
1122
|
+
return recoverDelegatedWorkItemInStore(store.getStoreData(), input);
|
|
1123
|
+
}
|
|
1124
|
+
export async function recoverDelegatedWorkItemInStore(store, input) {
|
|
1125
|
+
const validation = validateDelegatedRecoveryInput({
|
|
1126
|
+
diagnosis: input.diagnosis,
|
|
1127
|
+
changedCondition: input.changedCondition,
|
|
1128
|
+
verification: input.verification,
|
|
1129
|
+
recoveryId: input.recoveryId,
|
|
1130
|
+
});
|
|
1131
|
+
if (!validation.ok) {
|
|
1132
|
+
return {
|
|
1133
|
+
ok: false,
|
|
1134
|
+
errorCode: "INVALID_INPUT",
|
|
1135
|
+
message: `INVALID_INPUT: ${validation.message}`,
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
const precheck = precheckDelegatedRecovery(store, input);
|
|
1139
|
+
if (!precheck.ok) {
|
|
1140
|
+
return { ok: false, errorCode: precheck.errorCode, message: precheck.message };
|
|
1141
|
+
}
|
|
1142
|
+
const commitRecovery = (kind) => {
|
|
1143
|
+
// Re-run the synchronous eligibility check immediately before committing
|
|
1144
|
+
// so an authorization await can never commit against a changed target.
|
|
1145
|
+
const fresh = precheckDelegatedRecovery(store, input);
|
|
1146
|
+
if (!fresh.ok) {
|
|
1147
|
+
return { ok: false, errorCode: fresh.errorCode, message: fresh.message };
|
|
1148
|
+
}
|
|
1149
|
+
const now = toIsoNow();
|
|
1150
|
+
const recovery = {
|
|
1151
|
+
recoveryId: input.recoveryId,
|
|
1152
|
+
targetAttempt: input.attempt,
|
|
1153
|
+
kind,
|
|
1154
|
+
diagnosis: input.diagnosis.trim(),
|
|
1155
|
+
changedCondition: input.changedCondition.trim(),
|
|
1156
|
+
verification: input.verification.map((reference) => reference.trim()),
|
|
1157
|
+
recoveredAt: now,
|
|
1158
|
+
...(kind === "user_grant" && input.userMessageId
|
|
1159
|
+
? { userMessageId: input.userMessageId }
|
|
1160
|
+
: {}),
|
|
1161
|
+
};
|
|
1162
|
+
const updated = {
|
|
1163
|
+
...fresh.record,
|
|
1164
|
+
// Recovery returns a stopped item to the launch gate; an exhausted item
|
|
1165
|
+
// is already there. Either way recovery never accepts or closes work.
|
|
1166
|
+
state: "awaiting_implementer",
|
|
1167
|
+
delegated: {
|
|
1168
|
+
...fresh.record.delegated,
|
|
1169
|
+
recoveryHistory: [...fresh.record.delegated.recoveryHistory, recovery],
|
|
1170
|
+
},
|
|
1171
|
+
updatedAt: now,
|
|
1172
|
+
};
|
|
1173
|
+
store.records.set(createRecordLookupKey(input.sessionId, input.workItemId), updated);
|
|
1174
|
+
const delegated = updated.delegated;
|
|
1175
|
+
const attemptBudget = delegatedAttemptBudget(delegated);
|
|
1176
|
+
return {
|
|
1177
|
+
ok: true,
|
|
1178
|
+
record: cloneRecord(updated),
|
|
1179
|
+
recoveryId: input.recoveryId,
|
|
1180
|
+
kind,
|
|
1181
|
+
attemptBudget,
|
|
1182
|
+
remainingAttempts: Math.max(0, attemptBudget - delegated.attempts.length),
|
|
1183
|
+
};
|
|
1184
|
+
};
|
|
1185
|
+
if (!precheck.needsGrant) {
|
|
1186
|
+
return commitRecovery("resume");
|
|
1187
|
+
}
|
|
1188
|
+
if (input.userMessageId !== undefined) {
|
|
1189
|
+
const userMessageId = input.userMessageId.trim();
|
|
1190
|
+
if (!userMessageId) {
|
|
1191
|
+
return {
|
|
1192
|
+
ok: false,
|
|
1193
|
+
errorCode: "INVALID_INPUT",
|
|
1194
|
+
message: "INVALID_INPUT: userMessageId must be a non-empty string when provided",
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
if (precheck.record.delegated?.recoveryHistory.some((entry) => entry.userMessageId === userMessageId)) {
|
|
1198
|
+
return {
|
|
1199
|
+
ok: false,
|
|
1200
|
+
errorCode: "AUTHORIZATION_REUSED",
|
|
1201
|
+
message: `AUTHORIZATION_REUSED: message ${userMessageId} already authorized recovery of ${input.workItemId}; each message grants at most one unit per target`,
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
if (typeof input.lookupUserMessage !== "function") {
|
|
1205
|
+
return {
|
|
1206
|
+
ok: false,
|
|
1207
|
+
errorCode: "AUTHORIZATION_LOOKUP_FAILED",
|
|
1208
|
+
message: "AUTHORIZATION_LOOKUP_FAILED: user-authorized recovery requires a read-only message lookup bound to the plugin context",
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
const authorization = await validateRecoveryUserAuthorization({
|
|
1212
|
+
owningSessionId: input.sessionId,
|
|
1213
|
+
userMessageId,
|
|
1214
|
+
requireAfterMs: terminalAttemptTimeMs(precheck.target),
|
|
1215
|
+
lookup: input.lookupUserMessage,
|
|
1216
|
+
});
|
|
1217
|
+
if (!authorization.ok) {
|
|
1218
|
+
return { ok: false, errorCode: authorization.errorCode, message: authorization.message };
|
|
1219
|
+
}
|
|
1220
|
+
// The await above is an async boundary: re-verify non-reuse before commit
|
|
1221
|
+
// (the precheck inside commitRecovery covers the rest of the state).
|
|
1222
|
+
const fresh = findRecord(store, input.sessionId, input.workItemId);
|
|
1223
|
+
if (fresh?.delegated?.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.workItemId}`,
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
return commitRecovery("user_grant");
|
|
1231
|
+
}
|
|
1232
|
+
if (precheck.autonomousAvailable) {
|
|
1233
|
+
return commitRecovery("autonomous_grant");
|
|
1234
|
+
}
|
|
1235
|
+
if (input.advanceGrantApproved === true) {
|
|
1236
|
+
// The transaction layer already validated the execution authority and
|
|
1237
|
+
// reserved exactly one unit for this recoveryId; record the grant kind.
|
|
1238
|
+
return commitRecovery("advance_grant");
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
ok: false,
|
|
1242
|
+
errorCode: "AUTONOMOUS_GRANT_EXHAUSTED",
|
|
1243
|
+
message: `AUTONOMOUS_GRANT_EXHAUSTED: the single autonomous recovery grant for ${input.workItemId} is consumed; one further unit requires a recorded advance authority or a fresh root-user message referenced by userMessageId`,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
710
1246
|
// START_CONTRACT: revertInFlightDelegatedLaunches
|
|
711
1247
|
// PURPOSE: Reclaim delegated attempts that a process-restart boundary orphaned, restoring their items to a launchable state without consuming budget.
|
|
712
1248
|
// INPUTS: { store: WorkItemStoreData - hydrated store data, sessionId: string - owning session }
|