codex-workflow-v2 2.0.0-alpha.7.2 → 2.0.0-alpha.7.2.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/README.md +12 -2
- package/dist/src/alpha6/remediation.d.ts +18 -1
- package/dist/src/alpha6/remediation.js +458 -9
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/cli.js +4 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +19 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/workflow.d.ts +1 -0
- package/dist/src/workflow.js +47 -10
- package/dist/src/workflow.js.map +1 -1
- package/docs/alpha7.2.1-remediation-recovery-brief.md +86 -0
- package/package.json +1 -1
- package/schemas/remediation-mode-recovery-event.schema.json +42 -0
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ cross-machine synchronization are not part of its contract.
|
|
|
36
36
|
Node.js 22 or newer is required. Consumers pin the exact package version:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
npm install --save-dev --save-exact codex-workflow-v2@2.0.0-alpha.7.2
|
|
39
|
+
npm install --save-dev --save-exact codex-workflow-v2@2.0.0-alpha.7.2.1
|
|
40
40
|
npx codex-workflow gateway handshake --repo .
|
|
41
41
|
npx codex-workflow doctor --repo .
|
|
42
42
|
```
|
|
@@ -125,13 +125,23 @@ sidecars:
|
|
|
125
125
|
- project: `adoption-posture.jsonl`
|
|
126
126
|
- task: `plan-risk-audits.jsonl`, `step-review-events.jsonl`,
|
|
127
127
|
`reviewer-attestations.jsonl`, `remediation-events.jsonl`,
|
|
128
|
-
`corrective-decisions.jsonl`, `
|
|
128
|
+
`corrective-decisions.jsonl`, `corrective-decision-recoveries.jsonl`,
|
|
129
|
+
`remediation-mode-recoveries.jsonl`, `handoffs.jsonl`
|
|
129
130
|
- milestone: `scope-change-events.jsonl`, `autonomy-contract-events.jsonl`
|
|
130
131
|
|
|
131
132
|
Milestone scope-change recovery also uses Milestone-local `.transactions/` journal files.
|
|
132
133
|
Broken JSONL, duplicate event ids, broken hash chains, stale revision/hash/commit bindings,
|
|
133
134
|
or mismatched authority fail closed with structured diagnostics.
|
|
134
135
|
|
|
136
|
+
Alpha.7.2.1 can recover the one exact alpha.7.2 defect where a validated third
|
|
137
|
+
corrective attempt was persisted as `mode=ordinary`. `next` exposes
|
|
138
|
+
`task remediation-mode-recover` only for that fully verified legacy shape. Run the
|
|
139
|
+
returned command with its exact Task revision, actor, and writer token. Recovery
|
|
140
|
+
appends a bound event to `remediation-mode-recoveries.jsonl`; it never edits the
|
|
141
|
+
historical remediation event. An identical retry is idempotent. After recovery,
|
|
142
|
+
the three-attempt hard stop remains in force and `next` permits only
|
|
143
|
+
`split-required` or `stop-escalate`, never a fourth implementation run.
|
|
144
|
+
|
|
135
145
|
## Delegated approval
|
|
136
146
|
|
|
137
147
|
Delegation never makes an agent a human actor. A user first approves an exact grant policy;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { CorrectiveDecisionEvent, CorrectivePlanAuditInput, GuardedRiskCategory, RemediationEvent, RemediationFailureEvidence, RemediationFailureKind, RemediationMode, TaskState } from '../contracts.js';
|
|
1
|
+
import type { CorrectiveDecisionEvent, CorrectiveDecisionRecoveryEvent, CorrectivePlanAuditInput, GuardedRiskCategory, RemediationEvent, RemediationFailureEvidence, RemediationFailureKind, RemediationMode, RemediationModeRecoveryEvent, TaskState } from '../contracts.js';
|
|
2
2
|
import type { FileStateStore } from '../state/store.js';
|
|
3
3
|
import type { CurrentPlanRiskAudit } from './plan-risk.js';
|
|
4
4
|
export declare const REMEDIATION_EVENTS_SIDECAR = "remediation-events.jsonl";
|
|
5
5
|
export declare const CORRECTIVE_DECISIONS_SIDECAR = "corrective-decisions.jsonl";
|
|
6
|
+
export declare const CORRECTIVE_DECISION_RECOVERIES_SIDECAR = "corrective-decision-recoveries.jsonl";
|
|
7
|
+
export declare const REMEDIATION_MODE_RECOVERIES_SIDECAR = "remediation-mode-recoveries.jsonl";
|
|
6
8
|
export interface GuardedRemediationAttemptGate {
|
|
7
9
|
stepId: string;
|
|
8
10
|
guardedCategory: GuardedRiskCategory;
|
|
@@ -12,11 +14,26 @@ export interface GuardedRemediationAttemptGate {
|
|
|
12
14
|
correctiveDecision: CorrectiveDecisionEvent | null;
|
|
13
15
|
posture: 'ordinary' | 'decision-required' | 'continue-fix' | 'replan-required' | 'replan-ready' | 'split-required' | 'stop-escalate' | 'hard-stop';
|
|
14
16
|
}
|
|
17
|
+
export interface RemediationModeRecoveryAssessment {
|
|
18
|
+
task: TaskState;
|
|
19
|
+
remediation: RemediationEvent;
|
|
20
|
+
decision: CorrectiveDecisionEvent;
|
|
21
|
+
correctiveRecovery: CorrectiveDecisionRecoveryEvent;
|
|
22
|
+
existingRecovery: RemediationModeRecoveryEvent | null;
|
|
23
|
+
actor: string;
|
|
24
|
+
c1ClaimEventHash: string | null;
|
|
25
|
+
writerLeaseTokenHash: string | null;
|
|
26
|
+
evidenceFingerprint: string;
|
|
27
|
+
}
|
|
15
28
|
export declare function readRemediationEvents(store: FileStateStore, task: TaskState): RemediationEvent[];
|
|
16
29
|
export declare function readCorrectiveDecisionEvents(store: FileStateStore, task: TaskState): CorrectiveDecisionEvent[];
|
|
17
30
|
export declare function defaultCorrectiveAuditorActor(taskId: string): string;
|
|
18
31
|
export declare function assertCorrectiveAuditorIndependence(store: FileStateStore, task: TaskState, stepId: string, auditor: string): void;
|
|
32
|
+
export declare function readRemediationModeRecoveryEvents(store: FileStateStore, task: TaskState): RemediationModeRecoveryEvent[];
|
|
19
33
|
export declare function appendRemediationEvent(store: FileStateStore, task: TaskState, stepId: string, failureKind: RemediationFailureKind, failureEvidence: RemediationFailureEvidence, planRiskAudit: CurrentPlanRiskAudit, now: Date): RemediationEvent;
|
|
34
|
+
export declare function findRemediationModeRecoveryCandidate(store: FileStateStore, task: TaskState): RemediationModeRecoveryAssessment | null;
|
|
35
|
+
export declare function assessRemediationModeRecovery(store: FileStateStore, task: TaskState, stepId: string, actor: string, writerToken: string | null): RemediationModeRecoveryAssessment;
|
|
36
|
+
export declare function appendRemediationModeRecovery(store: FileStateStore, task: TaskState, stepId: string, expectedRevision: number, actor: string, writerToken: string | null, now: Date): RemediationModeRecoveryEvent;
|
|
20
37
|
export declare function appendCorrectiveDecisionEvent(store: FileStateStore, task: TaskState, stepId: string, correctiveAudit: CorrectivePlanAuditInput, planRiskAudit: CurrentPlanRiskAudit, planHash: string, now: Date): CorrectiveDecisionEvent;
|
|
21
38
|
export declare function assertGuardedRemediationAttemptAllowed(store: FileStateStore, task: TaskState, stepId: string, planRiskAudit: CurrentPlanRiskAudit, recoveredDecisionEventId?: string | null): GuardedRemediationAttemptGate;
|
|
22
39
|
export declare function readGuardedRemediationPostureForStep(store: FileStateStore, task: TaskState, stepId: string, planRiskAudit: CurrentPlanRiskAudit): GuardedRemediationAttemptGate | null;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { closeSync, existsSync, openSync, readFileSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { WorkflowError } from '../errors.js';
|
|
4
4
|
import { createUlid } from '../ulid.js';
|
|
5
|
-
import { assertHash64 } from './store-sidecars.js';
|
|
5
|
+
import { assertHash64, canonicalJsonStringify, sha256Hex } from './store-sidecars.js';
|
|
6
|
+
import { readTaskC1Posture } from './handoff.js';
|
|
6
7
|
import { buildCurrentStrictStepReviewCycle, readReviewerAttestations, readStepReviewEvents, } from './review.js';
|
|
7
8
|
export const REMEDIATION_EVENTS_SIDECAR = 'remediation-events.jsonl';
|
|
8
9
|
export const CORRECTIVE_DECISIONS_SIDECAR = 'corrective-decisions.jsonl';
|
|
10
|
+
export const CORRECTIVE_DECISION_RECOVERIES_SIDECAR = 'corrective-decision-recoveries.jsonl';
|
|
11
|
+
export const REMEDIATION_MODE_RECOVERIES_SIDECAR = 'remediation-mode-recoveries.jsonl';
|
|
12
|
+
const REMEDIATION_MODE_RECOVERY_LOCK = '.remediation-mode-recovery.lock';
|
|
9
13
|
const COMMIT_SHA_PATTERN = /^[a-f0-9]{40}$/;
|
|
10
14
|
const REMEDIATION_FAILURE_KINDS = new Set([
|
|
11
15
|
'checks-failed',
|
|
@@ -65,15 +69,35 @@ export function assertCorrectiveAuditorIndependence(store, task, stepId, auditor
|
|
|
65
69
|
function validateRemediationHistory(store, task) {
|
|
66
70
|
const remediationEvents = readRawRemediationEvents(store, task);
|
|
67
71
|
const correctiveDecisionEvents = readRawCorrectiveDecisionEvents(store, task);
|
|
72
|
+
const modeRecoveries = readRemediationModeRecoveryEvents(store, task);
|
|
68
73
|
validateRemediationAttemptOrdinals(remediationEvents, task);
|
|
69
74
|
validateRemediationReviewReferences(store, task, remediationEvents);
|
|
70
75
|
validateCorrectiveDecisionCoverage(task, correctiveDecisionEvents, remediationEvents);
|
|
71
|
-
|
|
76
|
+
const recoveredEventIds = validateRemediationModeRecoveries(store, task, remediationEvents, correctiveDecisionEvents, modeRecoveries);
|
|
77
|
+
validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisionEvents, recoveredEventIds);
|
|
72
78
|
return {
|
|
73
79
|
remediationEvents,
|
|
74
80
|
correctiveDecisionEvents,
|
|
75
81
|
};
|
|
76
82
|
}
|
|
83
|
+
export function readRemediationModeRecoveryEvents(store, task) {
|
|
84
|
+
const root = store.taskRoot(task.projectId, task.id);
|
|
85
|
+
const file = path.join(root, REMEDIATION_MODE_RECOVERIES_SIDECAR);
|
|
86
|
+
if (!existsSync(file))
|
|
87
|
+
return [];
|
|
88
|
+
return store.readSidecarEvents(root, REMEDIATION_MODE_RECOVERIES_SIDECAR, {
|
|
89
|
+
validateEvent: (event, context) => validateStoredRemediationModeRecovery(event, task, context.file, context.line),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function readRawCorrectiveDecisionRecoveries(store, task) {
|
|
93
|
+
const root = store.taskRoot(task.projectId, task.id);
|
|
94
|
+
const file = path.join(root, CORRECTIVE_DECISION_RECOVERIES_SIDECAR);
|
|
95
|
+
if (!existsSync(file))
|
|
96
|
+
return [];
|
|
97
|
+
return store.readSidecarEvents(root, CORRECTIVE_DECISION_RECOVERIES_SIDECAR, {
|
|
98
|
+
validateEvent: (event, context) => validateStoredCorrectiveDecisionRecoveryReference(event, task, context.file, context.line),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
77
101
|
function readRawRemediationEvents(store, task) {
|
|
78
102
|
const sidecarFile = path.join(store.taskRoot(task.projectId, task.id), REMEDIATION_EVENTS_SIDECAR);
|
|
79
103
|
if (!existsSync(sidecarFile))
|
|
@@ -91,6 +115,7 @@ function readRawCorrectiveDecisionEvents(store, task) {
|
|
|
91
115
|
});
|
|
92
116
|
}
|
|
93
117
|
export function appendRemediationEvent(store, task, stepId, failureKind, failureEvidence, planRiskAudit, now) {
|
|
118
|
+
const recoveredDecisionEventId = findCorrectiveRecoveryDecisionIdForFailure(store, task, stepId);
|
|
94
119
|
const existing = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
|
|
95
120
|
const matching = existing.filter((event) => remediationEventMatchesFailure(event, failureKind, failureEvidence));
|
|
96
121
|
if (matching.length > 1) {
|
|
@@ -104,7 +129,9 @@ export function appendRemediationEvent(store, task, stepId, failureKind, failure
|
|
|
104
129
|
if (matching.length === 1) {
|
|
105
130
|
const reused = matching[0];
|
|
106
131
|
const priorEvents = existing.filter((event) => event.attemptOrdinal < reused.attemptOrdinal);
|
|
107
|
-
const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, priorEvents, readCorrectiveDecisionEvents(store, task)
|
|
132
|
+
const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, priorEvents, readCorrectiveDecisionEvents(store, task), {
|
|
133
|
+
recoveredDecisionEventId,
|
|
134
|
+
});
|
|
108
135
|
if (reused.attemptOrdinal !== gate.nextAttemptOrdinal || reused.mode !== gate.mode || reused.guardedCategory !== gate.guardedCategory) {
|
|
109
136
|
throw new WorkflowError('STATE_CORRUPT', 'Existing remediation event conflicts with the current guarded retry binding.', {
|
|
110
137
|
taskId: task.id,
|
|
@@ -118,7 +145,9 @@ export function appendRemediationEvent(store, task, stepId, failureKind, failure
|
|
|
118
145
|
}
|
|
119
146
|
return reused;
|
|
120
147
|
}
|
|
121
|
-
const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, existing, readCorrectiveDecisionEvents(store, task)
|
|
148
|
+
const gate = deriveAttemptGateFromHistory(task, stepId, planRiskAudit, existing, readCorrectiveDecisionEvents(store, task), {
|
|
149
|
+
recoveredDecisionEventId,
|
|
150
|
+
});
|
|
122
151
|
return store.appendSidecarEvent(store.taskRoot(task.projectId, task.id), REMEDIATION_EVENTS_SIDECAR, {
|
|
123
152
|
eventId: `RME-${createUlid(now.getTime())}`,
|
|
124
153
|
recordedAt: now.toISOString(),
|
|
@@ -133,6 +162,290 @@ export function appendRemediationEvent(store, task, stepId, failureKind, failure
|
|
|
133
162
|
validateEvent: (event, context) => validateStoredRemediationEvent(event, task, context.file, context.line),
|
|
134
163
|
});
|
|
135
164
|
}
|
|
165
|
+
function findCorrectiveRecoveryDecisionIdForFailure(store, task, stepId) {
|
|
166
|
+
const remediations = readRawRemediationEvents(store, task).filter((event) => event.stepId === stepId);
|
|
167
|
+
if (remediations.length !== 2)
|
|
168
|
+
return null;
|
|
169
|
+
const decisions = readRawCorrectiveDecisionEvents(store, task).filter((event) => event.stepId === stepId
|
|
170
|
+
&& event.triggeringAttemptCount === 3
|
|
171
|
+
&& event.decision === 'continue-fix'
|
|
172
|
+
&& event.planHash !== task.planHash);
|
|
173
|
+
if (decisions.length !== 1)
|
|
174
|
+
return null;
|
|
175
|
+
const recoveries = readRawCorrectiveDecisionRecoveries(store, task).filter((event) => event.stepId === stepId
|
|
176
|
+
&& event.triggeringAttemptCount === 3
|
|
177
|
+
&& event.correctiveDecisionEventId === decisions[0].eventId
|
|
178
|
+
&& event.correctiveDecisionEventHash === decisions[0].eventHash
|
|
179
|
+
&& event.reboundPlanHash === task.planHash);
|
|
180
|
+
if (recoveries.length > 1) {
|
|
181
|
+
throw new WorkflowError('STATE_CORRUPT', 'Multiple corrective recoveries match one remediation attempt.', {
|
|
182
|
+
taskId: task.id,
|
|
183
|
+
stepId,
|
|
184
|
+
recoveryEventIds: recoveries.map((event) => event.eventId),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return recoveries[0]?.correctiveDecisionEventId ?? null;
|
|
188
|
+
}
|
|
189
|
+
export function findRemediationModeRecoveryCandidate(store, task) {
|
|
190
|
+
const raw = readRawRemediationEvents(store, task);
|
|
191
|
+
const candidates = raw.filter((event) => event.attemptOrdinal > 2 && event.mode === 'ordinary');
|
|
192
|
+
if (candidates.length === 0)
|
|
193
|
+
return null;
|
|
194
|
+
if (candidates.length !== 1) {
|
|
195
|
+
throw new WorkflowError('STATE_CORRUPT', 'Multiple malformed remediation modes are not recoverable.', {
|
|
196
|
+
taskId: task.id,
|
|
197
|
+
eventIds: candidates.map((event) => event.eventId),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const posture = readTaskC1Posture(store, task);
|
|
201
|
+
const lease = readWriterLeaseForModeRecovery(store, task);
|
|
202
|
+
const actor = posture.state === 'claimed' ? posture.claimant : lease?.owner ?? 'worker';
|
|
203
|
+
return assessRemediationModeRecovery(store, task, candidates[0].stepId, actor, lease?.token ?? null);
|
|
204
|
+
}
|
|
205
|
+
export function assessRemediationModeRecovery(store, task, stepId, actor, writerToken) {
|
|
206
|
+
const normalizedActor = actor.trim();
|
|
207
|
+
if (!normalizedActor)
|
|
208
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Remediation mode recovery actor is required.');
|
|
209
|
+
const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
|
|
210
|
+
if (!step)
|
|
211
|
+
throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
|
|
212
|
+
if (task.status !== 'needs_fix' || step.status !== 'failed') {
|
|
213
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation mode recovery requires the exact failed Task/Step posture.', {
|
|
214
|
+
taskId: task.id,
|
|
215
|
+
taskStatus: task.status,
|
|
216
|
+
stepId,
|
|
217
|
+
stepStatus: step.status,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (!task.planHash)
|
|
221
|
+
throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} is missing its Plan hash.`);
|
|
222
|
+
const remediations = readRawRemediationEvents(store, task);
|
|
223
|
+
const decisions = readRawCorrectiveDecisionEvents(store, task);
|
|
224
|
+
validateRemediationAttemptOrdinals(remediations, task);
|
|
225
|
+
validateRemediationReviewReferences(store, task, remediations);
|
|
226
|
+
validateCorrectiveDecisionCoverage(task, decisions, remediations);
|
|
227
|
+
const malformed = remediations.filter((event) => event.attemptOrdinal > 2 && event.mode === 'ordinary');
|
|
228
|
+
if (malformed.length !== 1 || malformed[0].stepId !== stepId || malformed[0].attemptOrdinal !== 3) {
|
|
229
|
+
throw new WorkflowError('STATE_CORRUPT', 'State does not contain the one exact alpha.7.2 remediation mode omission.', {
|
|
230
|
+
taskId: task.id,
|
|
231
|
+
stepId,
|
|
232
|
+
malformedEventIds: malformed.map((event) => event.eventId),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const stepRemediations = remediations.filter((event) => event.stepId === stepId);
|
|
236
|
+
if (stepRemediations.length !== 3
|
|
237
|
+
|| stepRemediations[0].mode !== 'ordinary'
|
|
238
|
+
|| stepRemediations[1].mode !== 'ordinary'
|
|
239
|
+
|| stepRemediations[2].eventId !== malformed[0].eventId) {
|
|
240
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery requires exactly two ordinary attempts and one malformed third attempt.', {
|
|
241
|
+
taskId: task.id,
|
|
242
|
+
stepId,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const decisionMatches = decisions.filter((event) => event.stepId === stepId
|
|
246
|
+
&& event.triggeringAttemptCount === 3
|
|
247
|
+
&& event.decision === 'continue-fix'
|
|
248
|
+
&& sameStringArray(event.coveredEventIds, stepRemediations.slice(0, 2).map((event) => event.eventId)));
|
|
249
|
+
if (decisionMatches.length !== 1) {
|
|
250
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery requires one exact continue-fix decision.', {
|
|
251
|
+
taskId: task.id,
|
|
252
|
+
stepId,
|
|
253
|
+
decisionEventIds: decisionMatches.map((event) => event.eventId),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const decision = decisionMatches[0];
|
|
257
|
+
const correctiveRecoveries = readRawCorrectiveDecisionRecoveries(store, task).filter((event) => event.stepId === stepId
|
|
258
|
+
&& event.triggeringAttemptCount === 3
|
|
259
|
+
&& event.correctiveDecisionEventId === decision.eventId
|
|
260
|
+
&& event.correctiveDecisionEventHash === decision.eventHash
|
|
261
|
+
&& event.reboundPlanHash === task.planHash);
|
|
262
|
+
if (correctiveRecoveries.length !== 1) {
|
|
263
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery requires one exact corrective-decision recovery.', {
|
|
264
|
+
taskId: task.id,
|
|
265
|
+
stepId,
|
|
266
|
+
recoveryEventIds: correctiveRecoveries.map((event) => event.eventId),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
const correctiveRecovery = correctiveRecoveries[0];
|
|
270
|
+
const authority = requireRemediationModeRecoveryAuthority(store, task, normalizedActor, writerToken);
|
|
271
|
+
const existing = readRemediationModeRecoveryEvents(store, task).filter((event) => event.remediationEventId === malformed[0].eventId);
|
|
272
|
+
if (existing.length > 1) {
|
|
273
|
+
throw new WorkflowError('STATE_CORRUPT', 'Multiple remediation mode recoveries target one event.', {
|
|
274
|
+
taskId: task.id,
|
|
275
|
+
remediationEventId: malformed[0].eventId,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (existing[0]) {
|
|
279
|
+
validateRemediationModeRecoveryBinding(existing[0], malformed[0], decision, correctiveRecovery);
|
|
280
|
+
const retryMismatches = [
|
|
281
|
+
existing[0].taskRevision !== task.revision ? 'taskRevision' : null,
|
|
282
|
+
existing[0].planHash !== task.planHash ? 'planHash' : null,
|
|
283
|
+
existing[0].actor !== normalizedActor ? 'actor' : null,
|
|
284
|
+
existing[0].c1ClaimEventHash !== authority.c1ClaimEventHash ? 'c1ClaimEventHash' : null,
|
|
285
|
+
existing[0].writerLeaseTokenHash !== authority.writerLeaseTokenHash ? 'writerLeaseTokenHash' : null,
|
|
286
|
+
].filter((value) => value !== null);
|
|
287
|
+
if (retryMismatches.length > 0) {
|
|
288
|
+
throw new WorkflowError('STATE_CONFLICT', 'Existing remediation mode recovery conflicts with the requested retry binding.', {
|
|
289
|
+
taskId: task.id,
|
|
290
|
+
stepId,
|
|
291
|
+
recoveryEventId: existing[0].eventId,
|
|
292
|
+
mismatches: retryMismatches,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const evidenceFingerprint = sha256Hex(canonicalJsonStringify({
|
|
297
|
+
taskId: task.id,
|
|
298
|
+
taskRevision: task.revision,
|
|
299
|
+
stepId,
|
|
300
|
+
planHash: task.planHash,
|
|
301
|
+
remediationEventHash: malformed[0].eventHash,
|
|
302
|
+
correctiveDecisionEventHash: decision.eventHash,
|
|
303
|
+
correctiveDecisionRecoveryEventHash: correctiveRecovery.eventHash,
|
|
304
|
+
actor: normalizedActor,
|
|
305
|
+
c1ClaimEventHash: authority.c1ClaimEventHash,
|
|
306
|
+
writerLeaseTokenHash: authority.writerLeaseTokenHash,
|
|
307
|
+
}));
|
|
308
|
+
return {
|
|
309
|
+
task,
|
|
310
|
+
remediation: malformed[0],
|
|
311
|
+
decision,
|
|
312
|
+
correctiveRecovery,
|
|
313
|
+
existingRecovery: existing[0] ?? null,
|
|
314
|
+
actor: normalizedActor,
|
|
315
|
+
c1ClaimEventHash: authority.c1ClaimEventHash,
|
|
316
|
+
writerLeaseTokenHash: authority.writerLeaseTokenHash,
|
|
317
|
+
evidenceFingerprint,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
export function appendRemediationModeRecovery(store, task, stepId, expectedRevision, actor, writerToken, now) {
|
|
321
|
+
if (task.revision !== expectedRevision) {
|
|
322
|
+
throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
|
|
323
|
+
expectedRevision,
|
|
324
|
+
actualRevision: task.revision,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const preflight = assessRemediationModeRecovery(store, task, stepId, actor, writerToken);
|
|
328
|
+
const root = store.taskRoot(task.projectId, task.id);
|
|
329
|
+
const lockFile = path.join(root, REMEDIATION_MODE_RECOVERY_LOCK);
|
|
330
|
+
let descriptor;
|
|
331
|
+
try {
|
|
332
|
+
descriptor = openSync(lockFile, 'wx', 0o600);
|
|
333
|
+
closeSync(descriptor);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (existsSync(lockFile)) {
|
|
337
|
+
throw new WorkflowError('LOCKED', `Remediation mode recovery for ${task.id} is already being serialized.`, {
|
|
338
|
+
taskId: task.id,
|
|
339
|
+
stepId,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const currentTask = store.readTask(task.projectId, task.id);
|
|
346
|
+
if (currentTask.revision !== expectedRevision) {
|
|
347
|
+
throw new WorkflowError('STATE_CONFLICT', `State revision conflict for ${task.id}`, {
|
|
348
|
+
expectedRevision,
|
|
349
|
+
actualRevision: currentTask.revision,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
let underLock;
|
|
353
|
+
try {
|
|
354
|
+
underLock = assessRemediationModeRecovery(store, currentTask, stepId, actor, writerToken);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
throw new WorkflowError('STATE_CONFLICT', 'Remediation mode recovery evidence changed after preflight.', {
|
|
358
|
+
taskId: task.id,
|
|
359
|
+
stepId,
|
|
360
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
if (underLock.existingRecovery)
|
|
364
|
+
return underLock.existingRecovery;
|
|
365
|
+
if (underLock.evidenceFingerprint !== preflight.evidenceFingerprint) {
|
|
366
|
+
throw new WorkflowError('STATE_CONFLICT', 'Remediation mode recovery evidence changed after preflight.', {
|
|
367
|
+
taskId: task.id,
|
|
368
|
+
stepId,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return store.appendSidecarEvent(root, REMEDIATION_MODE_RECOVERIES_SIDECAR, {
|
|
372
|
+
eventId: `RMR-${createUlid(now.getTime())}`,
|
|
373
|
+
recordedAt: now.toISOString(),
|
|
374
|
+
taskId: task.id,
|
|
375
|
+
stepId,
|
|
376
|
+
taskRevision: expectedRevision,
|
|
377
|
+
remediationEventId: underLock.remediation.eventId,
|
|
378
|
+
remediationEventHash: underLock.remediation.eventHash,
|
|
379
|
+
correctiveDecisionEventId: underLock.decision.eventId,
|
|
380
|
+
correctiveDecisionEventHash: underLock.decision.eventHash,
|
|
381
|
+
correctiveDecisionRecoveryEventId: underLock.correctiveRecovery.eventId,
|
|
382
|
+
correctiveDecisionRecoveryEventHash: underLock.correctiveRecovery.eventHash,
|
|
383
|
+
attemptOrdinal: 3,
|
|
384
|
+
planHash: currentTask.planHash,
|
|
385
|
+
actor: underLock.actor,
|
|
386
|
+
c1ClaimEventHash: underLock.c1ClaimEventHash,
|
|
387
|
+
writerLeaseTokenHash: underLock.writerLeaseTokenHash,
|
|
388
|
+
fromMode: 'ordinary',
|
|
389
|
+
toMode: 'corrective',
|
|
390
|
+
reason: 'alpha7.2-recovered-continue-fix-mode-omission',
|
|
391
|
+
}, {
|
|
392
|
+
validateEvent: (event, context) => validateStoredRemediationModeRecovery(event, currentTask, context.file, context.line),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
if (existsSync(lockFile))
|
|
397
|
+
unlinkSync(lockFile);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function requireRemediationModeRecoveryAuthority(store, task, actor, writerToken) {
|
|
401
|
+
const posture = readTaskC1Posture(store, task);
|
|
402
|
+
const lease = readWriterLeaseForModeRecovery(store, task);
|
|
403
|
+
if (posture.state === 'pending') {
|
|
404
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pending C1 handoff must be claimed before remediation mode recovery.');
|
|
405
|
+
}
|
|
406
|
+
if (posture.state === 'claimed') {
|
|
407
|
+
const tokenHash = writerToken ? sha256Hex(writerToken) : null;
|
|
408
|
+
if (!lease
|
|
409
|
+
|| actor !== posture.claimant
|
|
410
|
+
|| lease.owner !== posture.claimant
|
|
411
|
+
|| !writerToken
|
|
412
|
+
|| lease.token !== writerToken
|
|
413
|
+
|| tokenHash !== posture.writerLeaseTokenHash) {
|
|
414
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation mode recovery requires the claimed C1 actor and exact writer token.', {
|
|
415
|
+
taskId: task.id,
|
|
416
|
+
claimant: posture.claimant,
|
|
417
|
+
leaseOwner: lease?.owner ?? null,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return { c1ClaimEventHash: posture.latestEvent.eventHash, writerLeaseTokenHash: tokenHash };
|
|
421
|
+
}
|
|
422
|
+
if (lease && (lease.owner !== actor || !writerToken || lease.token !== writerToken)) {
|
|
423
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Existing writer lease is not owned by the remediation recovery actor/token.');
|
|
424
|
+
}
|
|
425
|
+
if (!lease && writerToken) {
|
|
426
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'A writer token was provided without an existing writer lease.');
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
c1ClaimEventHash: posture.latestEvent?.eventHash ?? null,
|
|
430
|
+
writerLeaseTokenHash: lease ? sha256Hex(lease.token) : null,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function readWriterLeaseForModeRecovery(store, task) {
|
|
434
|
+
const file = path.join(store.lockRoot(task.projectId), `${task.id}.json`);
|
|
435
|
+
if (!existsSync(file))
|
|
436
|
+
return null;
|
|
437
|
+
let lease;
|
|
438
|
+
try {
|
|
439
|
+
lease = JSON.parse(readFileSync(file, 'utf8'));
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
throw new WorkflowError('STATE_CORRUPT', `Writer lease is unreadable: ${file}`);
|
|
443
|
+
}
|
|
444
|
+
if (lease.entityId !== task.id || typeof lease.owner !== 'string' || typeof lease.token !== 'string') {
|
|
445
|
+
throw new WorkflowError('STATE_CORRUPT', `Writer lease does not match Task ${task.id}.`);
|
|
446
|
+
}
|
|
447
|
+
return lease;
|
|
448
|
+
}
|
|
136
449
|
export function appendCorrectiveDecisionEvent(store, task, stepId, correctiveAudit, planRiskAudit, planHash, now) {
|
|
137
450
|
validateCorrectiveDecisionAudit(correctiveAudit);
|
|
138
451
|
const remediationEvents = readRemediationEvents(store, task).filter((event) => event.stepId === stepId);
|
|
@@ -553,6 +866,142 @@ function validateStoredCorrectiveDecisionEvent(event, task, file, line) {
|
|
|
553
866
|
normalizeStringList(event.coveredEventIds, 'Corrective decision coveredEventIds');
|
|
554
867
|
assertHash64(event.planHash, `Corrective decision planHash at ${file}:${line}`);
|
|
555
868
|
}
|
|
869
|
+
function validateStoredCorrectiveDecisionRecoveryReference(event, task, file, line) {
|
|
870
|
+
if (!/^CDR-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.eventId)
|
|
871
|
+
|| event.taskId !== task.id
|
|
872
|
+
|| event.triggeringAttemptCount !== 3) {
|
|
873
|
+
throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery reference does not match the Task/ordinal.', {
|
|
874
|
+
file,
|
|
875
|
+
line,
|
|
876
|
+
taskId: task.id,
|
|
877
|
+
eventTaskId: event.taskId,
|
|
878
|
+
triggeringAttemptCount: event.triggeringAttemptCount,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
if (typeof event.stepId !== 'string' || !event.stepId.trim() || typeof event.actor !== 'string' || !event.actor.trim()) {
|
|
882
|
+
throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery reference is missing Step or actor.', { file, line });
|
|
883
|
+
}
|
|
884
|
+
for (const hash of [
|
|
885
|
+
event.correctiveDecisionEventHash,
|
|
886
|
+
event.decisionPlanHash,
|
|
887
|
+
event.reboundPlanHash,
|
|
888
|
+
event.semanticPlanHash,
|
|
889
|
+
event.fromKnowledgeMapHash,
|
|
890
|
+
event.toKnowledgeMapHash,
|
|
891
|
+
event.sourcePlanRiskAuditEventHash,
|
|
892
|
+
event.reboundPlanRiskAuditEventHash,
|
|
893
|
+
event.knowledgeMapApprovalFingerprint,
|
|
894
|
+
event.taskAuthorizationFingerprint,
|
|
895
|
+
event.delegationGrantFingerprint,
|
|
896
|
+
event.c1EvidenceFingerprint,
|
|
897
|
+
event.evidenceFingerprint,
|
|
898
|
+
]) {
|
|
899
|
+
assertHash64(hash, `Corrective recovery hash at ${file}:${line}`);
|
|
900
|
+
}
|
|
901
|
+
if (!Number.isInteger(event.fromKnowledgeMapRevision)
|
|
902
|
+
|| event.fromKnowledgeMapRevision < 1
|
|
903
|
+
|| !Number.isInteger(event.toKnowledgeMapRevision)
|
|
904
|
+
|| event.toKnowledgeMapRevision < 1) {
|
|
905
|
+
throw new WorkflowError('STATE_CORRUPT', 'Corrective recovery Knowledge Map revisions are malformed.', { file, line });
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
function validateStoredRemediationModeRecovery(event, task, file, line) {
|
|
909
|
+
if (!/^RMR-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.eventId)) {
|
|
910
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery eventId is malformed.', { file, line });
|
|
911
|
+
}
|
|
912
|
+
if (event.taskId !== task.id || typeof event.stepId !== 'string' || !event.stepId.trim()) {
|
|
913
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery does not match the current Task.', {
|
|
914
|
+
file,
|
|
915
|
+
line,
|
|
916
|
+
taskId: task.id,
|
|
917
|
+
eventTaskId: event.taskId,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
if (!Number.isInteger(event.taskRevision) || event.taskRevision < 1 || event.attemptOrdinal !== 3) {
|
|
921
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery has an invalid revision or ordinal.', { file, line });
|
|
922
|
+
}
|
|
923
|
+
if (event.fromMode !== 'ordinary'
|
|
924
|
+
|| event.toMode !== 'corrective'
|
|
925
|
+
|| event.reason !== 'alpha7.2-recovered-continue-fix-mode-omission') {
|
|
926
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery has an unsupported correction.', { file, line });
|
|
927
|
+
}
|
|
928
|
+
if (!/^RME-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.remediationEventId)
|
|
929
|
+
|| !/^CRD-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.correctiveDecisionEventId)
|
|
930
|
+
|| !/^CDR-[0-9A-HJKMNP-TV-Z]{26}$/.test(event.correctiveDecisionRecoveryEventId)
|
|
931
|
+
|| typeof event.actor !== 'string'
|
|
932
|
+
|| !event.actor.trim()) {
|
|
933
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery identifiers are malformed.', { file, line });
|
|
934
|
+
}
|
|
935
|
+
for (const [label, value] of [
|
|
936
|
+
['remediationEventHash', event.remediationEventHash],
|
|
937
|
+
['correctiveDecisionEventHash', event.correctiveDecisionEventHash],
|
|
938
|
+
['correctiveDecisionRecoveryEventHash', event.correctiveDecisionRecoveryEventHash],
|
|
939
|
+
['planHash', event.planHash],
|
|
940
|
+
]) {
|
|
941
|
+
assertHash64(value, `Remediation mode recovery ${label} at ${file}:${line}`);
|
|
942
|
+
}
|
|
943
|
+
if (event.c1ClaimEventHash !== null)
|
|
944
|
+
assertHash64(event.c1ClaimEventHash, `Remediation mode recovery C1 hash at ${file}:${line}`);
|
|
945
|
+
if (event.writerLeaseTokenHash !== null)
|
|
946
|
+
assertHash64(event.writerLeaseTokenHash, `Remediation mode recovery token hash at ${file}:${line}`);
|
|
947
|
+
}
|
|
948
|
+
function validateRemediationModeRecoveryBinding(recovery, remediation, decision, correctiveRecovery) {
|
|
949
|
+
const mismatches = [
|
|
950
|
+
recovery.taskId !== remediation.taskId ? 'taskId' : null,
|
|
951
|
+
recovery.stepId !== remediation.stepId ? 'stepId' : null,
|
|
952
|
+
recovery.remediationEventId !== remediation.eventId ? 'remediationEventId' : null,
|
|
953
|
+
recovery.remediationEventHash !== remediation.eventHash ? 'remediationEventHash' : null,
|
|
954
|
+
recovery.correctiveDecisionEventId !== decision.eventId ? 'correctiveDecisionEventId' : null,
|
|
955
|
+
recovery.correctiveDecisionEventHash !== decision.eventHash ? 'correctiveDecisionEventHash' : null,
|
|
956
|
+
recovery.correctiveDecisionRecoveryEventId !== correctiveRecovery.eventId ? 'correctiveDecisionRecoveryEventId' : null,
|
|
957
|
+
recovery.correctiveDecisionRecoveryEventHash !== correctiveRecovery.eventHash ? 'correctiveDecisionRecoveryEventHash' : null,
|
|
958
|
+
recovery.attemptOrdinal !== remediation.attemptOrdinal ? 'attemptOrdinal' : null,
|
|
959
|
+
recovery.planHash !== correctiveRecovery.reboundPlanHash ? 'planHash' : null,
|
|
960
|
+
recovery.actor !== correctiveRecovery.actor ? 'actor' : null,
|
|
961
|
+
].filter((value) => value !== null);
|
|
962
|
+
if (mismatches.length > 0) {
|
|
963
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery does not match its append-only evidence.', {
|
|
964
|
+
recoveryEventId: recovery.eventId,
|
|
965
|
+
mismatches,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
function validateRemediationModeRecoveries(store, task, remediations, decisions, recoveries) {
|
|
970
|
+
const correctiveRecoveries = readRawCorrectiveDecisionRecoveries(store, task);
|
|
971
|
+
const recoveredEventIds = new Set();
|
|
972
|
+
for (const recovery of recoveries) {
|
|
973
|
+
if (recoveredEventIds.has(recovery.remediationEventId)) {
|
|
974
|
+
throw new WorkflowError('STATE_CORRUPT', 'Duplicate remediation mode recoveries target one event.', {
|
|
975
|
+
taskId: task.id,
|
|
976
|
+
remediationEventId: recovery.remediationEventId,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
const remediation = remediations.find((event) => event.eventId === recovery.remediationEventId) ?? null;
|
|
980
|
+
const decision = decisions.find((event) => event.eventId === recovery.correctiveDecisionEventId) ?? null;
|
|
981
|
+
const correctiveRecovery = correctiveRecoveries.find((event) => event.eventId === recovery.correctiveDecisionRecoveryEventId) ?? null;
|
|
982
|
+
if (!remediation || !decision || !correctiveRecovery) {
|
|
983
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery references missing evidence.', {
|
|
984
|
+
taskId: task.id,
|
|
985
|
+
recoveryEventId: recovery.eventId,
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
if (recovery.taskRevision !== task.revision
|
|
989
|
+
|| recovery.planHash !== task.planHash
|
|
990
|
+
|| remediation.mode !== 'ordinary'
|
|
991
|
+
|| remediation.attemptOrdinal !== 3
|
|
992
|
+
|| decision.decision !== 'continue-fix'
|
|
993
|
+
|| decision.triggeringAttemptCount !== 3
|
|
994
|
+
|| correctiveRecovery.correctiveDecisionEventId !== decision.eventId) {
|
|
995
|
+
throw new WorkflowError('STATE_CORRUPT', 'Remediation mode recovery references an ineligible lifecycle shape.', {
|
|
996
|
+
taskId: task.id,
|
|
997
|
+
recoveryEventId: recovery.eventId,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
validateRemediationModeRecoveryBinding(recovery, remediation, decision, correctiveRecovery);
|
|
1001
|
+
recoveredEventIds.add(remediation.eventId);
|
|
1002
|
+
}
|
|
1003
|
+
return recoveredEventIds;
|
|
1004
|
+
}
|
|
556
1005
|
function validateRemediationAttemptOrdinals(events, task) {
|
|
557
1006
|
const attemptsByStep = new Map();
|
|
558
1007
|
for (const event of events) {
|
|
@@ -712,7 +1161,7 @@ function validateCorrectiveDecisionCoverage(task, decisions, remediationEvents)
|
|
|
712
1161
|
}
|
|
713
1162
|
}
|
|
714
1163
|
}
|
|
715
|
-
function validateHistoricalRemediationModes(task, stepId, remediationEvents, correctiveDecisions) {
|
|
1164
|
+
function validateHistoricalRemediationModes(task, stepId, remediationEvents, correctiveDecisions, recoveredEventIds) {
|
|
716
1165
|
for (const event of remediationEvents) {
|
|
717
1166
|
if (event.attemptOrdinal <= 2) {
|
|
718
1167
|
if (event.mode !== 'ordinary') {
|
|
@@ -726,7 +1175,7 @@ function validateHistoricalRemediationModes(task, stepId, remediationEvents, cor
|
|
|
726
1175
|
}
|
|
727
1176
|
continue;
|
|
728
1177
|
}
|
|
729
|
-
if (event.mode === 'ordinary') {
|
|
1178
|
+
if (event.mode === 'ordinary' && !recoveredEventIds.has(event.eventId)) {
|
|
730
1179
|
throw new WorkflowError('STATE_CORRUPT', 'Guarded remediation attempt ordinals above 2 must not remain ordinary.', {
|
|
731
1180
|
taskId: task.id,
|
|
732
1181
|
stepId,
|
|
@@ -753,13 +1202,13 @@ function validateHistoricalRemediationModes(task, stepId, remediationEvents, cor
|
|
|
753
1202
|
}
|
|
754
1203
|
}
|
|
755
1204
|
}
|
|
756
|
-
function validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisions) {
|
|
1205
|
+
function validateHistoricalRemediationModesForAllSteps(task, remediationEvents, correctiveDecisions, recoveredEventIds) {
|
|
757
1206
|
const stepIds = new Set(remediationEvents.map((event) => event.stepId));
|
|
758
1207
|
for (const stepId of stepIds) {
|
|
759
1208
|
const stepEvents = remediationEvents
|
|
760
1209
|
.filter((event) => event.stepId === stepId)
|
|
761
1210
|
.sort((left, right) => left.attemptOrdinal - right.attemptOrdinal);
|
|
762
|
-
validateHistoricalRemediationModes(task, stepId, stepEvents, correctiveDecisions);
|
|
1211
|
+
validateHistoricalRemediationModes(task, stepId, stepEvents, correctiveDecisions, recoveredEventIds);
|
|
763
1212
|
}
|
|
764
1213
|
}
|
|
765
1214
|
function validateCorrectiveDecisionAudit(audit) {
|