@fitlab-ai/agent-infra 0.11.11 → 0.11.13

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 (154) hide show
  1. package/dist/bin/internal-cli.js +2 -2
  2. package/dist/lib/agent-clients/adapter.js +7 -1
  3. package/dist/lib/agent-clients/adapters/claude-code.js +5 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +78 -63
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/binding.js +114 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +447 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +130 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +131 -0
  9. package/dist/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.js +2 -2
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +48 -1
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +124 -62
  12. package/dist/lib/{internal/codex-sandbox-controller.js → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js} +3 -3
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -5
  14. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -213
  15. package/dist/lib/agent-clients/adapters/codex-orchestration.js +468 -0
  16. package/dist/lib/agent-clients/adapters/codex.js +40 -1
  17. package/dist/lib/agent-clients/adapters/index.js +2 -10
  18. package/dist/lib/agent-clients/tokens.js +3 -2
  19. package/dist/lib/internal/agent-client.js +132 -72
  20. package/dist/lib/internal/cli-route-inventory.js +2 -2
  21. package/dist/lib/internal/sandbox-control.js +1 -81
  22. package/dist/lib/internal/task-finalization.js +26 -19
  23. package/dist/lib/internal/task-lifecycle.js +1 -1
  24. package/dist/lib/internal/task-operation-registry.js +1 -1
  25. package/dist/lib/internal/task-validate.js +1 -1
  26. package/dist/lib/platform/github-client.js +33 -3
  27. package/dist/lib/platform/issues.js +18 -6
  28. package/dist/lib/runtime/agent-runtime.js +6 -29
  29. package/dist/lib/sandbox/commands/control-status.js +1 -0
  30. package/dist/lib/sandbox/commands/create.js +7 -2
  31. package/dist/lib/sandbox/commands/ls.js +4 -5
  32. package/dist/lib/sandbox/commands/rm.js +1 -1
  33. package/dist/lib/sandbox/control/agent-operation.js +34 -0
  34. package/dist/lib/sandbox/control/client.js +14 -201
  35. package/dist/lib/sandbox/control/executor.js +12 -125
  36. package/dist/lib/sandbox/control/lifecycle.js +66 -4
  37. package/dist/lib/sandbox/control/protocol.js +15 -54
  38. package/dist/lib/sandbox/control/server.js +216 -281
  39. package/dist/lib/sandbox/control/task-view.js +0 -13
  40. package/dist/lib/sandbox/mounts.js +9 -1
  41. package/dist/lib/sandbox/recovery.js +31 -10
  42. package/dist/lib/sandbox/removal.js +28 -5
  43. package/dist/lib/sandbox/workspace-identity.js +18 -6
  44. package/dist/lib/sandbox/workspace-view.js +56 -19
  45. package/dist/lib/task/commands/ls.js +3 -3
  46. package/dist/lib/task/control-authority.js +56 -264
  47. package/dist/lib/task/control-recovery.js +2 -3
  48. package/dist/lib/task/create.js +2 -2
  49. package/dist/lib/task/delegation-receipts.js +74 -161
  50. package/dist/lib/task/events.js +88 -44
  51. package/dist/lib/task/finalization.js +35 -6
  52. package/dist/lib/task/lifecycle.js +274 -115
  53. package/dist/lib/task/orchestration.js +201 -194
  54. package/dist/lib/task/runtime-paths.js +10 -0
  55. package/dist/lib/task/short-id.js +37 -27
  56. package/dist/lib/task/verification-engine.js +32 -26
  57. package/dist/lib/task/workspace-snapshot.js +1 -1
  58. package/dist/lifecycle-build-manifest.json +3 -3
  59. package/lib/agent-clients/adapter.ts +39 -0
  60. package/lib/agent-clients/adapters/claude-code.ts +5 -1
  61. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +105 -80
  62. package/lib/agent-clients/adapters/codex-lifecycle/binding.ts +147 -0
  63. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +510 -0
  64. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +158 -0
  65. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +1 -1
  66. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +151 -0
  67. package/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.ts +4 -4
  68. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +43 -0
  69. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +8 -5
  70. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +190 -65
  71. package/lib/{internal/codex-sandbox-controller.ts → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts} +2 -2
  72. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -5
  73. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +270 -224
  74. package/lib/agent-clients/adapters/codex-orchestration.ts +632 -0
  75. package/lib/agent-clients/adapters/codex.ts +44 -1
  76. package/lib/agent-clients/adapters/index.ts +7 -20
  77. package/lib/agent-clients/tokens.ts +4 -3
  78. package/lib/internal/agent-client.ts +142 -73
  79. package/lib/internal/cli-route-inventory.ts +2 -2
  80. package/lib/internal/sandbox-control.ts +0 -97
  81. package/lib/internal/task-finalization.ts +30 -18
  82. package/lib/internal/task-lifecycle.ts +1 -1
  83. package/lib/internal/task-operation-registry.ts +1 -1
  84. package/lib/internal/task-validate.ts +1 -1
  85. package/lib/platform/github-client.ts +34 -3
  86. package/lib/platform/issues.ts +18 -6
  87. package/lib/runtime/agent-runtime.ts +6 -41
  88. package/lib/sandbox/commands/control-status.ts +1 -0
  89. package/lib/sandbox/commands/create.ts +7 -1
  90. package/lib/sandbox/commands/ls.ts +4 -5
  91. package/lib/sandbox/commands/rm.ts +1 -1
  92. package/lib/sandbox/control/agent-operation.ts +49 -0
  93. package/lib/sandbox/control/client.ts +20 -254
  94. package/lib/sandbox/control/executor.ts +13 -136
  95. package/lib/sandbox/control/lifecycle.ts +67 -4
  96. package/lib/sandbox/control/protocol.ts +22 -66
  97. package/lib/sandbox/control/server.ts +21 -95
  98. package/lib/sandbox/control/task-view.ts +0 -19
  99. package/lib/sandbox/mounts.ts +10 -1
  100. package/lib/sandbox/recovery.ts +38 -17
  101. package/lib/sandbox/removal.ts +33 -7
  102. package/lib/sandbox/workspace-identity.ts +16 -6
  103. package/lib/sandbox/workspace-view.ts +50 -18
  104. package/lib/task/commands/ls.ts +3 -3
  105. package/lib/task/control-authority.ts +66 -378
  106. package/lib/task/control-recovery.ts +3 -4
  107. package/lib/task/create.ts +3 -3
  108. package/lib/task/delegation-receipts.ts +87 -231
  109. package/lib/task/events.ts +100 -42
  110. package/lib/task/finalization.ts +37 -7
  111. package/lib/task/lifecycle.ts +257 -109
  112. package/lib/task/orchestration.ts +260 -237
  113. package/lib/task/runtime-paths.ts +12 -0
  114. package/lib/task/short-id.ts +33 -40
  115. package/lib/task/verification-engine.ts +30 -25
  116. package/lib/task/workspace-snapshot.ts +1 -1
  117. package/package.json +1 -1
  118. package/templates/.agents/hooks/lifecycle-delegation.js +1 -25
  119. package/templates/.agents/rules/README.en.md +2 -1
  120. package/templates/.agents/rules/README.zh-CN.md +2 -1
  121. package/templates/.agents/rules/next-step-output.en.md +6 -19
  122. package/templates/.agents/rules/next-step-output.zh-CN.md +6 -19
  123. package/templates/.agents/rules/strategy-pattern.en.md +25 -0
  124. package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
  125. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  126. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  127. package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
  128. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
  129. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  130. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  131. package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
  132. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
  133. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +0 -1
  134. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +0 -1
  135. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -29
  136. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -26
  137. package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
  138. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
  139. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  140. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  141. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  142. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  143. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  144. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  145. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  146. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  147. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  148. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  149. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +0 -546
  150. package/dist/lib/internal/codex-lifecycle.js +0 -385
  151. package/dist/lib/task/codex-orchestration.js +0 -206
  152. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +0 -776
  153. package/lib/internal/codex-lifecycle.ts +0 -390
  154. package/lib/task/codex-orchestration.ts +0 -276
