@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
@@ -0,0 +1,297 @@
1
+ import fs from 'node:fs';
2
+
3
+ import { createCodexLifecycleStore } from './store.ts';
4
+ import type { StoredCodexLifecycle } from './store.ts';
5
+ import { managedDelegationRole } from '../../../task/delegation-receipts.ts';
6
+ import type { DelegationReceipt } from '../../../task/delegation-receipts.ts';
7
+ import { normalizeAgentToken } from '../../tokens.ts';
8
+ import { locateActivityLog, pairEntries, appendActivityEntry } from '../../../task/activity-log.ts';
9
+ import type { ActivityLogSection, StepRow } from '../../../task/activity-log.ts';
10
+ import { captureTaskWriteMetadata, writeTask } from '../../../task/write.ts';
11
+ import { parseTypedTaskFrontmatter } from '../../../task/frontmatter.ts';
12
+ import { resolveTaskRef } from '../../../task/resolve-ref.ts';
13
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
14
+ import {
15
+ recoverActivatedOrchestrationDelegationUnderLock,
16
+ readRun
17
+ } from '../../../task/orchestration.ts';
18
+ import type { OrchestrationOptions, OrchestrationRun } from '../../../task/orchestration.ts';
19
+ import type {
20
+ AgentClientLifecycleRecoveryRequest,
21
+ AgentClientLifecycleRecoveryResult
22
+ } from '../../adapter.ts';
23
+
24
+ const ACTIONS = {
25
+ analysis: 'Analyze Task',
26
+ 'review-analysis': 'Review Analysis',
27
+ plan: 'Plan Task',
28
+ 'review-plan': 'Review Plan',
29
+ code: 'Code Task',
30
+ 'review-code': 'Review Code'
31
+ } as const;
32
+
33
+ type RecoveryStage = keyof typeof ACTIONS;
34
+ type LifecycleRecoveryRequest = AgentClientLifecycleRecoveryRequest;
35
+ type LifecycleRecoveryResult = AgentClientLifecycleRecoveryResult;
36
+ type LifecycleRecoveryOptions = Readonly<{
37
+ repoRoot?: string;
38
+ now?: () => string;
39
+ orchestration?: Pick<OrchestrationOptions, 'diagnosticLog' | 'now'>;
40
+ lifecycleStore?: ReturnType<typeof createCodexLifecycleStore>;
41
+ writeTask?: typeof writeTask;
42
+ }>;
43
+
44
+ type RecoveryCandidate = Readonly<{
45
+ receipt: DelegationReceipt;
46
+ row: StepRow;
47
+ }>;
48
+
49
+ function result(
50
+ request: LifecycleRecoveryRequest,
51
+ status: LifecycleRecoveryResult['status'],
52
+ extra: Partial<LifecycleRecoveryResult> = {}
53
+ ): LifecycleRecoveryResult {
54
+ return {
55
+ status,
56
+ changed: status === 'applied',
57
+ targetState: 'active',
58
+ requestRef: request.taskRef,
59
+ intent: 'recover-started',
60
+ taskId: null,
61
+ receiptId: null,
62
+ childId: null,
63
+ error: null,
64
+ ...extra
65
+ };
66
+ }
67
+
68
+ function failure(
69
+ request: LifecycleRecoveryRequest,
70
+ status: 'owner-unknown' | 'conflict',
71
+ code: string,
72
+ message: string,
73
+ extra: Partial<LifecycleRecoveryResult> = {}
74
+ ): LifecycleRecoveryResult {
75
+ return result(request, status, { error: { code, message }, ...extra });
76
+ }
77
+
78
+ function actionPattern(receipt: Pick<DelegationReceipt, 'stage' | 'round'>): RegExp | null {
79
+ const action = ACTIONS[receipt.stage as RecoveryStage];
80
+ if (!action) return null;
81
+ const escaped = action.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
82
+ return new RegExp(`^${escaped} \\(Round ${receipt.round}(?:, (?:fix for review-code(?:-r[2-9]|-r[1-9]\\d+)?.md|decision II-[1-9]\\d*))?\\)$`, 'u');
83
+ }
84
+
85
+ function openRows(section: ActivityLogSection): StepRow[] {
86
+ return pairEntries([...section.entries]).filter((row) => row.started !== '' && row.done === '');
87
+ }
88
+
89
+ function rowForReceipt(rows: readonly StepRow[], receipt: DelegationReceipt): StepRow[] {
90
+ const pattern = actionPattern(receipt);
91
+ return pattern ? rows.filter((row) => pattern.test(row.step)) : [];
92
+ }
93
+
94
+ function readLifecycleStore(
95
+ options: LifecycleRecoveryOptions,
96
+ repoRoot: string
97
+ ): ReturnType<typeof createCodexLifecycleStore> {
98
+ return options.lifecycleStore ?? createCodexLifecycleStore({
99
+ root: resolveAgentRuntimeStoreRoot({ repoRoot, store: 'lifecycle' }),
100
+ cliVersion: 'recovery',
101
+ now: options.now
102
+ });
103
+ }
104
+
105
+ function readStoredEvidence(
106
+ store: ReturnType<typeof createCodexLifecycleStore>,
107
+ childId: string
108
+ ): StoredCodexLifecycle | { missing: true } | { error: Error } {
109
+ try {
110
+ return store.read(childId);
111
+ } catch (error) {
112
+ const message = error instanceof Error ? error.message : String(error);
113
+ if (message.includes('was not found uniquely')) return { missing: true };
114
+ return { error: error instanceof Error ? error : new Error(message) };
115
+ }
116
+ }
117
+
118
+ function validateStopRecord(
119
+ record: StoredCodexLifecycle,
120
+ receipt: DelegationReceipt
121
+ ): { ok: true } | { ok: false; code: string; message: string } {
122
+ const start = record.state.startEvidence;
123
+ const stop = record.state.stopEvidence;
124
+ const provenance = receipt.lifecycleProvenance;
125
+ const host = receipt.hostEvidence;
126
+ if (
127
+ receipt.client !== 'codex'
128
+ || !provenance
129
+ || host?.kind !== 'codex-lifecycle-v2'
130
+ || !start
131
+ || !stop
132
+ || record.state.status !== 'stop-ready'
133
+ || stop.terminalStatus !== 'completed'
134
+ || stop.hookStopObserved !== true
135
+ || start.childThreadId !== receipt.childId
136
+ || start.parentThreadId !== receipt.parentId
137
+ || managedDelegationRole(start.nativeAgent) !== receipt.role
138
+ || start.hookDefinitionHash !== provenance.hookDefinitionHash
139
+ || host.hookDefinitionHash !== provenance.hookDefinitionHash
140
+ || host.capabilitySessionId !== provenance.capabilitySessionId
141
+ || host.capabilityTurnId !== provenance.capabilityTurnId
142
+ || host.capabilityToolUseId !== provenance.capabilityToolUseId
143
+ || host.spawnToolUseId !== start.spawnToolUseId
144
+ || !host.spawnObservedAt
145
+ || host.spawnObservedAt !== record.spawnObservedAt
146
+ || host.controllerInstanceDigest !== provenance.controllerInstanceDigest
147
+ || host.controlGeneration !== provenance.controlGeneration
148
+ || typeof host.controllerInstanceDigest !== 'string'
149
+ || typeof host.controlGeneration !== 'string'
150
+ || host.startRevision < 1
151
+ || record.revision <= host.startRevision
152
+ || record.state.child?.childThreadId !== receipt.childId
153
+ || record.state.stop?.childThreadId !== receipt.childId
154
+ ) {
155
+ return { ok: false, code: 'RECOVERY_STOP_EVIDENCE_INVALID', message: 'Codex stop evidence does not match the activated receipt' };
156
+ }
157
+ if (record.consumer !== null && record.consumer !== receipt.id) {
158
+ return { ok: false, code: 'RECOVERY_CONSUMER_CONFLICT', message: `Codex lifecycle evidence was consumed by '${record.consumer}'` };
159
+ }
160
+ return { ok: true };
161
+ }
162
+
163
+ function abortedCandidates(run: OrchestrationRun, rows: readonly StepRow[]): RecoveryCandidate[] {
164
+ const candidates = run.receipts.flatMap((receipt) => {
165
+ if (receipt.status !== 'aborted' || receipt.activatedAt === null || receipt.client !== 'codex') return [];
166
+ const matches = rowForReceipt(rows, receipt);
167
+ return matches.length === 1 ? [{ receipt, row: matches[0]! }] : [];
168
+ });
169
+ return candidates;
170
+ }
171
+
172
+ function appendRecoveryLog(
173
+ request: LifecycleRecoveryRequest,
174
+ taskId: string,
175
+ section: ActivityLogSection,
176
+ candidate: RecoveryCandidate,
177
+ repoRoot: string,
178
+ writer: typeof writeTask
179
+ ): LifecycleRecoveryResult {
180
+ const metadata = captureTaskWriteMetadata();
181
+ const body = appendActivityEntry(section, {
182
+ time: metadata.timestamp,
183
+ step: `${candidate.row.step} [aborted]`,
184
+ agent: request.agent,
185
+ note: `terminated Codex delegation; receipt=${candidate.receipt.id}; child=${candidate.receipt.childId}`
186
+ });
187
+ const written = writer({
188
+ taskRef: taskId,
189
+ expectedState: 'active',
190
+ mutations: [{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body }]
191
+ }, { repoRoot, metadataProvider: () => metadata });
192
+ if (written.status === 'failed') {
193
+ return failure(request, 'owner-unknown', 'RECOVERY_LOG_WRITE_FAILED', written.error.message, {
194
+ taskId, receiptId: candidate.receipt.id, childId: candidate.receipt.childId
195
+ });
196
+ }
197
+ return result(request, 'applied', {
198
+ taskId, receiptId: candidate.receipt.id, childId: candidate.receipt.childId
199
+ });
200
+ }
201
+
202
+ function recoverStartedLifecycleUnderLock(
203
+ request: LifecycleRecoveryRequest,
204
+ options: LifecycleRecoveryOptions = {}
205
+ ): LifecycleRecoveryResult {
206
+ if (!request || request.intent !== 'recover-started' || request.auto !== true || normalizeAgentToken(request.agent) !== 'codex') {
207
+ return failure(request, 'conflict', 'RECOVERY_PAYLOAD_INVALID', 'automatic recovery requires a task and the Codex agent');
208
+ }
209
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
210
+ if (!resolved.ok) return failure(request, 'conflict', resolved.code, resolved.message);
211
+ const taskId = resolved.taskId;
212
+ let content: string;
213
+ try {
214
+ content = fs.readFileSync(resolved.taskMdPath, 'utf8');
215
+ parseTypedTaskFrontmatter(content);
216
+ } catch (error) {
217
+ return failure(request, 'conflict', 'RECOVERY_TASK_INVALID', error instanceof Error ? error.message : String(error), { taskId });
218
+ }
219
+ const section = locateActivityLog(content);
220
+ if (!section) return failure(request, 'conflict', 'RECOVERY_LOG_INVALID', 'task has no unique Activity Log section', { taskId });
221
+ let run: OrchestrationRun | null;
222
+ try {
223
+ run = readRun(resolved.taskDir, options.orchestration);
224
+ } catch (error) {
225
+ return failure(request, 'owner-unknown', 'RECOVERY_ORCHESTRATION_UNKNOWN', error instanceof Error ? error.message : String(error), { taskId });
226
+ }
227
+ if (!run) return result(request, 'no-op', { taskId });
228
+
229
+ const rows = openRows(section);
230
+ const pending = run.pendingDelegation;
231
+ if (!pending) {
232
+ const candidates = abortedCandidates(run, rows);
233
+ if (candidates.length === 0) return result(request, 'no-op', { taskId });
234
+ if (candidates.length > 1) {
235
+ return failure(request, 'conflict', 'RECOVERY_CANDIDATE_AMBIGUOUS', 'more than one aborted delegation has an open lifecycle row', { taskId });
236
+ }
237
+ return appendRecoveryLog(request, taskId, section, candidates[0]!, resolved.repoRoot, options.writeTask ?? writeTask);
238
+ }
239
+ if (pending.status !== 'activated') return result(request, 'no-op', { taskId });
240
+ if (pending.client !== 'codex' || run.status !== 'running' || run.pause !== null) {
241
+ return failure(request, 'conflict', 'RECOVERY_ORCHESTRATION_INVALID', 'activated recovery requires one running Codex delegation', {
242
+ taskId, receiptId: pending.id, childId: pending.childId
243
+ });
244
+ }
245
+ const matchingRows = rowForReceipt(rows, pending);
246
+ if (matchingRows.length !== 1 || normalizeAgentToken(matchingRows[0]!.agent) !== 'codex' || matchingRows[0]!.note !== 'started') {
247
+ return failure(request, 'conflict', 'RECOVERY_LOG_CONFLICT', 'activated delegation does not have one matching open lifecycle row', {
248
+ taskId, receiptId: pending.id, childId: pending.childId
249
+ });
250
+ }
251
+ if (!pending.childId) {
252
+ return failure(request, 'conflict', 'RECOVERY_DELEGATION_INVALID', 'activated delegation has no child identity', { taskId, receiptId: pending.id });
253
+ }
254
+ const store = readLifecycleStore(options, resolved.repoRoot);
255
+ const stored = readStoredEvidence(store, pending.childId);
256
+ if ('error' in stored) return failure(request, 'owner-unknown', 'RECOVERY_STORE_UNKNOWN', stored.error.message, { taskId, receiptId: pending.id, childId: pending.childId });
257
+ if ('missing' in stored) return failure(request, 'owner-unknown', 'RECOVERY_STOP_EVIDENCE_MISSING', 'matching Codex lifecycle stop evidence is missing', { taskId, receiptId: pending.id, childId: pending.childId });
258
+ const evidence = validateStopRecord(stored, pending);
259
+ if (!evidence.ok) return failure(request, evidence.code === 'RECOVERY_CONSUMER_CONFLICT' ? 'conflict' : 'owner-unknown', evidence.code, evidence.message, { taskId, receiptId: pending.id, childId: pending.childId });
260
+
261
+ let consumed = stored;
262
+ if (stored.consumer === null) {
263
+ try {
264
+ consumed = store.consume(pending.childId, pending.id, pending.hostEvidence?.hookDefinitionHash);
265
+ } catch (error) {
266
+ return failure(request, 'owner-unknown', 'RECOVERY_EVIDENCE_CONSUME_FAILED', error instanceof Error ? error.message : String(error), { taskId, receiptId: pending.id, childId: pending.childId });
267
+ }
268
+ }
269
+ if (!consumed.consumedAt) {
270
+ return failure(request, 'owner-unknown', 'RECOVERY_EVIDENCE_INVALID', 'consumed stop evidence has no timestamp', { taskId, receiptId: pending.id, childId: pending.childId });
271
+ }
272
+ const recovered = recoverActivatedOrchestrationDelegationUnderLock(taskId, {
273
+ receiptId: pending.id,
274
+ stage: pending.stage,
275
+ round: pending.round,
276
+ artifact: pending.artifact,
277
+ startedAgent: request.agent,
278
+ childId: pending.childId,
279
+ stopRevision: consumed.revision,
280
+ consumer: pending.id,
281
+ consumedAt: consumed.consumedAt
282
+ }, { ...options.orchestration, repoRoot: resolved.repoRoot, now: options.now });
283
+ if (recovered.status === 'failed' || !recovered.run) {
284
+ return failure(request, 'owner-unknown', recovered.error?.code ?? 'RECOVERY_ORCHESTRATION_FAILED', recovered.error?.message ?? 'orchestration recovery failed', { taskId, receiptId: pending.id, childId: pending.childId });
285
+ }
286
+ return appendRecoveryLog(request, taskId, section, { receipt: recovered.run.receipts.at(-1)!, row: matchingRows[0]! }, resolved.repoRoot, options.writeTask ?? writeTask);
287
+ }
288
+
289
+ function recoverStartedLifecycleFromAdapter(
290
+ request: LifecycleRecoveryRequest,
291
+ options: LifecycleRecoveryOptions = {}
292
+ ): LifecycleRecoveryResult {
293
+ return recoverStartedLifecycleUnderLock(request, options);
294
+ }
295
+
296
+ export { recoverStartedLifecycleFromAdapter, recoverStartedLifecycleUnderLock };
297
+ export type { LifecycleRecoveryOptions, LifecycleRecoveryRequest, LifecycleRecoveryResult };
@@ -9,10 +9,8 @@ import * as toml from 'smol-toml';
9
9
  import {
10
10
  requestCodexControllerClose,
11
11
  requestCodexControllerOpen,
12
- requestCodexControllerVerify,
13
- requestSandboxControl
12
+ requestCodexControllerVerify
14
13
  } from '../../../sandbox/control/client.ts';
