@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
@@ -49,7 +49,9 @@ function taskControlTransportFailure(message, code = 'TASK_CONTROL_TRANSPORT_INV
49
49
  }
50
50
  let controlRouted = false;
51
51
  if (!taskViewGuardFailed && !markerlessHelp && (taskControlCommand || taskWorkflowCommand || manualValidationWorkflowCommand)) {
52
- const transport = resolveSandboxControlTransport(process.env);
52
+ const transport = resolveSandboxControlTransport(process.env, {
53
+ localWorkflow: command === 'task-orchestration' || taskWorkflowCommand || manualValidationWorkflowCommand
54
+ });
53
55
  switch (transport.kind) {
54
56
  case 'fail-closed': {
55
57
  const reason = transport.reasonCode ?? 'TASK_CONTROL_TRANSPORT_INVALID';
@@ -60,9 +62,12 @@ if (!taskViewGuardFailed && !markerlessHelp && (taskControlCommand || taskWorkfl
60
62
  // The host CLI is the direct-host authority. Domain handlers retain their
61
63
  // task locks, atomic writes, and operation-specific recovery facts.
62
64
  break;
65
+ case 'sandbox-local': {
66
+ break;
67
+ }
63
68
  case 'broker-client': {
64
69
  const { sandboxControl } = await import('../lib/internal/sandbox-control.js');
65
- await sandboxControl(['client', ...(taskControlCommand ? [command] : ['task-workflow', command]), ...process.argv.slice(3)]);
70
+ await sandboxControl(['client', command, ...process.argv.slice(3)]);
66
71
  break;
67
72
  }
68
73
  }
@@ -408,6 +408,9 @@ function defineAgentClientAdapter(candidate) {
408
408
  capabilities: Object.freeze(capabilities),
409
409
  modelSelection: frozenModelSelection,
410
410
  delegationEvidence: Object.freeze({ ...evidence }),
411
+ ...(candidate.orchestrationAdapter === undefined
412
+ ? {}
413
+ : { orchestrationAdapter: Object.freeze({ ...candidate.orchestrationAdapter }) }),
411
414
  project: Object.freeze({
412
415
  ownedPathPrefixes: Object.freeze(paths),
413
416
  ...projectAssets,
@@ -0,0 +1,234 @@
1
+ import fs from 'node:fs';
2
+ import { createCodexLifecycleStore } from './store.js';
3
+ import { managedDelegationRole } from '../../../task/delegation-receipts.js';
4
+ import { normalizeAgentToken } from '../../tokens.js';
5
+ import { locateActivityLog, pairEntries, appendActivityEntry } from '../../../task/activity-log.js';
6
+ import { captureTaskWriteMetadata, writeTask } from '../../../task/write.js';
7
+ import { parseTypedTaskFrontmatter } from '../../../task/frontmatter.js';
8
+ import { resolveTaskRef } from '../../../task/resolve-ref.js';
9
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
10
+ import { recoverActivatedOrchestrationDelegationUnderLock, readRun } from '../../../task/orchestration.js';
11
+ const ACTIONS = {
12
+ analysis: 'Analyze Task',
13
+ 'review-analysis': 'Review Analysis',
14
+ plan: 'Plan Task',
15
+ 'review-plan': 'Review Plan',
16
+ code: 'Code Task',
17
+ 'review-code': 'Review Code'
18
+ };
19
+ function result(request, status, extra = {}) {
20
+ return {
21
+ status,
22
+ changed: status === 'applied',
23
+ targetState: 'active',
24
+ requestRef: request.taskRef,
25
+ intent: 'recover-started',
26
+ taskId: null,
27
+ receiptId: null,
28
+ childId: null,
29
+ error: null,
30
+ ...extra
31
+ };
32
+ }
33
+ function failure(request, status, code, message, extra = {}) {
34
+ return result(request, status, { error: { code, message }, ...extra });
35
+ }
36
+ function actionPattern(receipt) {
37
+ const action = ACTIONS[receipt.stage];
38
+ if (!action)
39
+ return null;
40
+ const escaped = action.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
41
+ 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');
42
+ }
43
+ function openRows(section) {
44
+ return pairEntries([...section.entries]).filter((row) => row.started !== '' && row.done === '');
45
+ }
46
+ function rowForReceipt(rows, receipt) {
47
+ const pattern = actionPattern(receipt);
48
+ return pattern ? rows.filter((row) => pattern.test(row.step)) : [];
49
+ }
50
+ function readLifecycleStore(options, repoRoot) {
51
+ return options.lifecycleStore ?? createCodexLifecycleStore({
52
+ root: resolveAgentRuntimeStoreRoot({ repoRoot, store: 'lifecycle' }),
53
+ cliVersion: 'recovery',
54
+ now: options.now
55
+ });
56
+ }
57
+ function readStoredEvidence(store, childId) {
58
+ try {
59
+ return store.read(childId);
60
+ }
61
+ catch (error) {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ if (message.includes('was not found uniquely'))
64
+ return { missing: true };
65
+ return { error: error instanceof Error ? error : new Error(message) };
66
+ }
67
+ }
68
+ function validateStopRecord(record, receipt) {
69
+ const start = record.state.startEvidence;
70
+ const stop = record.state.stopEvidence;
71
+ const provenance = receipt.lifecycleProvenance;
72
+ const host = receipt.hostEvidence;
73
+ if (receipt.client !== 'codex'
74
+ || !provenance
75
+ || host?.kind !== 'codex-lifecycle-v2'
76
+ || !start
77
+ || !stop
78
+ || record.state.status !== 'stop-ready'
79
+ || stop.terminalStatus !== 'completed'
80
+ || stop.hookStopObserved !== true
81
+ || start.childThreadId !== receipt.childId
82
+ || start.parentThreadId !== receipt.parentId
83
+ || managedDelegationRole(start.nativeAgent) !== receipt.role
84
+ || start.hookDefinitionHash !== provenance.hookDefinitionHash
85
+ || host.hookDefinitionHash !== provenance.hookDefinitionHash
86
+ || host.capabilitySessionId !== provenance.capabilitySessionId
87
+ || host.capabilityTurnId !== provenance.capabilityTurnId
88
+ || host.capabilityToolUseId !== provenance.capabilityToolUseId
89
+ || host.spawnToolUseId !== start.spawnToolUseId
90
+ || !host.spawnObservedAt
91
+ || host.spawnObservedAt !== record.spawnObservedAt
92
+ || host.controllerInstanceDigest !== provenance.controllerInstanceDigest
93
+ || host.controlGeneration !== provenance.controlGeneration
94
+ || typeof host.controllerInstanceDigest !== 'string'
95
+ || typeof host.controlGeneration !== 'string'
96
+ || host.startRevision < 1
97
+ || record.revision <= host.startRevision
98
+ || record.state.child?.childThreadId !== receipt.childId
99
+ || record.state.stop?.childThreadId !== receipt.childId) {
100
+ return { ok: false, code: 'RECOVERY_STOP_EVIDENCE_INVALID', message: 'Codex stop evidence does not match the activated receipt' };
101
+ }
102
+ if (record.consumer !== null && record.consumer !== receipt.id) {
103
+ return { ok: false, code: 'RECOVERY_CONSUMER_CONFLICT', message: `Codex lifecycle evidence was consumed by '${record.consumer}'` };
104
+ }
105
+ return { ok: true };
106
+ }
107
+ function abortedCandidates(run, rows) {
108
+ const candidates = run.receipts.flatMap((receipt) => {
109
+ if (receipt.status !== 'aborted' || receipt.activatedAt === null || receipt.client !== 'codex')
110
+ return [];
111
+ const matches = rowForReceipt(rows, receipt);
112
+ return matches.length === 1 ? [{ receipt, row: matches[0] }] : [];
113
+ });
114
+ return candidates;
115
+ }
116
+ function appendRecoveryLog(request, taskId, section, candidate, repoRoot, writer) {
117
+ const metadata = captureTaskWriteMetadata();
118
+ const body = appendActivityEntry(section, {
119
+ time: metadata.timestamp,
120
+ step: `${candidate.row.step} [aborted]`,
121
+ agent: request.agent,
122
+ note: `terminated Codex delegation; receipt=${candidate.receipt.id}; child=${candidate.receipt.childId}`
123
+ });
124
+ const written = writer({
125
+ taskRef: taskId,
126
+ expectedState: 'active',
127
+ mutations: [{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body }]
128
+ }, { repoRoot, metadataProvider: () => metadata });
129
+ if (written.status === 'failed') {
130
+ return failure(request, 'owner-unknown', 'RECOVERY_LOG_WRITE_FAILED', written.error.message, {
131
+ taskId, receiptId: candidate.receipt.id, childId: candidate.receipt.childId
132
+ });
133
+ }
134
+ return result(request, 'applied', {
135
+ taskId, receiptId: candidate.receipt.id, childId: candidate.receipt.childId
136
+ });
137
+ }
138
+ function recoverStartedLifecycleUnderLock(request, options = {}) {
139
+ if (!request || request.intent !== 'recover-started' || request.auto !== true || normalizeAgentToken(request.agent) !== 'codex') {
140
+ return failure(request, 'conflict', 'RECOVERY_PAYLOAD_INVALID', 'automatic recovery requires a task and the Codex agent');
141
+ }
142
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
143
+ if (!resolved.ok)
144
+ return failure(request, 'conflict', resolved.code, resolved.message);
145
+ const taskId = resolved.taskId;
146
+ let content;
147
+ try {
148
+ content = fs.readFileSync(resolved.taskMdPath, 'utf8');
149
+ parseTypedTaskFrontmatter(content);
150
+ }
151
+ catch (error) {
152
+ return failure(request, 'conflict', 'RECOVERY_TASK_INVALID', error instanceof Error ? error.message : String(error), { taskId });
153
+ }
154
+ const section = locateActivityLog(content);
155
+ if (!section)
156
+ return failure(request, 'conflict', 'RECOVERY_LOG_INVALID', 'task has no unique Activity Log section', { taskId });
157
+ let run;
158
+ try {
159
+ run = readRun(resolved.taskDir, options.orchestration);
160
+ }
161
+ catch (error) {
162
+ return failure(request, 'owner-unknown', 'RECOVERY_ORCHESTRATION_UNKNOWN', error instanceof Error ? error.message : String(error), { taskId });
163
+ }
164
+ if (!run)
165
+ return result(request, 'no-op', { taskId });
166
+ const rows = openRows(section);
167
+ const pending = run.pendingDelegation;
168
+ if (!pending) {
169
+ const candidates = abortedCandidates(run, rows);
170
+ if (candidates.length === 0)
171
+ return result(request, 'no-op', { taskId });
172
+ if (candidates.length > 1) {
173
+ return failure(request, 'conflict', 'RECOVERY_CANDIDATE_AMBIGUOUS', 'more than one aborted delegation has an open lifecycle row', { taskId });
174
+ }
175
+ return appendRecoveryLog(request, taskId, section, candidates[0], resolved.repoRoot, options.writeTask ?? writeTask);
176
+ }
177
+ if (pending.status !== 'activated')
178
+ return result(request, 'no-op', { taskId });
179
+ if (pending.client !== 'codex' || run.status !== 'running' || run.pause !== null) {
180
+ return failure(request, 'conflict', 'RECOVERY_ORCHESTRATION_INVALID', 'activated recovery requires one running Codex delegation', {
181
+ taskId, receiptId: pending.id, childId: pending.childId
182
+ });
183
+ }
184
+ const matchingRows = rowForReceipt(rows, pending);
185
+ if (matchingRows.length !== 1 || normalizeAgentToken(matchingRows[0].agent) !== 'codex' || matchingRows[0].note !== 'started') {
186
+ return failure(request, 'conflict', 'RECOVERY_LOG_CONFLICT', 'activated delegation does not have one matching open lifecycle row', {
187
+ taskId, receiptId: pending.id, childId: pending.childId
188
+ });
189
+ }
190
+ if (!pending.childId) {
191
+ return failure(request, 'conflict', 'RECOVERY_DELEGATION_INVALID', 'activated delegation has no child identity', { taskId, receiptId: pending.id });
192
+ }
193
+ const store = readLifecycleStore(options, resolved.repoRoot);
194
+ const stored = readStoredEvidence(store, pending.childId);
195
+ if ('error' in stored)
196
+ return failure(request, 'owner-unknown', 'RECOVERY_STORE_UNKNOWN', stored.error.message, { taskId, receiptId: pending.id, childId: pending.childId });
197
+ if ('missing' in stored)
198
+ return failure(request, 'owner-unknown', 'RECOVERY_STOP_EVIDENCE_MISSING', 'matching Codex lifecycle stop evidence is missing', { taskId, receiptId: pending.id, childId: pending.childId });
199
+ const evidence = validateStopRecord(stored, pending);
200
+ if (!evidence.ok)
201
+ return failure(request, evidence.code === 'RECOVERY_CONSUMER_CONFLICT' ? 'conflict' : 'owner-unknown', evidence.code, evidence.message, { taskId, receiptId: pending.id, childId: pending.childId });
202
+ let consumed = stored;
203
+ if (stored.consumer === null) {
204
+ try {
205
+ consumed = store.consume(pending.childId, pending.id, pending.hostEvidence?.hookDefinitionHash);
206
+ }
207
+ catch (error) {
208
+ return failure(request, 'owner-unknown', 'RECOVERY_EVIDENCE_CONSUME_FAILED', error instanceof Error ? error.message : String(error), { taskId, receiptId: pending.id, childId: pending.childId });
209
+ }
210
+ }
211
+ if (!consumed.consumedAt) {
212
+ return failure(request, 'owner-unknown', 'RECOVERY_EVIDENCE_INVALID', 'consumed stop evidence has no timestamp', { taskId, receiptId: pending.id, childId: pending.childId });
213
+ }
214
+ const recovered = recoverActivatedOrchestrationDelegationUnderLock(taskId, {
215
+ receiptId: pending.id,
216
+ stage: pending.stage,
217
+ round: pending.round,
218
+ artifact: pending.artifact,
219
+ startedAgent: request.agent,
220
+ childId: pending.childId,
221
+ stopRevision: consumed.revision,
222
+ consumer: pending.id,
223
+ consumedAt: consumed.consumedAt
224
+ }, { ...options.orchestration, repoRoot: resolved.repoRoot, now: options.now });
225
+ if (recovered.status === 'failed' || !recovered.run) {
226
+ return failure(request, 'owner-unknown', recovered.error?.code ?? 'RECOVERY_ORCHESTRATION_FAILED', recovered.error?.message ?? 'orchestration recovery failed', { taskId, receiptId: pending.id, childId: pending.childId });
227
+ }
228
+ return appendRecoveryLog(request, taskId, section, { receipt: recovered.run.receipts.at(-1), row: matchingRows[0] }, resolved.repoRoot, options.writeTask ?? writeTask);
229
+ }
230
+ function recoverStartedLifecycleFromAdapter(request, options = {}) {
231
+ return recoverStartedLifecycleUnderLock(request, options);
232
+ }
233
+ export { recoverStartedLifecycleFromAdapter, recoverStartedLifecycleUnderLock };
234
+ //# sourceMappingURL=recovery.js.map
@@ -5,8 +5,7 @@ import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import semver from 'semver';
7
7
  import * as toml from 'smol-toml';
8
- import { requestCodexControllerClose, requestCodexControllerOpen, requestCodexControllerVerify, requestSandboxControl } from '../../../sandbox/control/client.js';
9
- import { readSandboxControlStatus } from '../../../sandbox/control/state.js';
8
+ import { requestCodexControllerClose, requestCodexControllerOpen, requestCodexControllerVerify } from '../../../sandbox/control/client.js';
10
9
  import { getProcessStartTime } from '../../../server/process-state.js';
11
10
  import { LIFECYCLE_PROTOCOL_VERSION } from './build-identity.js';
12
11
  import { contextFromControllerLease, controllerProofFromContext, verifyCodexSandboxControllerContextWithWarnings as verifyContextFileWithWarnings, writeCodexSandboxControllerContext } from './controller-context.js';
@@ -56,44 +55,18 @@ function verifyRuntime(runtimeDir) {
56
55
  fs.rmSync(probe, { force: true });
57
56
  }
58
57
  }
59
- function verifyControl(taskId, control) {
58
+ function verifyControllerBinding(context, control, requestControllerVerify) {
60
59
  verifyRuntime(control.runtimeDir);
61
- let status = null;
62
- let lastError = null;
63
- for (let attempt = 0; attempt < 5; attempt += 1) {
64
- try {
65
- status = readSandboxControlStatus(control.statusDir);
66
- break;
67
- }
68
- catch (error) {
69
- lastError = error;
70
- if (attempt < 4)
71
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
72
- }
73
- }
74
- if (!status) {
75
- throw new Error(`CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE: ${String(lastError)}`);
76
- }
77
- if (status.generation !== control.generation
78
- || status.state !== 'healthy'
79
- || Date.now() - status.updatedAt > 1_500) {
80
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_UNAVAILABLE');
81
- }
82
- const response = requestSandboxControl({
83
- family: 'task-orchestration',
84
- args: [taskId, 'status'],
60
+ const verified = requestControllerVerify({
61
+ controllerProof: controllerProofFromContext(context),
85
62
  ...control,
86
63
  timeoutMs: 30_000
87
64
  });
88
- let payload = {};
89
- try {
90
- payload = JSON.parse(response.stdout);
91
- }
92
- catch {
65
+ if (verified.binding.taskId !== context.taskId
66
+ || verified.binding.controlGeneration !== context.controlGeneration
67
+ || verified.binding.controllerInstanceDigest !== context.controllerInstanceDigest) {
93
68
  throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
94
69
  }
95
- if (payload.taskId !== taskId)
96
- throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
97
70
  }
98
71
  function verifyCodexSandboxControllerContextWithWarnings(contextPath, options = {}) {
99
72
  const control = options.control ?? controlFromEnvironment();
@@ -272,7 +245,7 @@ function prepareCodexSandboxController(input, options = {}) {
272
245
  || opened.lease.controllerProcess.startTime !== parentStartTime) {
273
246
  throw new Error('SANDBOX_CONTROL_RESULT_INVALID');
274
247
  }
275
- (options.verifyTaskBinding ?? verifyControl)(taskId, control);
248
+ verifyControllerBinding(context, control, options.verifyController ?? requestCodexControllerVerify);
276
249
  const contextPath = path.join(home, 'controller-context.json');
277
250
  writeCodexSandboxControllerContext(contextPath, context);
278
251
  const shimDir = path.join(home, 'bin');
@@ -6,19 +6,9 @@ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js'
6
6
  const LOCK_RETRY_MS = 10;
7
7
  const LOCK_TIMEOUT_MS = 1_000;
8
8
  const LOCK_STALE_MS = 30_000;
9
- const RECOVERY_CONSUMER_RE = /^lifecycle-recovery:([^:\r\n]+):([^:\r\n]+)$/u;
10
9
  function digest(value) {
11
10
  return crypto.createHash('sha256').update(value).digest('hex');
12
11
  }
13
- function isRecoveryConsumer(value) {
14
- return RECOVERY_CONSUMER_RE.test(value);
15
- }
16
- function recoveryConsumer(taskId, receiptId) {
17
- if (!/^[^:\r\n]+$/u.test(taskId) || !/^[^:\r\n]+$/u.test(receiptId)) {
18
- throw new Error('Codex lifecycle recovery claim identity is invalid');
19
- }
20
- return `lifecycle-recovery:${taskId}:${receiptId}`;
21
- }
22
12
  function readRecord(file) {
23
13
  const value = JSON.parse(fs.readFileSync(file, 'utf8'));
24
14
  if (value.schemaVersion !== 1
@@ -255,29 +245,8 @@ function createCodexLifecycleStore(options) {
255
245
  function consume(childThreadId, consumer, expectedHookDefinitionHash) {
256
246
  if (!consumer.trim())
257
247
  throw new Error('Codex lifecycle consumer is required');
258
- if (consumer.startsWith('lifecycle-recovery:')) {
259
- throw new Error('Codex lifecycle recovery claims require claimRecovery');
260
- }
261
248
  return consumeInternal(childThreadId, consumer, expectedHookDefinitionHash);
262
249
  }
263
- function claimRecovery(childThreadId, taskId, receiptId, expectedHookDefinitionHash) {
264
- return consumeInternal(childThreadId, recoveryConsumer(taskId, receiptId), expectedHookDefinitionHash);
265
- }
266
- function releaseRecovery(childThreadId, consumer) {
267
- if (!/^lifecycle-recovery:[^:\r\n]+:[^:\r\n]+$/.test(consumer))
268
- return false;
269
- return withWriteLock(() => {
270
- const matches = findByChild(childThreadId);
271
- if (matches.length !== 1)
272
- return false;
273
- const file = matches[0];
274
- const current = readRecord(file);
275
- if (current.state.status !== 'stop-ready' || current.consumer !== consumer || !current.consumedAt)
276
- return false;
277
- fs.unlinkSync(file);
278
- return true;
279
- });
280
- }
281
250
  function expireBefore(cutoff) {
282
251
  return withWriteLock(() => {
283
252
  let changed = 0;
@@ -285,8 +254,6 @@ function createCodexLifecycleStore(options) {
285
254
  const current = readRecord(file);
286
255
  if (current.updatedAt >= cutoff)
287
256
  continue;
288
- if (current.consumer && isRecoveryConsumer(current.consumer))
289
- continue;
290
257
  if (current.consumer || ['invalid', 'expired', 'stop-ready'].includes(current.state.status)) {
291
258
  fs.unlinkSync(file);
292
259
  changed += 1;
@@ -306,7 +273,7 @@ function createCodexLifecycleStore(options) {
306
273
  return changed;
307
274
  });
308
275
  }
309
- return Object.freeze({ root, apply, applyToSpawn, consume, claimRecovery, releaseRecovery, expireBefore, findByParent, read });
276
+ return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
310
277
  }
311
278
  export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
312
279
  //# sourceMappingURL=store.js.map
@@ -24,6 +24,16 @@ const codexAdapter = defineAgentClientAdapter({
24
24
  actualModel: 'app-server',
25
25
  actualReasoningEffort: 'app-server'
26
26
  },
27
+ orchestrationAdapter: {
28
+ prepareDelegation: async (...args) => {
29
+ const { prepareCodexOrchestrationDelegation } = await import('../../task/codex-orchestration.js');
30
+ return prepareCodexOrchestrationDelegation(...args);
31
+ },
32
+ recoverStarted: async (...args) => {
33
+ const { recoverStartedLifecycleFromAdapter } = await import('./codex-lifecycle/recovery.js');
34
+ return recoverStartedLifecycleFromAdapter(...args);
35
+ }
36
+ },
27
37
  project: {
28
38
  ownedPathPrefixes: ['.codex/'],
29
39
  managed: ['.codex/hooks.json', '.codex/agents/'],
@@ -5,7 +5,7 @@ import { resolveTaskRef } from '../task/resolve-ref.js';
5
5
  import { applyTaskEvent } from '../task/events.js';
6
6
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
7
7
  import { normalizeAgentToken } from '../agent-clients/tokens.js';
8
- import { createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, readManualValidationReceipt, writeManualValidationReceiptAtomic } from '../task/manual-validation-receipt.js';
8
+ import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, createManualValidationReceipt, manualValidationFinalSummaryDigest, manualValidationFinalSummaryProjectionMatches, readManualValidationReceipt, writeManualValidationReceiptAtomic } from '../task/manual-validation-receipt.js';
9
9
  import { archiveManualValidationGeneration, createManualValidationTransaction, readManualValidationTransaction, retryManualValidationTransaction, summaryPreimageDigest, transitionManualValidationTransaction, validateManualValidationGenerationArchive, writeManualValidationTransactionAtomic } from '../task/manual-validation-transaction.js';
10
10
  import { sha256File } from '../task/artifact-receipts.js';
11
11
  import { summaryCommentState, syncPullRequestSummary } from '../platform/pr-summary.js';
@@ -22,28 +22,22 @@ function printFailure(format, error) {
22
22
  process.stderr.write(`${error.code}: ${error.message}\n`);
23
23
  process.exitCode = 1;
24
24
  }
25
- 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;
26
- const TRAILING_CANONICAL_REPORT_PLACEHOLDER = /\n*<!--\s*canonical-pr-change-report\s*-->\s*$/u;
27
- function manualSummaryBody(body, phase, transactionId, receiptDigest = '', evidenceDigest = '', prHeadSha = '') {
25
+ const MANUAL_VALIDATION_STATUS_HEADING = /^###\s+(?:⚠️\s+(?:需人工校验|Manual Validation Required)|✅\s+(?:人工验证已通过|无需人工校验|Manual Validation Passed|No Manual Validation Required)|⏳\s+(?:人工验证待收尾|Manual Validation Pending))\s*$/mu;
26
+ function manualSummaryBody(body, phase) {
28
27
  const chinese = /###\s+(?:⚠️\s+需人工校验|✅\s+(?:人工验证已通过|无需人工校验)|⏳\s+人工验证待收尾)\s*$/mu.test(body);
29
- const section = phase === 'pending'
30
- ? chinese
31
- ? `### ⏳ 人工验证待收尾\n\n人工验证证据已暂存;事务 ${transactionId} 正在等待最终完成。`
32
- : `### ⏳ Manual Validation Pending\n\nManual validation evidence is staged; transaction ${transactionId} is awaiting final promotion.`
33
- : chinese
34
- ? `### ✅ 人工验证已通过\n\n人工验证已通过;transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`
35
- : `### ✅ Manual Validation Passed\n\nManual validation passed; transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`;
36
- let inserted = false;
37
- const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, (matched) => {
38
- if (inserted)
39
- return '';
40
- inserted = true;
41
- const trailingPlaceholder = TRAILING_CANONICAL_REPORT_PLACEHOLDER.exec(matched)?.[0];
42
- return trailingPlaceholder
43
- ? `${trailingPlaceholder.trim()}\n\n${section}\n\n`
44
- : `${section}\n\n`;
45
- });
46
- return inserted ? `${updated.replace(/\s+$/u, '')}\n` : `${body.replace(/\s+$/u, '')}\n\n${section}\n`;
28
+ const heading = phase === 'pending'
29
+ ? chinese ? '### ⏳ 人工验证待收尾' : '### ⏳ Manual Validation Pending'
30
+ : chinese ? '### ✅ 人工验证已通过' : '### ✅ Manual Validation Passed';
31
+ const marker = phase === 'final'
32
+ ? `\n\n${MANUAL_VALIDATION_RECEIPT_PLACEHOLDER}`
33
+ : '';
34
+ if (MANUAL_VALIDATION_STATUS_HEADING.test(body)) {
35
+ return `${body.replace(MANUAL_VALIDATION_STATUS_HEADING, `${heading}${marker}`).replace(/\s+$/u, '')}\n`;
36
+ }
37
+ const fallback = phase === 'pending'
38
+ ? chinese ? '人工验证正在等待最终完成。' : 'Manual validation is awaiting final promotion.'
39
+ : chinese ? '人工验证已通过。' : 'Manual validation passed.';
40
+ return `${body.replace(/\s+$/u, '')}\n\n${heading}${marker}\n\n${fallback}\n`;
47
41
  }
48
42
  function openManualValidationStarted(taskMdPath) {
49
43
  let content;
@@ -102,7 +96,9 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
102
96
  const pullRequest = currentState.pullRequest;
103
97
  const preimageBody = currentState.comment?.body ?? '';
104
98
  const preimage = { commentId: currentState.comment?.id ?? null, body: preimageBody, digest: summaryPreimageDigest(preimageBody) };
105
- const evidenceDigest = sha256File(path.resolve(cwd, values.summaryFile));
99
+ const summaryPath = path.resolve(cwd, values.summaryFile);
100
+ const finalBodyWithoutReceipt = fs.readFileSync(summaryPath, 'utf8');
101
+ const evidenceDigest = sha256File(summaryPath);
106
102
  const archiveGeneration = options.archiveGeneration ?? archiveManualValidationGeneration;
107
103
  let transactionResult = readManualValidationTransaction(resolved.taskDir, { taskId: resolved.taskId, prNumber: pullRequest.number, prHeadSha: pullRequest.head.sha, evidenceDigest, artifact: values.artifact });
108
104
  let previousTransaction = null;
@@ -135,8 +131,7 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
135
131
  const transactionId = transactionResult.ok
136
132
  ? transactionResult.value.transactionId
137
133
  : (openStarted.transactionId ?? values.transactionId ?? `mv-${Date.now()}`);
138
- const finalBodyWithoutReceipt = fs.readFileSync(path.resolve(cwd, values.summaryFile), 'utf8');
139
- const pendingBody = manualSummaryBody(finalBodyWithoutReceipt, 'pending', transactionId);
134
+ const pendingBody = manualSummaryBody(finalBodyWithoutReceipt, 'pending');
140
135
  const createTransaction = () => createManualValidationTransaction({
141
136
  transactionId,
142
137
  taskId: resolved.taskId,
@@ -145,7 +140,7 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
145
140
  evidenceDigest,
146
141
  summaryPreimage: preimage,
147
142
  pendingSummaryDigest: summaryPreimageDigest(pendingBody),
148
- finalSummaryDigest: manualValidationFinalSummaryDigest(manualSummaryBody(finalBodyWithoutReceipt, 'final', transactionId, '<receipt>', evidenceDigest, pullRequest.head.sha)),
143
+ finalSummaryDigest: manualValidationFinalSummaryDigest(manualSummaryBody(finalBodyWithoutReceipt, 'final')),
149
144
  artifact: values.artifact,
150
145
  attempt: 1,
151
146
  createdAt: new Date().toISOString(),
@@ -249,7 +244,7 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
249
244
  transitionAndPersist('final-promotion-in-progress');
250
245
  }
251
246
  const promoted = await syncPullRequestSummary(taskRef, {
252
- 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,
247
+ cwd, client: options.client, agent, body: manualSummaryBody(finalBodyWithoutReceipt, 'final'), changeReportFile: path.resolve(cwd, values.changeReportFile), primaryResult, strict: true,
253
248
  manualValidation: { phase: 'final', transactionId: transaction.transactionId, receiptDigest: receipt.receiptDigest, prHeadSha: receipt.prHeadSha, authority: 'coordinator' }, lockAlreadyHeld: true
254
249
  });
255
250
  if (!['applied', 'no-op'].includes(promoted.status))
@@ -1,10 +1,9 @@
1
- import { recoverSandboxControl, requestSandboxControl, requestSandboxTaskFinalization, requestSandboxTaskWorkflow, resolveVisibleActiveShortId, SandboxControlClientError } from '../sandbox/control/client.js';
1
+ import { recoverSandboxControl, requestSandboxControl, requestSandboxTaskFinalization, SandboxControlClientError } from '../sandbox/control/client.js';
2
2
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
3
3
  import { serveSandboxControl } from '../sandbox/control/server.js';
4
4
  import { runSandboxControlExecutor } from '../sandbox/control/executor.js';
5
5
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
6
6
  import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.js';
7
- import { createTaskWorkflowRequest } from '../sandbox/control/task-workflow.js';
8
7
  function writeFinalizationEnvelope(status, accepted, error, changed = false, result = null) {
9
8
  process.stdout.write(`${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`);
10
9
  }
@@ -132,47 +131,6 @@ async function sandboxControl(args) {
132
131
  sandboxFinalizationClient(commandArgs);
133
132
  return;
134
133
  }
135
- if (family === 'task-workflow') {
136
- const command = commandArgs[0];
137
- const taskId = process.env.AGENT_INFRA_TASK_ID;
138
- const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
139
- if (!command || !taskId || !generation) {
140
- 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`);
141
- process.exitCode = 1;
142
- return;
143
- }
144
- let workflow;
145
- try {
146
- const args = commandArgs.slice(1);
147
- if (/^\d+$/.test(args[0] ?? '')) {
148
- const resolved = resolveVisibleActiveShortId(args[0]);
149
- if (resolved !== taskId)
150
- throw new Error('SANDBOX_TASK_REF_MISMATCH');
151
- args[0] = resolved;
152
- }
153
- workflow = createTaskWorkflowRequest(command, args, taskId, generation);
154
- }
155
- catch (error) {
156
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'SANDBOX_CONTROL_REQUEST_INVALID', message: error instanceof Error ? error.message : String(error) } })}\n`);
157
- process.exitCode = 1;
158
- return;
159
- }
160
- let response;
161
- try {
162
- response = requestSandboxTaskWorkflow({ workflow });
163
- }
164
- catch (error) {
165
- if (!(error instanceof SandboxControlClientError))
166
- throw error;
167
- writeClientError(error);
168
- process.exitCode = error.detail.retryable ? 75 : 1;
169
- return;
170
- }
171
- process.stdout.write(response.stdout);
172
- process.stderr.write(response.stderr);
173
- process.exitCode = response.phase === 'rejected' ? response.error?.retryable ? 75 : 1 : response.exitCode ?? 1;
174
- return;
175
- }
176
134
  let response;
177
135
  try {
178
136
  response = requestSandboxControl({ family, args: commandArgs });
@@ -2,7 +2,7 @@ import { createDirectHostExecutionContext, dispatchTaskControlOperation, parseTa
2
2
  import { lifecycleIntentCatalog } from '../task/lifecycle.js';
3
3
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
4
4
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
5
- 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`;
5
+ 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`;
6
6
  function usageFailure(message) {
7
7
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
8
8
  process.stderr.write(USAGE);
@@ -229,6 +229,8 @@ export function resolveDelegatedTaskOperation(args) {
229
229
  const [operation, ...rest] = args;
230
230
  if (operation !== 'client' || !rest[0])
231
231
  return null;
232
+ if (rest[0] === 'task-orchestration')
233
+ return null;
232
234
  return resolveTaskOperation('internal', rest[0], rest.slice(1));
233
235
  }
234
236
  const TASK_MARKER_KEYS = [
@@ -352,14 +354,16 @@ export function resolveSandboxControlTransport(env = process.env, options = {})
352
354
  if (!hasCompleteConfig) {
353
355
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
354
356
  }
355
- return { kind: 'broker-client', reasonCode: null };
357
+ return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
356
358
  }
357
359
  if (!hasAnyMarker)
358
360
  return { kind: 'direct-host', reasonCode: null };
359
361
  if (!hasCompleteConfig || (taskBound && !runtime) || (!taskBound && runtime)) {
360
362
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
361
363
  }
362
- return { kind: 'broker-client', reasonCode: null };
364
+ return options.localWorkflow
365
+ ? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' }
366
+ : { kind: 'broker-client', reasonCode: null };
363
367
  }
364
368
  export function hasTaskBoundMarker(env = process.env) {
365
369
  return taskMarkerState(env) === 'task-bound';