@fitlab-ai/agent-infra 0.11.2 → 0.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/bin/internal-cli.js +7 -2
  2. package/dist/lib/agent-clients/adapter.js +3 -0
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
  6. package/dist/lib/agent-clients/adapters/codex.js +10 -0
  7. package/dist/lib/internal/manual-validation.js +22 -27
  8. package/dist/lib/internal/sandbox-control.js +1 -43
  9. package/dist/lib/internal/task-lifecycle.js +1 -1
  10. package/dist/lib/internal/task-operation-registry.js +6 -2
  11. package/dist/lib/platform/issue-comments.js +2 -31
  12. package/dist/lib/platform/pr-summary.js +7 -2
  13. package/dist/lib/sandbox/control/client.js +3 -44
  14. package/dist/lib/sandbox/control/executor.js +14 -79
  15. package/dist/lib/sandbox/control/finalization-response.js +3 -5
  16. package/dist/lib/sandbox/control/protocol.js +5 -64
  17. package/dist/lib/sandbox/control/server.js +29 -206
  18. package/dist/lib/sandbox/control/state.js +1 -7
  19. package/dist/lib/sandbox/control/task-view.js +1 -2
  20. package/dist/lib/task/control-authority.js +34 -36
  21. package/dist/lib/task/control-recovery.js +3 -42
  22. package/dist/lib/task/delegation-receipts.js +2 -2
  23. package/dist/lib/task/finalization-artifacts.js +1 -3
  24. package/dist/lib/task/finalization-state.js +1 -1
  25. package/dist/lib/task/finalization.js +53 -126
  26. package/dist/lib/task/lifecycle.js +1 -1
  27. package/dist/lib/task/manual-validation-receipt.js +8 -3
  28. package/dist/lib/task/orchestration.js +12 -6
  29. package/dist/lifecycle-build-manifest.json +3 -3
  30. package/lib/agent-clients/adapter.ts +46 -0
  31. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
  32. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
  33. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
  34. package/lib/agent-clients/adapters/codex.ts +10 -0
  35. package/lib/internal/manual-validation.ts +22 -26
  36. package/lib/internal/sandbox-control.ts +0 -42
  37. package/lib/internal/task-lifecycle.ts +2 -2
  38. package/lib/internal/task-operation-registry.ts +7 -4
  39. package/lib/platform/issue-comments.ts +1 -35
  40. package/lib/platform/pr-summary.ts +10 -2
  41. package/lib/sandbox/control/client.ts +5 -68
  42. package/lib/sandbox/control/executor.ts +14 -81
  43. package/lib/sandbox/control/finalization-response.ts +3 -5
  44. package/lib/sandbox/control/protocol.ts +7 -83
  45. package/lib/sandbox/control/server.ts +30 -220
  46. package/lib/sandbox/control/state.ts +1 -9
  47. package/lib/sandbox/control/task-view.ts +1 -2
  48. package/lib/task/control-authority.ts +38 -47
  49. package/lib/task/control-recovery.ts +3 -43
  50. package/lib/task/delegation-receipts.ts +2 -2
  51. package/lib/task/finalization-artifacts.ts +1 -3
  52. package/lib/task/finalization-state.ts +1 -1
  53. package/lib/task/finalization.ts +57 -121
  54. package/lib/task/lifecycle.ts +2 -2
  55. package/lib/task/manual-validation-receipt.ts +8 -2
  56. package/lib/task/orchestration.ts +30 -19
  57. package/package.json +1 -1
  58. package/templates/.agents/rules/lifecycle-orchestration.en.md +19 -5
  59. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +19 -5
  60. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  61. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  62. package/templates/.agents/skills/complete-task/SKILL.en.md +6 -7
  63. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +6 -7
  64. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
  65. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
  66. package/templates/.agents/skills/run-task/SKILL.en.md +9 -8
  67. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -8
  68. package/dist/lib/sandbox/control/task-workflow.js +0 -74
  69. package/dist/lib/sandbox/control/workflow-executor.js +0 -142
  70. package/dist/lib/task/lifecycle-recovery.js +0 -549
  71. package/dist/lib/task/recovery-warning.js +0 -19
  72. package/dist/lib/task/workflow-command.js +0 -21
  73. package/dist/lib/task/workflow-dispatch.js +0 -213
  74. package/lib/sandbox/control/task-workflow.ts +0 -95
  75. package/lib/sandbox/control/workflow-executor.ts +0 -156
  76. package/lib/task/lifecycle-recovery.ts +0 -691
  77. package/lib/task/recovery-warning.ts +0 -21
  78. package/lib/task/workflow-command.ts +0 -22
  79. package/lib/task/workflow-dispatch.ts +0 -214