15
- import { readSandboxControlStatus } from '../../../sandbox/control/state.ts';
16
14
  import { getProcessStartTime, type ProcessIdentity } from '../../../server/process-state.ts';
17
15
  import { LIFECYCLE_PROTOCOL_VERSION, type LifecycleIdentityWarning } from './build-identity.ts';
18
16
  import {
@@ -46,7 +44,7 @@ type ControllerOptions = Readonly<{
46
44
  control?: ControllerControl;
47
45
  now?: () => number;
48
46
  codexVersion?: () => string;
49
- verifyTaskBinding?: (taskId: string, control: ControllerControl) => void;
47
+ verifyController?: typeof requestCodexControllerVerify;
50
48
  openController?: typeof requestCodexControllerOpen;
51
49
  closeController?: typeof requestCodexControllerClose;
52
50
  environment?: NodeJS.ProcessEnv;
@@ -117,40 +115,22 @@ function verifyRuntime(runtimeDir: string): void {
117
115
  }
118
116
  }
119
117
 
120
- function verifyControl(taskId: string, control: ControllerControl): void {
118
+ function verifyControllerBinding(
119
+ context: CodexSandboxControllerContextV2,
120
+ control: ControllerControl,
121
+ requestControllerVerify: typeof requestCodexControllerVerify
122
+ ): void {
121
123
  verifyRuntime(control.runtimeDir);
122
- let status: ReturnType<typeof readSandboxControlStatus> | null = null;
123
- let lastError: unknown = null;
124
- for (let attempt = 0; attempt < 5; attempt += 1) {
125
- try {
126
- status = readSandboxControlStatus(control.statusDir);
127
- break;
128
- } catch (error) {
129
- lastError = error;
130
- if (attempt < 4) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
131
- }
132
- }
133
- if (!status) {
134
- throw new Error(`CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE: ${String(lastError)}`);
135
- }
136
- if (status.generation !== control.generation
137
- || status.state !== 'healthy'
138
- || Date.now() - status.updatedAt > 1_500) {
139
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE');
140
- }
141
- const response = requestSandboxControl({
142
- family: 'task-orchestration',
143
- args: [taskId, 'status'],
124
+ const verified = requestControllerVerify({
125
+ controllerProof: controllerProofFromContext(context),
144
126
  ...control,
145
127
  timeoutMs: 30_000
146
128
  });
147
- let payload: { taskId?: unknown } = {};
148
- try {
149
- payload = JSON.parse(response.stdout) as { taskId?: unknown };
150
- } catch {
129
+ if (verified.binding.taskId !== context.taskId
130
+ || verified.binding.controlGeneration !== context.controlGeneration
131
+ || verified.binding.controllerInstanceDigest !== context.controllerInstanceDigest) {
151
132
  throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
152
133
  }
153
- if (payload.taskId !== taskId) throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
154
134
  }
155
135
 
156
136
  function verifyCodexSandboxControllerContextWithWarnings(
@@ -349,7 +329,7 @@ function prepareCodexSandboxController(
349
329
  || opened.lease.controllerProcess.startTime !== parentStartTime) {
350
330
  throw new Error('SANDBOX_CONTROL_RESULT_INVALID');
351
331
  }
352
- (options.verifyTaskBinding ?? verifyControl)(taskId, control);
332
+ verifyControllerBinding(context, control, options.verifyController ?? requestCodexControllerVerify);
353
333
  const contextPath = path.join(home, 'controller-context.json');
354
334
  writeCodexSandboxControllerContext(contextPath, context);
355
335
 
@@ -43,23 +43,10 @@ type ActiveCodexLifecycleEvidenceQuery = Readonly<{
43
43
  const LOCK_RETRY_MS = 10;
44
44
  const LOCK_TIMEOUT_MS = 1_000;
45
45
  const LOCK_STALE_MS = 30_000;
46
- const RECOVERY_CONSUMER_RE = /^lifecycle-recovery:([^:\r\n]+):([^:\r\n]+)$/u;
47
-
48
46
  function digest(value: string): string {
49
47
  return crypto.createHash('sha256').update(value).digest('hex');
50
48
  }
51
49
 
52
- function isRecoveryConsumer(value: string): boolean {
53
- return RECOVERY_CONSUMER_RE.test(value);
54
- }
55
-
56
- function recoveryConsumer(taskId: string, receiptId: string): string {
57
- if (!/^[^:\r\n]+$/u.test(taskId) || !/^[^:\r\n]+$/u.test(receiptId)) {
58
- throw new Error('Codex lifecycle recovery claim identity is invalid');
59
- }
60
- return `lifecycle-recovery:${taskId}:${receiptId}`;
61
- }
62
-
63
50
  function readRecord(file: string): StoredCodexLifecycle {
64
51
  const value = JSON.parse(fs.readFileSync(file, 'utf8')) as StoredCodexLifecycle;
65
52
  if (
@@ -310,45 +297,15 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
310
297
  expectedHookDefinitionHash?: string
311
298
  ): StoredCodexLifecycle {
312
299
  if (!consumer.trim()) throw new Error('Codex lifecycle consumer is required');
313
- if (consumer.startsWith('lifecycle-recovery:')) {
314
- throw new Error('Codex lifecycle recovery claims require claimRecovery');
315
- }
316
300
  return consumeInternal(childThreadId, consumer, expectedHookDefinitionHash);
317
301
  }
318
302
 
319
- function claimRecovery(
320
- childThreadId: string,
321
- taskId: string,
322
- receiptId: string,
323
- expectedHookDefinitionHash?: string
324
- ): StoredCodexLifecycle {
325
- return consumeInternal(
326
- childThreadId,
327
- recoveryConsumer(taskId, receiptId),
328
- expectedHookDefinitionHash
329
- );
330
- }
331
-
332
- function releaseRecovery(childThreadId: string, consumer: string): boolean {
333
- if (!/^lifecycle-recovery:[^:\r\n]+:[^:\r\n]+$/.test(consumer)) return false;
334
- return withWriteLock(() => {
335
- const matches = findByChild(childThreadId);
336
- if (matches.length !== 1) return false;
337
- const file = matches[0]!;
338
- const current = readRecord(file);
339
- if (current.state.status !== 'stop-ready' || current.consumer !== consumer || !current.consumedAt) return false;
340
- fs.unlinkSync(file);
341
- return true;
342
- });
343
- }
344
-
345
303
  function expireBefore(cutoff: string): number {
346
304
  return withWriteLock(() => {
347
305
  let changed = 0;
348
306
  for (const file of recordFiles(root)) {
349
307
  const current = readRecord(file);
350
308
  if (current.updatedAt >= cutoff) continue;
351
- if (current.consumer && isRecoveryConsumer(current.consumer)) continue;
352
309
  if (current.consumer || ['invalid', 'expired', 'stop-ready'].includes(current.state.status)) {
353
310
  fs.unlinkSync(file);
354
311
  changed += 1;
@@ -368,7 +325,7 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
368
325
  });
369
326
  }
370
327
 
371
- return Object.freeze({ root, apply, applyToSpawn, consume, claimRecovery, releaseRecovery, expireBefore, findByParent, read });
328
+ return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
372
329
  }
373
330
 
374
331
  export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
@@ -28,6 +28,16 @@ const codexAdapter = defineAgentClientAdapter({
28
28
  actualModel: 'app-server',
29
29
  actualReasoningEffort: 'app-server'
30
30
  },
31
+ orchestrationAdapter: {
32
+ prepareDelegation: async (...args) => {
33
+ const { prepareCodexOrchestrationDelegation } = await import('../../task/codex-orchestration.ts');
34
+ return prepareCodexOrchestrationDelegation(...args);
35
+ },
36
+ recoverStarted: async (...args) => {
37
+ const { recoverStartedLifecycleFromAdapter } = await import('./codex-lifecycle/recovery.ts');
38
+ return recoverStartedLifecycleFromAdapter(...args);
39
+ }
40
+ },
31
41
  project: {
32
42
  ownedPathPrefixes: ['.codex/'],
33
43
  managed: ['.codex/hooks.json', '.codex/agents/'],
@@ -6,7 +6,7 @@ import { resolveTaskRef } from '../task/resolve-ref.ts';
6
6
  import { applyTaskEvent } from '../task/events.ts';
7
7
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
8
8
  import { normalizeAgentToken } from '../agent-clients/tokens.ts';
9
- import { createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, readManualValidationReceipt, writeManualValidationReceiptAtomic } from '../task/manual-validation-receipt.ts';
9
+ import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, readManualValidationReceipt, writeManualValidationReceiptAtomic } from '../task/manual-validation-receipt.ts';
10
10
  import { archiveManualValidationGeneration, createManualValidationTransaction, readManualValidationTransaction, retryManualValidationTransaction, summaryPreimageDigest, transitionManualValidationTransaction, validateManualValidationGenerationArchive, writeManualValidationTransactionAtomic } from '../task/manual-validation-transaction.ts';
11
11
  import type { ManualValidationTransaction } from '../task/manual-validation-transaction.ts';
12
12
  import type { ManualValidationReceipt } from '../task/manual-validation-receipt.ts';
@@ -43,28 +43,23 @@ function printFailure(format: OutputFormat, error: { code: string; message: stri
43
43
  process.exitCode = 1;
44
44
  }
45
45
 
46
- const MANUAL_VALIDATION_STATUS_SECTION = /^###\s+(?:⚠️\s+(?:需人工校验|Manual Validation Required)|✅\s+(?:人工验证已通过|无需人工校验|Manual Validation Passed|No Manual Validation Required)|⏳\s+(?:人工验证待收尾|Manual Validation Pending))\s*$[\s\S]*?(?=^#{1,3}\s|(?![\s\S]))/gmu;
47
- const TRAILING_CANONICAL_REPORT_PLACEHOLDER = /\n*<!--\s*canonical-pr-change-report\s*-->\s*$/u;
46
+ const MANUAL_VALIDATION_STATUS_HEADING = /^###\s+(?:⚠️\s+(?:需人工校验|Manual Validation Required)|✅\s+(?:人工验证已通过|无需人工校验|Manual Validation Passed|No Manual Validation Required)|⏳\s+(?:人工验证待收尾|Manual Validation Pending))\s*$/mu;
48
47
 
49
- function manualSummaryBody(body: string, phase: 'pending' | 'final', transactionId: string, receiptDigest = '', evidenceDigest = '', prHeadSha = ''): string {
48
+ function manualSummaryBody(body: string, phase: 'pending' | 'final'): string {
50
49
  const chinese = /###\s+(?:⚠️\s+需人工校验|✅\s+(?:人工验证已通过|无需人工校验)|⏳\s+人工验证待收尾)\s*$/mu.test(body);
51
- const section = phase === 'pending'
52
- ? chinese
53
- ? `### ⏳ 人工验证待收尾\n\n人工验证证据已暂存;事务 ${transactionId} 正在等待最终完成。`
54
- : `### ⏳ Manual Validation Pending\n\nManual validation evidence is staged; transaction ${transactionId} is awaiting final promotion.`
55
- : chinese
56
- ? `### ✅ 人工验证已通过\n\n人工验证已通过;transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`
57
- : `### ✅ Manual Validation Passed\n\nManual validation passed; transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`;
58
- let inserted = false;
59
- const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, (matched) => {
60
- if (inserted) return '';
61
- inserted = true;
62
- const trailingPlaceholder = TRAILING_CANONICAL_REPORT_PLACEHOLDER.exec(matched)?.[0];
63
- return trailingPlaceholder
64
- ? `${trailingPlaceholder.trim()}\n\n${section}\n\n`
65
- : `${section}\n\n`;
66
- });
67
- return inserted ? `${updated.replace(/\s+$/u, '')}\n` : `${body.replace(/\s+$/u, '')}\n\n${section}\n`;
50
+ const heading = phase === 'pending'
51
+ ? chinese ? '### ⏳ 人工验证待收尾' : '### ⏳ Manual Validation Pending'
52
+ : chinese ? '### ✅ 人工验证已通过' : '### ✅ Manual Validation Passed';
53
+ const marker = phase === 'final'
54
+ ? `\n\n${MANUAL_VALIDATION_RECEIPT_PLACEHOLDER}`
55
+ : '';
56
+ if (MANUAL_VALIDATION_STATUS_HEADING.test(body)) {
57
+ return `${body.replace(MANUAL_VALIDATION_STATUS_HEADING, `${heading}${marker}`).replace(/\s+$/u, '')}\n`;
58
+ }
59
+ const fallback = phase === 'pending'
60
+ ? chinese ? '人工验证正在等待最终完成。' : 'Manual validation is awaiting final promotion.'
61
+ : chinese ? '人工验证已通过。' : 'Manual validation passed.';
62
+ return `${body.replace(/\s+$/u, '')}\n\n${heading}${marker}\n\n${fallback}\n`;
68
63
  }
69
64
 
70
65
  function openManualValidationStarted(taskMdPath: string): { present: boolean; transactionId: string | null } {
@@ -126,7 +121,9 @@ async function executeManualValidationTransactionLocked(
126
121
  const pullRequest = currentState.pullRequest;
127
122
  const preimageBody = currentState.comment?.body ?? '';
128
123
  const preimage = { commentId: currentState.comment?.id ?? null, body: preimageBody, digest: summaryPreimageDigest(preimageBody) };
129
- const evidenceDigest = sha256File(path.resolve(cwd, values.summaryFile!));
124
+ const summaryPath = path.resolve(cwd, values.summaryFile!);
125
+ const finalBodyWithoutReceipt = fs.readFileSync(summaryPath, 'utf8');
126
+ const evidenceDigest = sha256File(summaryPath);
130
127
  const archiveGeneration = options.archiveGeneration ?? archiveManualValidationGeneration;
131
128
  let transactionResult = readManualValidationTransaction(resolved.taskDir, { taskId: resolved.taskId, prNumber: pullRequest.number, prHeadSha: pullRequest.head.sha, evidenceDigest, artifact: values.artifact! });
132
129
  let previousTransaction: ManualValidationTransaction | null = null;
@@ -151,8 +148,7 @@ async function executeManualValidationTransactionLocked(
151
148
  const transactionId = transactionResult.ok
152
149
  ? transactionResult.value.transactionId
153
150
  : (openStarted.transactionId ?? values.transactionId ?? `mv-${Date.now()}`);
154
- const finalBodyWithoutReceipt = fs.readFileSync(path.resolve(cwd, values.summaryFile!), 'utf8');
155
- const pendingBody = manualSummaryBody(finalBodyWithoutReceipt, 'pending', transactionId);
151
+ const pendingBody = manualSummaryBody(finalBodyWithoutReceipt, 'pending');
156
152
  const createTransaction = (): ManualValidationTransaction => createManualValidationTransaction({
157
153
  transactionId,
158
154
  taskId: resolved.taskId,
@@ -161,7 +157,7 @@ async function executeManualValidationTransactionLocked(
161
157
  evidenceDigest,
162
158
  summaryPreimage: preimage,
163
159
  pendingSummaryDigest: summaryPreimageDigest(pendingBody),
164
- finalSummaryDigest: manualValidationFinalSummaryDigest(manualSummaryBody(finalBodyWithoutReceipt, 'final', transactionId, '<receipt>', evidenceDigest, pullRequest.head.sha)),
160
+ finalSummaryDigest: manualValidationFinalSummaryDigest(manualSummaryBody(finalBodyWithoutReceipt, 'final')),
165
161
  artifact: values.artifact!,
166
162
  attempt: 1,
167
163
  createdAt: new Date().toISOString(),
@@ -254,7 +250,7 @@ async function executeManualValidationTransactionLocked(
254
250
  transitionAndPersist('final-promotion-in-progress');
255
251
  }
256
252
  const promoted = await syncPullRequestSummary(taskRef, {
257
- cwd, client: options.client, agent, body: manualSummaryBody(finalBodyWithoutReceipt, 'final', transaction.transactionId, receipt.receiptDigest, receipt.evidenceDigest, receipt.prHeadSha), changeReportFile: path.resolve(cwd, values.changeReportFile!), primaryResult, strict: true,
253
+ cwd, client: options.client, agent, body: manualSummaryBody(finalBodyWithoutReceipt, 'final'), changeReportFile: path.resolve(cwd, values.changeReportFile!), primaryResult, strict: true,
258
254
  manualValidation: { phase: 'final', transactionId: transaction.transactionId, receiptDigest: receipt.receiptDigest, prHeadSha: receipt.prHeadSha, authority: 'coordinator' }, lockAlreadyHeld: true
259
255
  });
260
256
  if (!['applied', 'no-op'].includes(promoted.status)) transactionFailure(promoted.error ?? { code: 'MANUAL_VALIDATION_TRANSACTION_RECOVERY_REQUIRED', message: 'final summary promotion failed' });
@@ -2,8 +2,6 @@ import {
2
2
  recoverSandboxControl,
3
3
  requestSandboxControl,
4
4
  requestSandboxTaskFinalization,
5
- requestSandboxTaskWorkflow,
6
- resolveVisibleActiveShortId,
7
5
  SandboxControlClientError
8
6
  } from '../sandbox/control/client.ts';
9
7
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
@@ -11,7 +9,6 @@ import { serveSandboxControl } from '../sandbox/control/server.ts';
11
9
  import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
12
10
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
13
11
  import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.ts';
14
- import { createTaskWorkflowRequest } from '../sandbox/control/task-workflow.ts';
15
12
 
16
13
  type FinalizationStatus = 'completed' | 'failed' | 'blocked' | 'unknown';
17
14
 
@@ -139,45 +136,6 @@ async function sandboxControl(args: string[]): Promise<void> {
139
136
  sandboxFinalizationClient(commandArgs);
140
137
  return;
141
138
  }
142
- if (family === 'task-workflow') {
143
- const command = commandArgs[0] as Parameters<typeof createTaskWorkflowRequest>[0] | undefined;
144
- const taskId = process.env.AGENT_INFRA_TASK_ID;
145
- const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
146
- if (!command || !taskId || !generation) {
147
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'SANDBOX_CONTROL_REQUEST_INVALID', message: 'task-workflow requires a bound task and generation' } })}\n`);
148
- process.exitCode = 1;
149
- return;
150
- }
151
- let workflow;
152
- try {
153
- const args = commandArgs.slice(1);
154
- if (/^\d+$/.test(args[0] ?? '')) {
155
- const resolved = resolveVisibleActiveShortId(args[0]!);
156
- if (resolved !== taskId) throw new Error('SANDBOX_TASK_REF_MISMATCH');
157
- args[0] = resolved;
158
- }
159
- workflow = createTaskWorkflowRequest(command, args, taskId, generation);
160
- }
161
- catch (error) {
162
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'SANDBOX_CONTROL_REQUEST_INVALID', message: error instanceof Error ? error.message : String(error) } })}\n`);
163
- process.exitCode = 1;
164
- return;
165
- }
166
- let response;
167
- try {
168
- response = requestSandboxTaskWorkflow({ workflow });
169
- }
170
- catch (error) {
171
- if (!(error instanceof SandboxControlClientError)) throw error;
172
- writeClientError(error);
173
- process.exitCode = error.detail.retryable ? 75 : 1;
174
- return;
175
- }
176
- process.stdout.write(response.stdout);
177
- process.stderr.write(response.stderr);
178
- process.exitCode = response.phase === 'rejected' ? response.error?.retryable ? 75 : 1 : response.exitCode ?? 1;
179
- return;
180
- }
181
139
  let response;
182
140
  try {
183
141
  response = requestSandboxControl({ family, args: commandArgs });
@@ -7,10 +7,10 @@ import {
7
7
  import { lifecycleIntentCatalog } from '../task/lifecycle.ts';
8
8
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
9
9
  import type { TaskLifecycleResult } from '../task/lifecycle.ts';
10
- import type { LifecycleRecoveryResult } from '../task/lifecycle-recovery.ts';
10
+ import type { AgentClientLifecycleRecoveryResult as LifecycleRecoveryResult } from '../agent-clients/adapter.ts';
11
11
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
12
12
 
13
- const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --stage <stage> --round <round> --artifact <artifact> --reason <reason>\nOverride: --override-ticket <ticket> --override-target <target> --override-scope <scope>\n`;
13
+ const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --auto\nOverride: --override-ticket <ticket> --override-target <target> --override-scope <scope>\n`;
14
14
 
15
15
  function usageFailure(message: string): void {
16
16
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);