@@ -1,21 +1,25 @@
1
1
  import crypto from 'node:crypto';
2
- import fs from 'node:fs';
3
2
  import path from 'node:path';
4
3
 
4
+ import {
5
+ mutateDelegationAdapterEvidence,
6
+ mutateDelegationAdapterEvidenceWithinTaskLock,
7
+ readRun
8
+ } from '../../../task/orchestration.ts';
9
+ import type { DelegationReceipt } from '../../../task/delegation-receipts.ts';
10
+ import { resolveTaskRef } from '../../../task/resolve-ref.ts';
5
11
  import {
6
12
  createCodexLifecycleState,
7
13
  expireCodexLifecycleState,
8
14
  reduceCodexLifecycleEvent
9
15
  } from './evidence.ts';
10
- import type {
11
- CodexLifecycleEvent,
12
- CodexLifecycleState
13
- } from './evidence.ts';
14
- import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
16
+ import type { CodexLifecycleEvent, CodexLifecycleState } from './evidence.ts';
17
+ import type { CodexLifecycleTaskBinding } from './binding.ts';
15
18
 
16
19
  type StoredCodexLifecycle = Readonly<{
17
- schemaVersion: 1;
20
+ schemaVersion: 2;
18
21
  revision: number;
22
+ taskBinding: CodexLifecycleTaskBinding | null;
19
23
  state: CodexLifecycleState;
20
24
  consumer: string | null;
21
25
  consumedAt: string | null;
@@ -23,8 +27,34 @@ type StoredCodexLifecycle = Readonly<{
23
27
  updatedAt: string;
24
28
  }>;
25
29
 
30
+ type CodexLifecycleActivationEvidence = Readonly<{
31
+ kind: 'codex-lifecycle-v2';
32
+ hookDefinitionHash: string;
33
+ startRevision: number;
34
+ protocolVersion?: number;
35
+ packageVersion?: string;
36
+ internalExecutableBuildHash?: string;
37
+ lifecycleContractHash?: string;
38
+ hookSource?: 'project' | 'managed' | 'isolated-user';
39
+ hookSourcePathDigest?: string;
40
+ hookSourceHash?: string;
41
+ capabilitySessionId?: string;
42
+ capabilityTurnId?: string;
43
+ capabilityToolUseId?: string;
44
+ spawnToolUseId?: string;
45
+ spawnObservedAt?: string;
46
+ controllerInstanceDigest?: string | null;
47
+ controlGeneration?: string | null;
48
+ }>;
49
+
50
+ type CodexAdapterState = Readonly<{
51
+ records: Readonly<Record<string, StoredCodexLifecycle>>;
52
+ activationEvidence?: CodexLifecycleActivationEvidence;
53
+ }>;
54
+
26
55
  type CodexLifecycleStoreOptions = Readonly<{
27
- root?: string;
56
+ repoRoot?: string;
57
+ taskId: string;
28
58
  cliVersion: string;
29
59
  now?: () => string;
30
60
  }>;
@@ -36,128 +66,119 @@ type CodexLifecycleStoreResult = Readonly<{
36
66
  }>;
37
67
 
38
68
  type ActiveCodexLifecycleEvidenceQuery = Readonly<{
39
- nativeAgent: string;
40
69
  hookDefinitionHash: string;
70
+ identity?: Readonly<{ sessionId: string; turnId: string; toolUseId: string }>;
41
71
  }>;
42
72
 
43
- const LOCK_RETRY_MS = 10;
44
- const LOCK_TIMEOUT_MS = 1_000;
45
- const LOCK_STALE_MS = 30_000;
46
- function digest(value: string): string {
47
- return crypto.createHash('sha256').update(value).digest('hex');
73
+ function readCodexLifecycleActivationEvidence(receipt: DelegationReceipt): CodexLifecycleActivationEvidence | null {
74
+ const state = storedEvidence(receipt.adapterEvidence?.codex);
75
+ return state.activationEvidence && typeof state.activationEvidence === 'object'
76
+ ? state.activationEvidence
77
+ : null;
48
78
  }
49
79
 
50
- function readRecord(file: string): StoredCodexLifecycle {
51
- const value = JSON.parse(fs.readFileSync(file, 'utf8')) as StoredCodexLifecycle;
52
- if (
53
- value.schemaVersion !== 1
54
- || !Number.isSafeInteger(value.revision)
55
- || value.revision < 1
56
- || !value.state
57
- || value.state.schemaVersion !== 1
58
- || (value.spawnObservedAt != null && (
59
- typeof value.spawnObservedAt !== 'string'
60
- || !Number.isFinite(Date.parse(value.spawnObservedAt))
61
- ))
62
- ) {
63
- throw new Error(`Codex lifecycle record '${path.basename(file)}' is invalid`);
64
- }
65
- return Object.freeze({ ...value, spawnObservedAt: value.spawnObservedAt ?? null });
80
+ const NON_PERSISTENT_FAILURES = new Set([
81
+ 'CODEX_EVIDENCE_IDENTITY_MISMATCH',
82
+ 'CODEX_EVIDENCE_PARENT_MISMATCH',
83
+ 'CODEX_EVIDENCE_REPLAY_CONFLICT'
84
+ ]);
85
+
86
+ function recordKey(sessionId: string, turnId: string, toolUseId: string): string {
87
+ return crypto.createHash('sha256').update(`${sessionId}\0${turnId}\0${toolUseId}`).digest('hex');
88
+ }
89
+
90
+ function isRecord(value: unknown): value is Record<string, unknown> {
91
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
92
+ }
93
+
94
+ function storedEvidence(value: unknown): CodexAdapterState {
95
+ if (!isRecord(value) || !isRecord(value.records)) return Object.freeze({ records: Object.freeze({}) });
96
+ return value as unknown as CodexAdapterState;
97
+ }
98
+
99
+ function receipts(taskDir: string): readonly DelegationReceipt[] {
100
+ const run = readRun(taskDir);
101
+ return run ? Object.freeze([...run.receipts, ...(run.pendingDelegation ? [run.pendingDelegation] : [])]) : [];
102
+ }
103
+
104
+ function allRecords(taskDir: string): readonly StoredCodexLifecycle[] {
105
+ return Object.freeze(receipts(taskDir).flatMap((receipt) => {
106
+ const state = storedEvidence(receipt.adapterEvidence?.codex);
107
+ return Object.values(state.records);
108
+ }));
66
109
  }
67
110
 
68
- function recordFiles(root: string): string[] {
69
- if (!fs.existsSync(root)) return [];
70
- return fs.readdirSync(root)
71
- .filter((name) => /^[a-f0-9]{64}\.json$/.test(name))
72
- .map((name) => path.join(root, name));
111
+ function lifecyclePath(taskDir: string): string {
112
+ return path.join(taskDir, '.runtime', 'orchestration.json');
113
+ }
114
+
115
+ function assertCurrentCanApply(
116
+ current: StoredCodexLifecycle,
117
+ binding: CodexLifecycleTaskBinding | null
118
+ ): void {
119
+ if (current.revision > 0 && binding && JSON.stringify(current.taskBinding) !== JSON.stringify(binding)) {
120
+ throw new Error('Codex lifecycle task binding does not match the stored spawn');
121
+ }
122
+ if (current.consumer) throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
73
123
  }
74
124
 
75
125
  function hasActiveCodexLifecycleEvidence(
76
- root: string,
126
+ taskDir: string,
77
127
  query: ActiveCodexLifecycleEvidenceQuery
78
128
  ): boolean {
79
- return recordFiles(root).map(readRecord).some((record) => {
80
- const evidence = record.state.startEvidence;
129
+ return allRecords(taskDir).some((record) => {
81
130
  return record.consumer === null
82
- && ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status)
83
- && evidence?.nativeAgent === query.nativeAgent
84
- && evidence.hookDefinitionHash === query.hookDefinitionHash;
131
+ && !['invalid', 'expired'].includes(record.state.status)
132
+ && record.state.spawn?.hookDefinitionHash === query.hookDefinitionHash
133
+ && (!query.identity || record.state.spawn?.sessionId === query.identity.sessionId
134
+ && record.state.spawn.turnId === query.identity.turnId
135
+ && record.state.spawn.toolUseId === query.identity.toolUseId);
85
136
  });
86
137
  }
87
138
 
88
- function writeRecord(file: string, record: StoredCodexLifecycle, expectedRevision: number): void {
89
- const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
90
- fs.writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`, {
91
- encoding: 'utf8',
92
- flag: 'wx',
93
- mode: 0o600
94
- });
95
- try {
96
- const actualRevision = fs.existsSync(file) ? readRecord(file).revision : 0;
97
- if (actualRevision !== expectedRevision) {
98
- throw new Error(`Codex lifecycle revision changed from ${expectedRevision} to ${actualRevision}`);
99
- }
100
- fs.renameSync(temp, file);
101
- fs.chmodSync(file, 0o600);
102
- } finally {
103
- if (fs.existsSync(temp)) fs.unlinkSync(temp);
104
- }
105
- }
106
-
107
139
  function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
108
- const root = options.root ?? resolveAgentRuntimeStoreRoot({ store: 'lifecycle' });
140
+ if (!/^TASK-[0-9]{8}-[0-9]{6}$/u.test(options.taskId)) {
141
+ throw new Error('Codex lifecycle store task id is invalid');
142
+ }
143
+ const repoRoot = options.repoRoot ?? process.cwd();
144
+ const resolved = resolveTaskRef(options.taskId, { repoRoot });
145
+ if (!resolved.ok) throw new Error(`${resolved.code}: ${resolved.message}`);
146
+ const taskDir = resolved.taskDir;
109
147
  const now = options.now ?? (() => new Date().toISOString());
110
- fs.mkdirSync(root, { recursive: true, mode: 0o700 });
111
- fs.chmodSync(root, 0o700);
112
-
113
- function withWriteLock<T>(operation: () => T): T {
114
- const lock = path.join(root, '.write.lock');
115
- const deadline = Date.now() + LOCK_TIMEOUT_MS;
116
- let descriptor: number | null = null;
117
- while (descriptor === null) {
118
- try {
119
- descriptor = fs.openSync(lock, 'wx', 0o600);
120
- } catch (error) {
121
- if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
122
- try {
123
- if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
124
- fs.unlinkSync(lock);
125
- continue;
126
- }
127
- } catch (lockError) {
128
- if ((lockError as NodeJS.ErrnoException).code === 'ENOENT') continue;
129
- throw lockError;
130
- }
131
- if (Date.now() >= deadline) throw new Error('Codex lifecycle store is busy');
132
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
133
- }
134
- }
135
- try {
136
- return operation();
137
- } finally {
138
- fs.closeSync(descriptor);
139
- fs.unlinkSync(lock);
148
+
149
+ function currentBinding(): CodexLifecycleTaskBinding {
150
+ const run = readRun(taskDir);
151
+ const receipt = run?.pendingDelegation;
152
+ if (!run || !receipt || receipt.client !== 'codex') {
153
+ throw new Error('Codex lifecycle evidence has no current task delegation');
140
154
  }
155
+ return Object.freeze({ taskId: receipt.taskId, runId: receipt.runId, receiptId: receipt.id });
141
156
  }
142
157
 
143
- function findByChild(childThreadId: string): string[] {
144
- return recordFiles(root).filter((file) => {
145
- const child = readRecord(file).state.child;
146
- return child?.childThreadId === childThreadId;
147
- });
158
+ function findByChild(childThreadId: string): StoredCodexLifecycle[] {
159
+ return allRecords(taskDir).filter((record) => record.state.child?.childThreadId === childThreadId);
148
160
  }
149
161
 
150
- function locate(event: CodexLifecycleEvent): string {
162
+ function locate(event: CodexLifecycleEvent): StoredCodexLifecycle {
151
163
  if (event.type === 'hook-spawn') {
152
- return path.join(root, `${digest(`${event.sessionId}\0${event.turnId}\0${event.toolUseId}`)}.json`);
164
+ const key = recordKey(event.sessionId, event.turnId, event.toolUseId);
165
+ const existing = allRecords(taskDir).find((record) => record.spawnObservedAt && record.state.spawn
166
+ && recordKey(record.state.spawn.sessionId, record.state.spawn.turnId, record.state.spawn.toolUseId) === key);
167
+ return existing ?? Object.freeze({
168
+ schemaVersion: 2,
169
+ revision: 0,
170
+ taskBinding: event.taskBinding ?? null,
171
+ state: createCodexLifecycleState(options.cliVersion),
172
+ consumer: null,
173
+ consumedAt: null,
174
+ spawnObservedAt: null,
175
+ updatedAt: now()
176
+ });
153
177
  }
154
178
  if (event.type === 'hook-child') {
155
- const matches = recordFiles(root).filter((file) => {
156
- const state = readRecord(file).state;
157
- return state.spawn?.sessionId === event.parentThreadId
158
- && state.spawn.nativeAgent === event.nativeAgent
159
- && (!state.child || state.child.childThreadId === event.childThreadId);
160
- });
179
+ const matches = allRecords(taskDir).filter((record) => record.state.spawn?.sessionId === event.parentThreadId
180
+ && record.state.spawn.nativeAgent === event.nativeAgent
181
+ && (!record.state.child || record.state.child.childThreadId === event.childThreadId));
161
182
  if (matches.length !== 1) {
162
183
  throw new Error(matches.length === 0
163
184
  ? 'Codex lifecycle parent session and agent correlation was not found'
@@ -174,164 +195,189 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
174
195
  return matches[0]!;
175
196
  }
176
197
 
198
+ function update(
199
+ binding: CodexLifecycleTaskBinding,
200
+ key: string,
201
+ mutate: (current: StoredCodexLifecycle | null) => StoredCodexLifecycle,
202
+ taskLockHeld = false
203
+ ): StoredCodexLifecycle {
204
+ let result: StoredCodexLifecycle | null = null;
205
+ const updateState = taskLockHeld ? mutateDelegationAdapterEvidenceWithinTaskLock : mutateDelegationAdapterEvidence;
206
+ updateState(options.taskId, binding.receiptId, 'codex', (value) => {
207
+ const currentState = storedEvidence(value);
208
+ const current = currentState.records[key] ?? null;
209
+ result = mutate(current);
210
+ return Object.freeze({
211
+ ...currentState,
212
+ records: Object.freeze({ ...currentState.records, [key]: result })
213
+ });
214
+ }, { repoRoot, now });
215
+ if (!result) throw new Error('Codex lifecycle record was not updated');
216
+ return result;
217
+ }
218
+
177
219
  function apply(event: CodexLifecycleEvent): CodexLifecycleStoreResult {
178
- return withWriteLock(() => {
179
- const file = locate(event);
180
- const observedAt = now();
181
- const current = fs.existsSync(file)
182
- ? readRecord(file)
183
- : Object.freeze({
184
- schemaVersion: 1 as const,
185
- revision: 0,
186
- state: createCodexLifecycleState(options.cliVersion),
187
- consumer: null,
188
- consumedAt: null,
189
- spawnObservedAt: null,
190
- updatedAt: observedAt
191
- });
192
- if (current.consumer) throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
193
- const nextState = reduceCodexLifecycleEvent(current.state, event);
194
- const next = Object.freeze({
195
- schemaVersion: 1 as const,
196
- revision: current.revision + 1,
197
- state: nextState,
220
+ const existing = event.type === 'hook-spawn' ? null : locate(event);
221
+ const binding = event.type === 'hook-spawn' ? event.taskBinding ?? currentBinding() : existing?.taskBinding;
222
+ if (!binding) throw new Error('Codex lifecycle event has no task receipt binding');
223
+ const key = event.type === 'hook-spawn'
224
+ ? recordKey(event.sessionId, event.turnId, event.toolUseId)
225
+ : existing?.state.spawn
226
+ ? recordKey(existing.state.spawn.sessionId, existing.state.spawn.turnId, existing.state.spawn.toolUseId)
227
+ : '';
228
+ const observedAt = now();
229
+ const next = update(binding, key, (current) => {
230
+ const base = current ?? Object.freeze({
231
+ schemaVersion: 2 as const,
232
+ revision: 0,
233
+ taskBinding: binding,
234
+ state: createCodexLifecycleState(options.cliVersion),
198
235
  consumer: null,
199
236
  consumedAt: null,
200
- spawnObservedAt: event.type === 'hook-spawn' && current.revision === 0
201
- ? observedAt
202
- : current.spawnObservedAt,
237
+ spawnObservedAt: null,
238
+ updatedAt: observedAt
239
+ });
240
+ assertCurrentCanApply(base, binding);
241
+ const state = reduceCodexLifecycleEvent(base.state, event);
242
+ if (state === base.state) return base;
243
+ if (state.status === 'invalid' && state.error && NON_PERSISTENT_FAILURES.has(state.error.code)) {
244
+ throw new Error(`${state.error.code}: ${state.error.message}`);
245
+ }
246
+ return Object.freeze({
247
+ ...base,
248
+ revision: base.revision + 1,
249
+ state,
250
+ spawnObservedAt: event.type === 'hook-spawn' && base.revision === 0 ? observedAt : base.spawnObservedAt,
203
251
  updatedAt: observedAt
204
252
  });
205
- writeRecord(file, next, current.revision);
206
- return Object.freeze({ path: file, revision: next.revision, state: next.state });
207
253
  });
254
+ return Object.freeze({ path: lifecyclePath(taskDir), revision: next.revision, state: next.state });
208
255
  }
209
256
 
210
257
  function applyToSpawn(
211
- identity: Readonly<{ sessionId: string; turnId: string; toolUseId: string }>,
212
- event: Extract<CodexLifecycleEvent, { type: 'hook-child' }>
258
+ identity: Readonly<{ sessionId: string; turnId: string; toolUseId: string; taskBinding: CodexLifecycleTaskBinding }>,
259
+ event: Extract<CodexLifecycleEvent, { type: 'hook-child' }>,
260
+ expectedRevision: number
213
261
  ): CodexLifecycleStoreResult {
214
- return withWriteLock(() => {
215
- const file = path.join(root, `${digest(`${identity.sessionId}\0${identity.turnId}\0${identity.toolUseId}`)}.json`);
216
- if (!fs.existsSync(file)) throw new Error('Codex lifecycle spawn identity was not found');
217
- const current = readRecord(file);
218
- if (current.consumer) throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
219
- if (
220
- current.state.spawn?.sessionId !== identity.sessionId
221
- || current.state.spawn.turnId !== identity.turnId
222
- || current.state.spawn.toolUseId !== identity.toolUseId
223
- ) throw new Error('Codex lifecycle spawn identity does not match the stored event');
224
- const nextState = reduceCodexLifecycleEvent(current.state, event);
225
- const next = Object.freeze({
226
- ...current,
227
- revision: current.revision + 1,
228
- state: nextState,
229
- updatedAt: now()
230
- });
231
- writeRecord(file, next, current.revision);
232
- return Object.freeze({ path: file, revision: next.revision, state: next.state });
262
+ const key = recordKey(identity.sessionId, identity.turnId, identity.toolUseId);
263
+ const current = allRecords(taskDir).find((record) => record.state.spawn
264
+ && recordKey(record.state.spawn.sessionId, record.state.spawn.turnId, record.state.spawn.toolUseId) === key);
265
+ if (!current) throw new Error('Codex lifecycle spawn identity was not found');
266
+ const next = update(identity.taskBinding, key, (latest) => {
267
+ if (!latest || JSON.stringify(latest.taskBinding) !== JSON.stringify(identity.taskBinding)) {
268
+ throw new Error('Codex lifecycle task binding does not match the stored spawn');
269
+ }
270
+ if (latest.revision !== expectedRevision) {
271
+ throw new Error(`Codex lifecycle revision changed from ${expectedRevision} to ${latest.revision}`);
272
+ }
273
+ if (latest.consumer) throw new Error(`Codex lifecycle evidence was already consumed by '${latest.consumer}'`);
274
+ if (latest.state.spawn?.sessionId !== identity.sessionId
275
+ || latest.state.spawn.turnId !== identity.turnId
276
+ || latest.state.spawn.toolUseId !== identity.toolUseId
277
+ || latest.state.spawn.nativeAgent !== event.nativeAgent
278
+ || event.sessionId !== identity.sessionId
279
+ || event.parentThreadId !== identity.sessionId) {
280
+ throw new Error('Codex lifecycle spawn identity does not match the stored event');
281
+ }
282
+ const state = reduceCodexLifecycleEvent(latest.state, event);
283
+ if (state.status === 'invalid' && state.error && NON_PERSISTENT_FAILURES.has(state.error.code)) {
284
+ throw new Error(`${state.error.code}: ${state.error.message}`);
285
+ }
286
+ return state === latest.state ? latest : Object.freeze({ ...latest, revision: latest.revision + 1, state, updatedAt: now() });
233
287
  });
288
+ return Object.freeze({ path: lifecyclePath(taskDir), revision: next.revision, state: next.state });
234
289
  }
235
290
 
236
291
  function findByParent(parentThreadId: string): readonly StoredCodexLifecycle[] {
237
- const matches = recordFiles(root)
238
- .map(readRecord)
239
- .filter((record) => record.state.startEvidence?.parentThreadId === parentThreadId
240
- && ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status));
292
+ const matches = allRecords(taskDir).filter((record) => record.state.startEvidence?.parentThreadId === parentThreadId
293
+ && ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status));
241
294
  const unconsumed = matches.filter((record) => !record.consumer);
242
295
  if (unconsumed.length > 1) throw new Error(`Codex lifecycle parent '${parentThreadId}' has ambiguous active children`);
243
- if (unconsumed.length === 1) return Object.freeze(unconsumed);
244
- return Object.freeze(matches
245
- .filter((record) => record.consumer && record.state.status === 'stop-ready')
246
- .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)));
296
+ return Object.freeze(unconsumed.length === 1 ? unconsumed : matches.filter((record) => record.consumer));
297
+ }
298
+
299
+ function findByTaskBinding(binding: CodexLifecycleTaskBinding): readonly StoredCodexLifecycle[] {
300
+ return Object.freeze(allRecords(taskDir).filter((record) => record.taskBinding?.runId === binding.runId
301
+ && record.taskBinding.receiptId === binding.receiptId));
247
302
  }
248
303
 
249
304
  function read(childThreadId: string): StoredCodexLifecycle {
250
305
  const matches = findByChild(childThreadId);
251
- if (matches.length === 0) throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
252
- if (matches.length > 1) throw new Error(`Codex lifecycle child '${childThreadId}' is ambiguous`);
253
- return readRecord(matches[0]!);
306
+ if (matches.length !== 1) throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
307
+ return matches[0]!;
254
308
  }
255
309
 
256
- function consumeInternal(
310
+ function consumeWithLockMode(
257
311
  childThreadId: string,
258
312
  consumer: string,
259
- expectedHookDefinitionHash?: string
313
+ expectedHookDefinitionHash: string | undefined,
314
+ expectedTaskBinding: CodexLifecycleTaskBinding | undefined,
315
+ taskLockHeld: boolean
260
316
  ): StoredCodexLifecycle {
261
317
  if (!consumer.trim()) throw new Error('Codex lifecycle consumer is required');
262
- return withWriteLock(() => {
263
- const matches = findByChild(childThreadId);
264
- if (matches.length !== 1) throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
265
- const file = matches[0]!;
266
- const current = readRecord(file);
267
- if (current.consumer) {
268
- if (current.consumer !== consumer) {
269
- throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
270
- }
271
- if (
272
- expectedHookDefinitionHash
273
- && current.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash
274
- ) throw new Error('Codex lifecycle hook definition hash is stale');
275
- return current;
318
+ const current = read(childThreadId);
319
+ const binding = expectedTaskBinding ?? current.taskBinding;
320
+ if (!binding) throw new Error('Codex lifecycle task receipt binding is missing');
321
+ const spawn = current.state.spawn;
322
+ if (!spawn) {
323
+ throw new Error(taskLockHeld
324
+ ? 'Codex lifecycle task receipt binding is missing'
325
+ : 'Codex lifecycle spawn evidence is missing');
326
+ }
327
+ const key = recordKey(spawn.sessionId, spawn.turnId, spawn.toolUseId);
328
+ return update(binding, key, (latest) => {
329
+ if (!latest || expectedTaskBinding && JSON.stringify(latest.taskBinding) !== JSON.stringify(expectedTaskBinding)) {
330
+ throw new Error('Codex lifecycle task binding does not match the expected receipt');
276
331
  }
277
- if (current.state.status !== 'stop-ready') throw new Error('Codex lifecycle evidence is not stop-ready');
278
- if (
279
- expectedHookDefinitionHash
280
- && current.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash
281
- ) throw new Error('Codex lifecycle hook definition hash is stale');
282
- const next = Object.freeze({
283
- ...current,
284
- revision: current.revision + 1,
285
- consumer,
286
- consumedAt: now(),
287
- updatedAt: now()
288
- });
289
- writeRecord(file, next, current.revision);
290
- return next;
291
- });
332
+ if (latest.consumer && latest.consumer !== consumer) {
333
+ throw new Error(`Codex lifecycle evidence was already consumed by '${latest.consumer}'`);
334
+ }
335
+ if (latest.state.status !== 'stop-ready') throw new Error('Codex lifecycle evidence is not stop-ready');
336
+ if (expectedHookDefinitionHash && latest.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash) {
337
+ throw new Error('Codex lifecycle hook definition hash is stale');
338
+ }
339
+ if (latest.consumer) return latest;
340
+ return Object.freeze({ ...latest, revision: latest.revision + 1, consumer, consumedAt: now(), updatedAt: now() });
341
+ }, taskLockHeld);
292
342
  }
293
343
 
294
344
  function consume(
295
345
  childThreadId: string,
296
346
  consumer: string,
297
- expectedHookDefinitionHash?: string
347
+ expectedHookDefinitionHash?: string,
348
+ expectedTaskBinding?: CodexLifecycleTaskBinding
298
349
  ): StoredCodexLifecycle {
299
- if (!consumer.trim()) throw new Error('Codex lifecycle consumer is required');
300
- return consumeInternal(childThreadId, consumer, expectedHookDefinitionHash);
350
+ return consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, false);
351
+ }
352
+
353
+ function consumeWithinTaskLock(
354
+ childThreadId: string,
355
+ consumer: string,
356
+ expectedHookDefinitionHash?: string,
357
+ expectedTaskBinding?: CodexLifecycleTaskBinding
358
+ ): StoredCodexLifecycle {
359
+ return consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, true);
301
360
  }
302
361
 
303
362
  function expireBefore(cutoff: string): number {
304
- return withWriteLock(() => {
305
- let changed = 0;
306
- for (const file of recordFiles(root)) {
307
- const current = readRecord(file);
308
- if (current.updatedAt >= cutoff) continue;
309
- if (current.consumer || ['invalid', 'expired', 'stop-ready'].includes(current.state.status)) {
310
- fs.unlinkSync(file);
311
- changed += 1;
312
- continue;
313
- }
314
- const expiredState = expireCodexLifecycleState(current.state);
315
- if (expiredState === current.state) continue;
316
- writeRecord(file, Object.freeze({
317
- ...current,
318
- revision: current.revision + 1,
319
- state: expiredState,
320
- updatedAt: now()
321
- }), current.revision);
363
+ let changed = 0;
364
+ for (const record of allRecords(taskDir)) {
365
+ if (record.updatedAt >= cutoff || !record.taskBinding?.receiptId || !record.state.spawn) continue;
366
+ const key = recordKey(record.state.spawn.sessionId, record.state.spawn.turnId, record.state.spawn.toolUseId);
367
+ update(record.taskBinding, key, (latest) => {
368
+ if (!latest || latest.updatedAt >= cutoff || latest.consumer
369
+ || ['invalid', 'expired', 'stop-ready'].includes(latest.state.status)) return latest ?? record;
370
+ const state = expireCodexLifecycleState(latest.state);
371
+ if (state === latest.state) return latest;
322
372
  changed += 1;
323
- }
324
- return changed;
325
- });
373
+ return Object.freeze({ ...latest, revision: latest.revision + 1, state, updatedAt: now() });
374
+ });
375
+ }
376
+ return changed;
326
377
  }
327
378
 
328
- return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
379
+ return Object.freeze({ taskId: options.taskId, root: path.join(taskDir, '.runtime'), apply, applyToSpawn, consume, consumeWithinTaskLock, expireBefore, findByParent, findByTaskBinding, read });
329
380
  }
330
381
 
331
- export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
332
- export type {
333
- ActiveCodexLifecycleEvidenceQuery,
334
- CodexLifecycleStoreOptions,
335
- CodexLifecycleStoreResult,
336
- StoredCodexLifecycle
337
- };
382
+ export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence, readCodexLifecycleActivationEvidence };
383
+ export type { ActiveCodexLifecycleEvidenceQuery, CodexLifecycleActivationEvidence, CodexLifecycleStoreOptions, CodexLifecycleStoreResult, StoredCodexLifecycle };