@@ -1,691 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
5
- import type { StoredCodexLifecycle } from '../agent-clients/adapters/codex-lifecycle/store.ts';
6
- import { managedDelegationRole } from './delegation-receipts.ts';
7
- import type { DelegationReceipt } from './delegation-receipts.ts';
8
- import { normalizeAgentToken } from '../agent-clients/tokens.ts';
9
- import { artifactName, parseArtifactName } from './artifact-name.ts';
10
- import { locateActivityLog, pairEntries } from './activity-log.ts';
11
- import type { LogEntry, StepRow } from './activity-log.ts';
12
- import { captureTaskWriteMetadata, writeTask } from './write.ts';
13
- import { parseTypedTaskFrontmatter } from './frontmatter.ts';
14
- import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.ts';
15
- import { resolveAgentRuntimeStoreRoot } from '../runtime/agent-runtime.ts';
16
- import {
17
- recoverActivatedOrchestrationDelegationUnderLock,
18
- readRun
19
- } from './orchestration.ts';
20
- import type { OrchestrationOptions, OrchestrationRun } from './orchestration.ts';
21
- import { isRecoveryWarning, sameRecoveryWarning } from './recovery-warning.ts';
22
- import type { RecoveryWarning } from './recovery-warning.ts';
23
-
24
- const RECOVERY_NOTE_PREFIX = 'lifecycle-recovery:v1 ';
25
- const RECOVERY_STAGES = ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'] as const;
26
- const ACTIONS: Readonly<Record<typeof RECOVERY_STAGES[number], string>> = {
27
- analysis: 'Analyze Task',
28
- 'review-analysis': 'Review Analysis',
29
- plan: 'Plan Task',
30
- 'review-plan': 'Review Plan',
31
- code: 'Code Task',
32
- 'review-code': 'Review Code'
33
- };
34
- const RECOVERY_NOTE_KEYS = [
35
- 'version', 'taskId', 'stage', 'round', 'artifact', 'startedAgent', 'receiptId', 'childId',
36
- 'stopRevision', 'consumer', 'consumedAt', 'owner', 'reason'
37
- ] as const;
38
-
39
- type RecoveryStage = (typeof RECOVERY_STAGES)[number];
40
- type LifecycleRecoveryRequest = Readonly<{
41
- taskRef: string;
42
- intent: 'recover-started';
43
- agent: string;
44
- stage: RecoveryStage;
45
- round: number;
46
- artifact: string;
47
- reason: string;
48
- }>;
49
- type RecoveryCommitVerification = { ok: true; receipt: DelegationReceipt } | { ok: false; message: string };
50
- type LifecycleRecoveryResult = Readonly<{
51
- status: 'applied' | 'no-op' | 'owner-unknown' | 'conflict';
52
- changed: boolean;
53
- targetState: 'active';
54
- requestRef: string;
55
- intent: 'recover-started';
56
- taskId: string | null;
57
- stage: RecoveryStage | null;
58
- round: number | null;
59
- artifact: string | null;
60
- receiptId: string | null;
61
- childId: string | null;
62
- warning: RecoveryWarning | null;
63
- error: Readonly<{ code: string; message: string }> | null;
64
- }>;
65
- type LifecycleRecoveryOptions = Readonly<{
66
- repoRoot?: string;
67
- now?: () => string;
68
- orchestration?: Pick<OrchestrationOptions, 'diagnosticLog' | 'now'>;
69
- lifecycleStore?: ReturnType<typeof createCodexLifecycleStore>;
70
- releaseRecovery?: (childThreadId: string, consumer: string) => boolean;
71
- writeTask?: typeof writeTask;
72
- verifyRecoveryCommit?: (
73
- taskMdPath: string,
74
- taskDir: string,
75
- taskId: string,
76
- request: LifecycleRecoveryRequest,
77
- note: RecoveryNote,
78
- options: LifecycleRecoveryOptions
79
- ) => RecoveryCommitVerification;
80
- }>;
81
-
82
- type RecoveryNote = Readonly<{
83
- version: 1;
84
- taskId: string;
85
- stage: RecoveryStage;
86
- round: number;
87
- artifact: string;
88
- startedAgent: string;
89
- receiptId: string;
90
- childId: string;
91
- stopRevision: number;
92
- consumer: string;
93
- consumedAt: string;
94
- owner: 'terminated';
95
- reason: string;
96
- }>;
97
- type RecoveryTerminalFacts = Readonly<{
98
- note: RecoveryNote;
99
- receipt: DelegationReceipt;
100
- consumer: string;
101
- stored: StoredCodexLifecycle | null;
102
- }>;
103
- type RecoveryTerminalFactsResult =
104
- | { ok: true; facts: RecoveryTerminalFacts }
105
- | { ok: false; code: string; message: string };
106
-
107
- const RECOVERY_RELEASE_RETRY_WARNING: RecoveryWarning = Object.freeze({
108
- code: 'RECOVERY_RELEASE_RETRY_REQUIRED',
109
- message: 'recovery receipt and Activity Log are complete but the protected lifecycle claim could not be released',
110
- action: 'retry recover-started with the same selector and reason'
111
- });
112
-
113
- function text(value: unknown): value is string {
114
- return typeof value === 'string' && value.length > 0 && value.trim() === value && !/[\r\n]/u.test(value);
115
- }
116
-
117
- function isReleaseRetryWarning(value: unknown): value is RecoveryWarning {
118
- return sameRecoveryWarning(value, RECOVERY_RELEASE_RETRY_WARNING);
119
- }
120
-
121
- function timestamp(value: unknown): value is string {
122
- return text(value) && Number.isFinite(Date.parse(value));
123
- }
124
-
125
- function recoveryConsumer(taskId: string, receiptId: string): string {
126
- return `lifecycle-recovery:${taskId}:${receiptId}`;
127
- }
128
-
129
- function actionPattern(stage: RecoveryStage, round: number): RegExp {
130
- const escaped = ACTIONS[stage].replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
131
- return new RegExp(`^${escaped} \\(Round ${round}(?:, (?:fix for review-code(?:-r[2-9]|-r[1-9]\\d+)?.md|decision II-[1-9]\\d*))?\\)$`, 'u');
132
- }
133
-
134
- function baseStep(step: string): string {
135
- return step.replace(/\s*\[(?:started|aborted)\]\s*$/u, '');
136
- }
137
-
138
- function isRecoveryStage(value: unknown): value is RecoveryStage {
139
- return (RECOVERY_STAGES as readonly string[]).includes(value as string);
140
- }
141
-
142
- function parseRecoveryNote(note: string): RecoveryNote | null {
143
- if (!note.startsWith(RECOVERY_NOTE_PREFIX)) return null;
144
- let value: unknown;
145
- try { value = JSON.parse(note.slice(RECOVERY_NOTE_PREFIX.length)); }
146
- catch { return null; }
147
- if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
148
- const record = value as Record<string, unknown>;
149
- if (Object.keys(record).sort().join(',') !== [...RECOVERY_NOTE_KEYS].sort().join(',')) return null;
150
- if (
151
- record.version !== 1
152
- || typeof record.taskId !== 'string' || !TASK_ID_RE.test(record.taskId)
153
- || !isRecoveryStage(record.stage)
154
- || !Number.isSafeInteger(record.round) || (record.round as number) < 1
155
- || !text(record.artifact)
156
- || !text(record.startedAgent)
157
- || !text(record.receiptId)
158
- || !text(record.childId)
159
- || !Number.isSafeInteger(record.stopRevision) || (record.stopRevision as number) < 1
160
- || !text(record.consumer)
161
- || !timestamp(record.consumedAt)
162
- || record.owner !== 'terminated'
163
- || !text(record.reason)
164
- ) return null;
165
- const parsedArtifact = parseArtifactName(record.artifact);
166
- if (!parsedArtifact || parsedArtifact.family !== record.stage || parsedArtifact.round !== record.round) return null;
167
- if (record.consumer !== recoveryConsumer(record.taskId, record.receiptId)) return null;
168
- return record as RecoveryNote;
169
- }
170
-
171
- function renderRecoveryNote(note: RecoveryNote): string {
172
- return `${RECOVERY_NOTE_PREFIX}${JSON.stringify(note)}`;
173
- }
174
-
175
- function result(
176
- request: LifecycleRecoveryRequest,
177
- status: LifecycleRecoveryResult['status'],
178
- extra: Partial<LifecycleRecoveryResult> = {}
179
- ): LifecycleRecoveryResult {
180
- return {
181
- status,
182
- changed: status === 'applied',
183
- targetState: 'active',
184
- requestRef: request.taskRef,
185
- intent: request.intent,
186
- taskId: null,
187
- stage: request.stage,
188
- round: request.round,
189
- artifact: request.artifact,
190
- receiptId: null,
191
- childId: null,
192
- warning: null,
193
- error: null,
194
- ...extra
195
- };
196
- }
197
-
198
- function failure(
199
- request: LifecycleRecoveryRequest,
200
- status: 'owner-unknown' | 'conflict',
201
- code: string,
202
- message: string,
203
- extra: Partial<LifecycleRecoveryResult> = {}
204
- ): LifecycleRecoveryResult {
205
- return result(request, status, { error: { code, message }, ...extra });
206
- }
207
-
208
- function recoveryFailure(
209
- request: LifecycleRecoveryRequest,
210
- status: 'owner-unknown' | 'conflict',
211
- code: string,
212
- message: string
213
- ): LifecycleRecoveryResult {
214
- return failure(request, status, code, message);
215
- }
216
-
217
- function normalizeRequest(request: LifecycleRecoveryRequest): LifecycleRecoveryRequest | { code: string; message: string } {
218
- if (
219
- !request || typeof request !== 'object'
220
- || request.intent !== 'recover-started'
221
- || typeof request.taskRef !== 'string' || !request.taskRef.trim()
222
- || typeof request.agent !== 'string'
223
- || !isRecoveryStage(request.stage)
224
- || !Number.isSafeInteger(request.round) || request.round < 1
225
- || typeof request.artifact !== 'string'
226
- || typeof request.reason !== 'string' || !request.reason.trim() || /[\r\n]/u.test(request.reason)
227
- ) return { code: 'RECOVERY_PAYLOAD_INVALID', message: 'recover-started requires a task, agent, stage, round, artifact, and single-line reason' };
228
- const agent = normalizeAgentToken(request.agent);
229
- if (!agent) return { code: 'RECOVERY_PAYLOAD_INVALID', message: 'recovery agent is not a recognized agent token' };
230
- const expectedArtifact = artifactName(request.stage, request.round);
231
- if (request.artifact !== expectedArtifact) {
232
- return { code: 'RECOVERY_PAYLOAD_INVALID', message: `artifact must be ${expectedArtifact} for ${request.stage} round ${request.round}` };
233
- }
234
- return { ...request, agent, reason: request.reason.trim() };
235
- }
236
-
237
- function targetRows(sectionEntries: readonly LogEntry[], stage: RecoveryStage, round: number): {
238
- rows: StepRow[];
239
- recoveryEntries: LogEntry[];
240
- } {
241
- const pattern = actionPattern(stage, round);
242
- const rows = pairEntries([...sectionEntries]).filter((row) => pattern.test(row.step));
243
- const recoveryEntries = sectionEntries.filter((entry) => entry.step.endsWith(' [aborted]') && pattern.test(baseStep(entry.step)));
244
- return { rows, recoveryEntries };
245
- }
246
-
247
- function validateStopRecord(
248
- record: StoredCodexLifecycle,
249
- receipt: DelegationReceipt,
250
- consumer: string
251
- ): { ok: true; stopRevision: number; consumedAt: string } | { ok: false; code: string; message: string } {
252
- const start = record.state.startEvidence;
253
- const stop = record.state.stopEvidence;
254
- const provenance = receipt.lifecycleProvenance;
255
- const host = receipt.hostEvidence;
256
- if (
257
- receipt.client !== 'codex'
258
- || !provenance
259
- || host?.kind !== 'codex-lifecycle-v2'
260
- || !start
261
- || !stop
262
- || record.state.status !== 'stop-ready'
263
- || stop.terminalStatus !== 'completed'
264
- || stop.hookStopObserved !== true
265
- || start.childThreadId !== receipt.childId
266
- || start.parentThreadId !== receipt.parentId
267
- || managedDelegationRole(start.nativeAgent) !== receipt.role
268
- || start.hookDefinitionHash !== provenance.hookDefinitionHash
269
- || host.hookDefinitionHash !== provenance.hookDefinitionHash
270
- || host.capabilitySessionId !== provenance.capabilitySessionId
271
- || host.capabilityTurnId !== provenance.capabilityTurnId
272
- || host.capabilityToolUseId !== provenance.capabilityToolUseId
273
- || host.spawnToolUseId !== start.spawnToolUseId
274
- || !host.spawnObservedAt
275
- || !record.spawnObservedAt
276
- || host.spawnObservedAt !== record.spawnObservedAt
277
- || host.controllerInstanceDigest !== provenance.controllerInstanceDigest
278
- || host.controlGeneration !== provenance.controlGeneration
279
- || typeof host.controllerInstanceDigest !== 'string'
280
- || typeof host.controlGeneration !== 'string'
281
- || host.startRevision < 1
282
- || record.revision <= host.startRevision
283
- ) return { ok: false, code: 'RECOVERY_STOP_EVIDENCE_INVALID', message: 'Codex stop evidence does not match the managed activated receipt' };
284
- if (record.state.child?.childThreadId !== receipt.childId || record.state.stop?.childThreadId !== receipt.childId) {
285
- return { ok: false, code: 'RECOVERY_STOP_EVIDENCE_INVALID', message: 'Codex stop evidence child identity is inconsistent' };
286
- }
287
- if (record.consumer !== null && record.consumer !== consumer) {
288
- return { ok: false, code: 'RECOVERY_CONSUMER_CONFLICT', message: `Codex lifecycle evidence was consumed by '${record.consumer}'` };
289
- }
290
- if (record.consumer === null) return { ok: true, stopRevision: record.revision, consumedAt: '' };
291
- if (receipt.status === 'activated' && record.consumer === consumer && record.consumedAt) {
292
- return { ok: true, stopRevision: record.revision, consumedAt: record.consumedAt };
293
- }
294
- if (
295
- record.consumer !== consumer
296
- || !record.consumedAt
297
- || receipt.hostEvidence?.stopRevision !== record.revision
298
- || receipt.hostEvidence.consumer !== consumer
299
- || receipt.hostEvidence.consumedAt !== record.consumedAt
300
- ) return { ok: false, code: 'RECOVERY_REFERENCE_CONFLICT', message: 'recovery receipt references do not match the protected lifecycle claim' };
301
- return { ok: true, stopRevision: record.revision, consumedAt: record.consumedAt };
302
- }
303
-
304
- function readLifecycleStore(
305
- options: LifecycleRecoveryOptions,
306
- repoRoot: string
307
- ): ReturnType<typeof createCodexLifecycleStore> {
308
- if (options.lifecycleStore) return options.lifecycleStore;
309
- return createCodexLifecycleStore({
310
- root: resolveAgentRuntimeStoreRoot({ repoRoot, store: 'lifecycle' }),
311
- cliVersion: 'recovery',
312
- now: options.now
313
- });
314
- }
315
-
316
- function readStoredEvidence(
317
- store: ReturnType<typeof createCodexLifecycleStore>,
318
- childId: string
319
- ): StoredCodexLifecycle | { missing: true } | { error: Error } {
320
- try {
321
- return store.read(childId);
322
- } catch (error) {
323
- const message = error instanceof Error ? error.message : String(error);
324
- if (message.includes('was not found uniquely')) return { missing: true };
325
- return { error: error instanceof Error ? error : new Error(message) };
326
- }
327
- }
328
-
329
- function taskNote(
330
- taskId: string,
331
- request: LifecycleRecoveryRequest,
332
- receipt: DelegationReceipt,
333
- stopRevision: number,
334
- consumer: string,
335
- consumedAt: string
336
- ): RecoveryNote {
337
- return {
338
- version: 1,
339
- taskId,
340
- stage: request.stage,
341
- round: request.round,
342
- artifact: request.artifact,
343
- startedAgent: request.agent,
344
- receiptId: receipt.id,
345
- childId: receipt.childId!,
346
- stopRevision,
347
- consumer,
348
- consumedAt,
349
- owner: 'terminated',
350
- reason: request.reason
351
- };
352
- }
353
-
354
- function releaseResult(
355
- request: LifecycleRecoveryRequest,
356
- taskId: string,
357
- receipt: DelegationReceipt,
358
- released: boolean,
359
- previousChange: boolean
360
- ): LifecycleRecoveryResult {
361
- if (released) return result(request, 'applied', { taskId, receiptId: receipt.id, childId: receipt.childId });
362
- return result(request, 'applied', {
363
- changed: previousChange,
364
- taskId,
365
- receiptId: receipt.id,
366
- childId: receipt.childId,
367
- warning: RECOVERY_RELEASE_RETRY_WARNING
368
- });
369
- }
370
-
371
- function isPostActivationAbortedReceipt(
372
- receipt: DelegationReceipt,
373
- taskId: string,
374
- request: LifecycleRecoveryRequest
375
- ): boolean {
376
- return receipt.status === 'aborted'
377
- && receipt.activatedAt !== null
378
- && receipt.agent === null
379
- && receipt.taskId === taskId
380
- && receipt.stage === request.stage
381
- && receipt.round === request.round
382
- && receipt.artifact === request.artifact;
383
- }
384
-
385
- function matchingPostActivationAbortedReceipts(
386
- run: OrchestrationRun,
387
- taskId: string,
388
- request: LifecycleRecoveryRequest
389
- ): DelegationReceipt[] {
390
- return run.receipts.filter((receipt) => isPostActivationAbortedReceipt(receipt, taskId, request));
391
- }
392
-
393
- function sameRecoveryNote(left: RecoveryNote, right: RecoveryNote): boolean {
394
- return left.version === right.version
395
- && left.taskId === right.taskId
396
- && left.stage === right.stage
397
- && left.round === right.round
398
- && left.artifact === right.artifact
399
- && left.startedAgent === right.startedAgent
400
- && left.receiptId === right.receiptId
401
- && left.childId === right.childId
402
- && left.stopRevision === right.stopRevision
403
- && left.consumer === right.consumer
404
- && left.consumedAt === right.consumedAt
405
- && left.owner === right.owner
406
- && left.reason === right.reason;
407
- }
408
-
409
- function recoveryNoteMatchesRequest(
410
- note: RecoveryNote,
411
- taskId: string,
412
- request: LifecycleRecoveryRequest
413
- ): boolean {
414
- return note.taskId === taskId
415
- && note.stage === request.stage
416
- && note.round === request.round
417
- && note.artifact === request.artifact
418
- && note.startedAgent === request.agent
419
- && note.reason === request.reason;
420
- }
421
-
422
- function recoveryReferencesMatch(
423
- receipt: DelegationReceipt,
424
- note: RecoveryNote,
425
- consumer: string
426
- ): boolean {
427
- return receipt.childId === note.childId
428
- && note.consumer === consumer
429
- && receipt.hostEvidence?.stopRevision === note.stopRevision
430
- && receipt.hostEvidence.consumer === consumer
431
- && receipt.hostEvidence.consumedAt === note.consumedAt;
432
- }
433
-
434
- function recoveryRunIsStable(run: OrchestrationRun | null): run is OrchestrationRun {
435
- return run !== null && run.status === 'running' && run.pendingDelegation === null;
436
- }
437
-
438
- function recoveryTerminalFailureStatus(code: string): 'owner-unknown' | 'conflict' {
439
- switch (code) {
440
- case 'RECOVERY_LOG_CONFLICT':
441
- case 'RECOVERY_NOTE_INVALID':
442
- case 'RECOVERY_ORCHESTRATION_INVALID':
443
- case 'RECOVERY_RECEIPT_INVALID':
444
- case 'RECOVERY_REFERENCE_CONFLICT':
445
- case 'RECOVERY_CONSUMER_CONFLICT':
446
- return 'conflict';
447
- default:
448
- return 'owner-unknown';
449
- }
450
- }
451
-
452
- function readRecoveryTerminalFacts(
453
- section: Readonly<{ entries: readonly LogEntry[] }>,
454
- taskId: string,
455
- request: LifecycleRecoveryRequest,
456
- run: OrchestrationRun | null,
457
- store: ReturnType<typeof createCodexLifecycleStore>
458
- ): RecoveryTerminalFactsResult {
459
- const targets = targetRows(section.entries, request.stage, request.round);
460
- const paired = targets.rows.filter((row) => row.started !== '' && row.done !== '');
461
- if (targets.recoveryEntries.length !== 1 || paired.length !== 1
462
- || paired[0]!.done !== targets.recoveryEntries[0]!.time) {
463
- return { ok: false, code: 'RECOVERY_LOG_CONFLICT', message: 'recovery Activity Log does not contain one completed started/aborted pair' };
464
- }
465
- const note = parseRecoveryNote(targets.recoveryEntries[0]!.note);
466
- if (!note || !recoveryNoteMatchesRequest(note, taskId, request)) {
467
- return { ok: false, code: 'RECOVERY_NOTE_INVALID', message: 'recovery Activity Log note does not match the selector' };
468
- }
469
- if (!recoveryRunIsStable(run)) {
470
- return { ok: false, code: 'RECOVERY_ORCHESTRATION_INVALID', message: 'completed recovery state must be running with no pending delegation' };
471
- }
472
- const receipts = matchingPostActivationAbortedReceipts(run, taskId, request);
473
- if (receipts.length !== 1 || receipts[0]!.id !== note.receiptId) {
474
- return { ok: false, code: 'RECOVERY_RECEIPT_INVALID', message: 'there is not one matching post-activation aborted receipt' };
475
- }
476
- const receipt = receipts[0]!;
477
- const consumer = recoveryConsumer(taskId, receipt.id);
478
- if (!recoveryReferencesMatch(receipt, note, consumer)) {
479
- return { ok: false, code: 'RECOVERY_REFERENCE_CONFLICT', message: 'recovery receipt and Activity Log references do not match' };
480
- }
481
- const stored = readStoredEvidence(store, note.childId);
482
- if ('error' in stored) return { ok: false, code: 'RECOVERY_STORE_UNKNOWN', message: stored.error.message };
483
- if ('missing' in stored) return { ok: true, facts: { note, receipt, consumer, stored: null } };
484
- const evidence = validateStopRecord(stored, receipt, consumer);
485
- if (!evidence.ok) return evidence;
486
- if (stored.consumer !== consumer || !stored.consumedAt
487
- || evidence.stopRevision !== note.stopRevision || evidence.consumedAt !== note.consumedAt) {
488
- return { ok: false, code: 'RECOVERY_REFERENCE_CONFLICT', message: 'complete recovery facts do not reference the protected recovery claim' };
489
- }
490
- return { ok: true, facts: { note, receipt, consumer, stored } };
491
- }
492
-
493
- function verifyRecoveryCommit(
494
- taskMdPath: string,
495
- taskDir: string,
496
- taskId: string,
497
- request: LifecycleRecoveryRequest,
498
- note: RecoveryNote,
499
- options: LifecycleRecoveryOptions
500
- ): RecoveryCommitVerification {
501
- try {
502
- const content = fs.readFileSync(taskMdPath, 'utf8');
503
- parseTypedTaskFrontmatter(content);
504
- const section = locateActivityLog(content);
505
- if (!section) return { ok: false, message: 'recovery Activity Log cannot be reread uniquely' };
506
- const run = readRun(taskDir, options.orchestration);
507
- const store = readLifecycleStore(options, options.repoRoot ?? path.resolve(taskDir, '../../../..'));
508
- const facts = readRecoveryTerminalFacts(section, taskId, request, run, store);
509
- if (!facts.ok) return { ok: false, message: facts.message };
510
- if (!sameRecoveryNote(facts.facts.note, note)) {
511
- return { ok: false, message: 'recovery Activity Log note changed during commit verification' };
512
- }
513
- if (!facts.facts.stored) return { ok: false, message: 'protected lifecycle claim disappeared before release' };
514
- return { ok: true, receipt: facts.facts.receipt };
515
- } catch (error) {
516
- return { ok: false, message: error instanceof Error ? error.message : String(error) };
517
- }
518
- }
519
-
520
- function recoveryDomainFailure(): Readonly<Record<string, unknown>> {
521
- return { consistent: false, recovery: true, targetState: 'active' };
522
- }
523
-
524
- export function readLifecycleRecoveryDomainEvidence(
525
- repoRoot: string,
526
- requestInput: LifecycleRecoveryRequest,
527
- terminalResult: Readonly<{ status: string; changed: boolean | null; targetState: string | null; warning?: unknown | null }>,
528
- options: Pick<LifecycleRecoveryOptions, 'lifecycleStore' | 'now'> = {}
529
- ): Readonly<Record<string, unknown>> {
530
- const terminalStateValid = terminalResult.status === 'no-op'
531
- ? terminalResult.changed === false
532
- : terminalResult.status === 'applied'
533
- && (terminalResult.changed === true || (terminalResult.changed === false && isReleaseRetryWarning(terminalResult.warning)));
534
- const normalized = normalizeRequest(requestInput);
535
- if ('code' in normalized || terminalResult.targetState !== 'active'
536
- || !terminalStateValid) return recoveryDomainFailure();
537
- const request = normalized;
538
- const resolved = resolveTaskRef(request.taskRef, { repoRoot });
539
- if (!resolved.ok) return recoveryDomainFailure();
540
- try {
541
- const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
542
- parseTypedTaskFrontmatter(content);
543
- const section = locateActivityLog(content);
544
- if (!section) return recoveryDomainFailure();
545
- const run = readRun(resolved.taskDir);
546
- const store = readLifecycleStore(options, repoRoot);
547
- const facts = readRecoveryTerminalFacts(section, resolved.taskId, request, run, store);
548
- if (!facts.ok) return recoveryDomainFailure();
549
- if (!facts.facts.stored) return { consistent: true, recovery: true, targetState: 'active', recoveryState: 'released' };
550
- if (!isReleaseRetryWarning(terminalResult.warning)) return recoveryDomainFailure();
551
- return {
552
- consistent: true,
553
- recovery: true,
554
- targetState: 'active',
555
- recoveryState: 'retry-required',
556
- warning: terminalResult.warning
557
- };
558
- } catch {
559
- return recoveryDomainFailure();
560
- }
561
- }
562
-
563
- function recoverStartedLifecycleUnderLock(
564
- requestInput: LifecycleRecoveryRequest,
565
- options: LifecycleRecoveryOptions = {}
566
- ): LifecycleRecoveryResult {
567
- const normalized = normalizeRequest(requestInput);
568
- if ('code' in normalized) return failure(requestInput, 'conflict', normalized.code, normalized.message);
569
- const request = normalized;
570
- const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
571
- if (!resolved.ok) return failure(request, 'conflict', resolved.code, resolved.message);
572
- const taskId = resolved.taskId;
573
- let content: string;
574
- try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
575
- catch (error) { return failure(request, 'owner-unknown', 'RECOVERY_TASK_READ_FAILED', String(error), { taskId }); }
576
- try { parseTypedTaskFrontmatter(content); }
577
- catch (error) { return failure(request, 'conflict', 'RECOVERY_TASK_INVALID', error instanceof Error ? error.message : String(error), { taskId }); }
578
- const section = locateActivityLog(content);
579
- if (!section) return failure(request, 'conflict', 'RECOVERY_LOG_INVALID', 'task has no unique Activity Log section', { taskId });
580
- const targets = targetRows(section.entries, request.stage, request.round);
581
- const open = targets.rows.filter((row) => row.started !== '' && row.done === '');
582
- if (open.length > 1 || targets.recoveryEntries.length > 1) {
583
- return failure(request, 'conflict', 'RECOVERY_SELECTOR_AMBIGUOUS', 'recovery selector does not identify one lifecycle attempt', { taskId });
584
- }
585
- let run: OrchestrationRun | null;
586
- try { run = readRun(resolved.taskDir, options.orchestration); }
587
- catch (error) { return failure(request, 'owner-unknown', 'RECOVERY_ORCHESTRATION_UNKNOWN', String(error), { taskId }); }
588
- if (!run) return failure(request, 'owner-unknown', 'RECOVERY_ORCHESTRATION_MISSING', 'no orchestration run exists for the open lifecycle execution', { taskId });
589
-
590
- if (open.length === 0) {
591
- if (targets.recoveryEntries.length !== 1) {
592
- return failure(request, 'conflict', 'RECOVERY_TERMINAL_MISSING', 'there is no matching open or structured recovery lifecycle row', { taskId });
593
- }
594
- const store = readLifecycleStore(options, resolved.repoRoot);
595
- const facts = readRecoveryTerminalFacts(section, taskId, request, run, store);
596
- if (!facts.ok) {
597
- return failure(request, recoveryTerminalFailureStatus(facts.code), facts.code, facts.message, { taskId });
598
- }
599
- const { note, receipt, consumer, stored } = facts.facts;
600
- if (!stored) return result(request, 'no-op', { taskId, receiptId: receipt.id, childId: receipt.childId });
601
- let released = false;
602
- try { released = (options.releaseRecovery ?? store.releaseRecovery)(note.childId, consumer); }
603
- catch (error) {
604
- return releaseResult(request, taskId, receipt, false, false);
605
- }
606
- return releaseResult(request, taskId, receipt, released, false);
607
- }
608
-
609
- const started = open[0]!;
610
- if (normalizeAgentToken(started.agent) !== request.agent || started.note !== 'started') {
611
- return failure(request, 'conflict', 'RECOVERY_SELECTOR_MISMATCH', 'open started lifecycle row does not match the recovery selector', { taskId });
612
- }
613
- if (targets.recoveryEntries.length > 0) {
614
- return failure(request, 'conflict', 'RECOVERY_LOG_CONFLICT', 'open lifecycle execution already has a recovery terminal row', { taskId });
615
- }
616
- const pending = run.pendingDelegation;
617
- const recovered = run.receipts.filter((candidate) => isPostActivationAbortedReceipt(candidate, taskId, request));
618
- if (pending && recovered.length > 0) {
619
- return failure(request, 'conflict', 'RECOVERY_RECEIPT_INVALID', 'open lifecycle execution has both a pending and an aborted matching delegation', { taskId });
620
- }
621
- const receipt = pending ?? (recovered.length === 1 ? recovered[0]! : null);
622
- if (!receipt || (pending && (receipt.status !== 'activated' || receipt.client !== 'codex')) || (!pending && receipt.status !== 'aborted')) {
623
- return failure(request, 'owner-unknown', 'RECOVERY_DELEGATION_UNAVAILABLE', 'open lifecycle execution is not backed by one matching activated Codex delegation', { taskId });
624
- }
625
- if (normalizeAgentToken(started.agent) !== normalizeAgentToken(receipt.client)) {
626
- return failure(request, 'conflict', 'RECOVERY_SELECTOR_MISMATCH', 'started agent does not match the delegation client', { taskId, receiptId: receipt.id, childId: receipt.childId });
627
- }
628
- const consumer = recoveryConsumer(taskId, receipt.id);
629
- const store = readLifecycleStore(options, resolved.repoRoot);
630
- const stored = readStoredEvidence(store, receipt.childId ?? '');
631
- if ('error' in stored) return failure(request, 'owner-unknown', 'RECOVERY_STORE_UNKNOWN', stored.error.message, { taskId, receiptId: receipt.id, childId: receipt.childId });
632
- if ('missing' in stored) return failure(request, 'owner-unknown', 'RECOVERY_STOP_EVIDENCE_MISSING', 'matching Codex lifecycle stop evidence is missing', { taskId, receiptId: receipt.id, childId: receipt.childId });
633
- const evidence = validateStopRecord(stored, receipt, consumer);
634
- if (!evidence.ok) return failure(request, evidence.code === 'RECOVERY_CONSUMER_CONFLICT' ? 'conflict' : 'owner-unknown', evidence.code, evidence.message, { taskId, receiptId: receipt.id, childId: receipt.childId });
635
- let claimed = stored;
636
- if (stored.consumer === null) {
637
- try { claimed = store.claimRecovery(receipt.childId!, taskId, receipt.id, receipt.hostEvidence?.hookDefinitionHash); }
638
- catch (error) { return failure(request, 'owner-unknown', 'RECOVERY_CLAIM_FAILED', error instanceof Error ? error.message : String(error), { taskId, receiptId: receipt.id, childId: receipt.childId }); }
639
- }
640
- const stopRevision = claimed.revision;
641
- const consumedAt = claimed.consumedAt;
642
- if (!consumedAt) return failure(request, 'owner-unknown', 'RECOVERY_CLAIM_INVALID', 'protected lifecycle claim has no consumed timestamp', { taskId, receiptId: receipt.id, childId: receipt.childId });
643
- if (receipt.status === 'activated') {
644
- const recoveredRun = recoverActivatedOrchestrationDelegationUnderLock(taskId, {
645
- receiptId: receipt.id,
646
- stage: request.stage,
647
- round: request.round,
648
- artifact: request.artifact,
649
- startedAgent: request.agent,
650
- childId: receipt.childId!,
651
- stopRevision,
652
- consumer,
653
- consumedAt
654
- }, { ...options.orchestration, repoRoot: resolved.repoRoot, now: options.now });
655
- if (recoveredRun.status === 'failed' || !recoveredRun.run) {
656
- return failure(request, 'owner-unknown', recoveredRun.error?.code ?? 'RECOVERY_ORCHESTRATION_FAILED', recoveredRun.error?.message ?? 'orchestration recovery failed', { taskId, receiptId: receipt.id, childId: receipt.childId });
657
- }
658
- }
659
- const note = taskNote(taskId, request, receipt, stopRevision, consumer, consumedAt);
660
- const metadata = captureTaskWriteMetadata();
661
- const updatedBody = `${section.body ? `${section.body}\n` : ''}- ${metadata.timestamp} — **${baseStep(started.step)} [aborted]** by ${request.agent} — ${renderRecoveryNote(note)}`;
662
- const written = (options.writeTask ?? writeTask)({
663
- taskRef: taskId,
664
- expectedState: 'active',
665
- mutations: [{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body: updatedBody }]
666
- }, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
667
- if (written.status === 'failed') {
668
- return failure(request, 'owner-unknown', 'RECOVERY_LOG_WRITE_FAILED', written.error.message, { taskId, receiptId: receipt.id, childId: receipt.childId });
669
- }
670
- const verified = (options.verifyRecoveryCommit ?? verifyRecoveryCommit)(resolved.taskMdPath, resolved.taskDir, taskId, request, note, options);
671
- if (!verified.ok) {
672
- return failure(request, 'owner-unknown', 'RECOVERY_COMMIT_VERIFY_FAILED', verified.message, { taskId, receiptId: receipt.id, childId: receipt.childId });
673
- }
674
- let released = false;
675
- try { released = (options.releaseRecovery ?? store.releaseRecovery)(receipt.childId!, consumer); }
676
- catch { released = false; }
677
- if (!released) {
678
- return releaseResult(request, taskId, receipt, false, true);
679
- }
680
- return result(request, 'applied', { taskId, receiptId: receipt.id, childId: receipt.childId });
681
- }
682
-
683
- export {
684
- RECOVERY_NOTE_PREFIX,
685
- parseRecoveryNote,
686
- recoveryFailure,
687
- recoveryConsumer,
688
- recoverStartedLifecycleUnderLock,
689
- renderRecoveryNote
690
- };
691
- export type { LifecycleRecoveryOptions, LifecycleRecoveryRequest, LifecycleRecoveryResult, RecoveryNote, RecoveryStage };