@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,120 +1,101 @@
1
1
  import crypto from 'node:crypto';
2
- import fs from 'node:fs';
3
2
  import path from 'node:path';
3
+ import { mutateDelegationAdapterEvidence, mutateDelegationAdapterEvidenceWithinTaskLock, readRun } from '../../../task/orchestration.js';
4
+ import { resolveTaskRef } from '../../../task/resolve-ref.js';
4
5
  import { createCodexLifecycleState, expireCodexLifecycleState, reduceCodexLifecycleEvent } from './evidence.js';
5
- import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
6
- const LOCK_RETRY_MS = 10;
7
- const LOCK_TIMEOUT_MS = 1_000;
8
- const LOCK_STALE_MS = 30_000;
9
- function digest(value) {
10
- return crypto.createHash('sha256').update(value).digest('hex');
6
+ function readCodexLifecycleActivationEvidence(receipt) {
7
+ const state = storedEvidence(receipt.adapterEvidence?.codex);
8
+ return state.activationEvidence && typeof state.activationEvidence === 'object'
9
+ ? state.activationEvidence
10
+ : null;
11
11
  }
12
- function readRecord(file) {
13
- const value = JSON.parse(fs.readFileSync(file, 'utf8'));
14
- if (value.schemaVersion !== 1
15
- || !Number.isSafeInteger(value.revision)
16
- || value.revision < 1
17
- || !value.state
18
- || value.state.schemaVersion !== 1
19
- || (value.spawnObservedAt != null && (typeof value.spawnObservedAt !== 'string'
20
- || !Number.isFinite(Date.parse(value.spawnObservedAt))))) {
21
- throw new Error(`Codex lifecycle record '${path.basename(file)}' is invalid`);
22
- }
23
- return Object.freeze({ ...value, spawnObservedAt: value.spawnObservedAt ?? null });
12
+ const NON_PERSISTENT_FAILURES = new Set([
13
+ 'CODEX_EVIDENCE_IDENTITY_MISMATCH',
14
+ 'CODEX_EVIDENCE_PARENT_MISMATCH',
15
+ 'CODEX_EVIDENCE_REPLAY_CONFLICT'
16
+ ]);
17
+ function recordKey(sessionId, turnId, toolUseId) {
18
+ return crypto.createHash('sha256').update(`${sessionId}\0${turnId}\0${toolUseId}`).digest('hex');
24
19
  }
25
- function recordFiles(root) {
26
- if (!fs.existsSync(root))
27
- return [];
28
- return fs.readdirSync(root)
29
- .filter((name) => /^[a-f0-9]{64}\.json$/.test(name))
30
- .map((name) => path.join(root, name));
20
+ function isRecord(value) {
21
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
31
22
  }
32
- function hasActiveCodexLifecycleEvidence(root, query) {
33
- return recordFiles(root).map(readRecord).some((record) => {
34
- const evidence = record.state.startEvidence;
35
- return record.consumer === null
36
- && ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status)
37
- && evidence?.nativeAgent === query.nativeAgent
38
- && evidence.hookDefinitionHash === query.hookDefinitionHash;
39
- });
23
+ function storedEvidence(value) {
24
+ if (!isRecord(value) || !isRecord(value.records))
25
+ return Object.freeze({ records: Object.freeze({}) });
26
+ return value;
40
27
  }
41
- function writeRecord(file, record, expectedRevision) {
42
- const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
43
- fs.writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`, {
44
- encoding: 'utf8',
45
- flag: 'wx',
46
- mode: 0o600
47
- });
48
- try {
49
- const actualRevision = fs.existsSync(file) ? readRecord(file).revision : 0;
50
- if (actualRevision !== expectedRevision) {
51
- throw new Error(`Codex lifecycle revision changed from ${expectedRevision} to ${actualRevision}`);
52
- }
53
- fs.renameSync(temp, file);
54
- fs.chmodSync(file, 0o600);
55
- }
56
- finally {
57
- if (fs.existsSync(temp))
58
- fs.unlinkSync(temp);
28
+ function receipts(taskDir) {
29
+ const run = readRun(taskDir);
30
+ return run ? Object.freeze([...run.receipts, ...(run.pendingDelegation ? [run.pendingDelegation] : [])]) : [];
31
+ }
32
+ function allRecords(taskDir) {
33
+ return Object.freeze(receipts(taskDir).flatMap((receipt) => {
34
+ const state = storedEvidence(receipt.adapterEvidence?.codex);
35
+ return Object.values(state.records);
36
+ }));
37
+ }
38
+ function lifecyclePath(taskDir) {
39
+ return path.join(taskDir, '.runtime', 'orchestration.json');
40
+ }
41
+ function assertCurrentCanApply(current, binding) {
42
+ if (current.revision > 0 && binding && JSON.stringify(current.taskBinding) !== JSON.stringify(binding)) {
43
+ throw new Error('Codex lifecycle task binding does not match the stored spawn');
59
44
  }
45
+ if (current.consumer)
46
+ throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
47
+ }
48
+ function hasActiveCodexLifecycleEvidence(taskDir, query) {
49
+ return allRecords(taskDir).some((record) => {
50
+ return record.consumer === null
51
+ && !['invalid', 'expired'].includes(record.state.status)
52
+ && record.state.spawn?.hookDefinitionHash === query.hookDefinitionHash
53
+ && (!query.identity || record.state.spawn?.sessionId === query.identity.sessionId
54
+ && record.state.spawn.turnId === query.identity.turnId
55
+ && record.state.spawn.toolUseId === query.identity.toolUseId);
56
+ });
60
57
  }
61
58
  function createCodexLifecycleStore(options) {
62
- const root = options.root ?? resolveAgentRuntimeStoreRoot({ store: 'lifecycle' });
59
+ if (!/^TASK-[0-9]{8}-[0-9]{6}$/u.test(options.taskId)) {
60
+ throw new Error('Codex lifecycle store task id is invalid');
61
+ }
62
+ const repoRoot = options.repoRoot ?? process.cwd();
63
+ const resolved = resolveTaskRef(options.taskId, { repoRoot });
64
+ if (!resolved.ok)
65
+ throw new Error(`${resolved.code}: ${resolved.message}`);
66
+ const taskDir = resolved.taskDir;
63
67
  const now = options.now ?? (() => new Date().toISOString());
64
- fs.mkdirSync(root, { recursive: true, mode: 0o700 });
65
- fs.chmodSync(root, 0o700);
66
- function withWriteLock(operation) {
67
- const lock = path.join(root, '.write.lock');
68
- const deadline = Date.now() + LOCK_TIMEOUT_MS;
69
- let descriptor = null;
70
- while (descriptor === null) {
71
- try {
72
- descriptor = fs.openSync(lock, 'wx', 0o600);
73
- }
74
- catch (error) {
75
- if (error.code !== 'EEXIST')
76
- throw error;
77
- try {
78
- if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
79
- fs.unlinkSync(lock);
80
- continue;
81
- }
82
- }
83
- catch (lockError) {
84
- if (lockError.code === 'ENOENT')
85
- continue;
86
- throw lockError;
87
- }
88
- if (Date.now() >= deadline)
89
- throw new Error('Codex lifecycle store is busy');
90
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
91
- }
92
- }
93
- try {
94
- return operation();
95
- }
96
- finally {
97
- fs.closeSync(descriptor);
98
- fs.unlinkSync(lock);
68
+ function currentBinding() {
69
+ const run = readRun(taskDir);
70
+ const receipt = run?.pendingDelegation;
71
+ if (!run || !receipt || receipt.client !== 'codex') {
72
+ throw new Error('Codex lifecycle evidence has no current task delegation');
99
73
  }
74
+ return Object.freeze({ taskId: receipt.taskId, runId: receipt.runId, receiptId: receipt.id });
100
75
  }
101
76
  function findByChild(childThreadId) {
102
- return recordFiles(root).filter((file) => {
103
- const child = readRecord(file).state.child;
104
- return child?.childThreadId === childThreadId;
105
- });
77
+ return allRecords(taskDir).filter((record) => record.state.child?.childThreadId === childThreadId);
106
78
  }
107
79
  function locate(event) {
108
80
  if (event.type === 'hook-spawn') {
109
- return path.join(root, `${digest(`${event.sessionId}\0${event.turnId}\0${event.toolUseId}`)}.json`);
81
+ const key = recordKey(event.sessionId, event.turnId, event.toolUseId);
82
+ const existing = allRecords(taskDir).find((record) => record.spawnObservedAt && record.state.spawn
83
+ && recordKey(record.state.spawn.sessionId, record.state.spawn.turnId, record.state.spawn.toolUseId) === key);
84
+ return existing ?? Object.freeze({
85
+ schemaVersion: 2,
86
+ revision: 0,
87
+ taskBinding: event.taskBinding ?? null,
88
+ state: createCodexLifecycleState(options.cliVersion),
89
+ consumer: null,
90
+ consumedAt: null,
91
+ spawnObservedAt: null,
92
+ updatedAt: now()
93
+ });
110
94
  }
111
95
  if (event.type === 'hook-child') {
112
- const matches = recordFiles(root).filter((file) => {
113
- const state = readRecord(file).state;
114
- return state.spawn?.sessionId === event.parentThreadId
115
- && state.spawn.nativeAgent === event.nativeAgent
116
- && (!state.child || state.child.childThreadId === event.childThreadId);
117
- });
96
+ const matches = allRecords(taskDir).filter((record) => record.state.spawn?.sessionId === event.parentThreadId
97
+ && record.state.spawn.nativeAgent === event.nativeAgent
98
+ && (!record.state.child || record.state.child.childThreadId === event.childThreadId));
118
99
  if (matches.length !== 1) {
119
100
  throw new Error(matches.length === 0
120
101
  ? 'Codex lifecycle parent session and agent correlation was not found'
@@ -130,150 +111,167 @@ function createCodexLifecycleStore(options) {
130
111
  }
131
112
  return matches[0];
132
113
  }
114
+ function update(binding, key, mutate, taskLockHeld = false) {
115
+ let result = null;
116
+ const updateState = taskLockHeld ? mutateDelegationAdapterEvidenceWithinTaskLock : mutateDelegationAdapterEvidence;
117
+ updateState(options.taskId, binding.receiptId, 'codex', (value) => {
118
+ const currentState = storedEvidence(value);
119
+ const current = currentState.records[key] ?? null;
120
+ result = mutate(current);
121
+ return Object.freeze({
122
+ ...currentState,
123
+ records: Object.freeze({ ...currentState.records, [key]: result })
124
+ });
125
+ }, { repoRoot, now });
126
+ if (!result)
127
+ throw new Error('Codex lifecycle record was not updated');
128
+ return result;
129
+ }
133
130
  function apply(event) {
134
- return withWriteLock(() => {
135
- const file = locate(event);
136
- const observedAt = now();
137
- const current = fs.existsSync(file)
138
- ? readRecord(file)
139
- : Object.freeze({
140
- schemaVersion: 1,
141
- revision: 0,
142
- state: createCodexLifecycleState(options.cliVersion),
143
- consumer: null,
144
- consumedAt: null,
145
- spawnObservedAt: null,
146
- updatedAt: observedAt
147
- });
148
- if (current.consumer)
149
- throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
150
- const nextState = reduceCodexLifecycleEvent(current.state, event);
151
- const next = Object.freeze({
152
- schemaVersion: 1,
153
- revision: current.revision + 1,
154
- state: nextState,
131
+ const existing = event.type === 'hook-spawn' ? null : locate(event);
132
+ const binding = event.type === 'hook-spawn' ? event.taskBinding ?? currentBinding() : existing?.taskBinding;
133
+ if (!binding)
134
+ throw new Error('Codex lifecycle event has no task receipt binding');
135
+ const key = event.type === 'hook-spawn'
136
+ ? recordKey(event.sessionId, event.turnId, event.toolUseId)
137
+ : existing?.state.spawn
138
+ ? recordKey(existing.state.spawn.sessionId, existing.state.spawn.turnId, existing.state.spawn.toolUseId)
139
+ : '';
140
+ const observedAt = now();
141
+ const next = update(binding, key, (current) => {
142
+ const base = current ?? Object.freeze({
143
+ schemaVersion: 2,
144
+ revision: 0,
145
+ taskBinding: binding,
146
+ state: createCodexLifecycleState(options.cliVersion),
155
147
  consumer: null,
156
148
  consumedAt: null,
157
- spawnObservedAt: event.type === 'hook-spawn' && current.revision === 0
158
- ? observedAt
159
- : current.spawnObservedAt,
149
+ spawnObservedAt: null,
150
+ updatedAt: observedAt
151
+ });
152
+ assertCurrentCanApply(base, binding);
153
+ const state = reduceCodexLifecycleEvent(base.state, event);
154
+ if (state === base.state)
155
+ return base;
156
+ if (state.status === 'invalid' && state.error && NON_PERSISTENT_FAILURES.has(state.error.code)) {
157
+ throw new Error(`${state.error.code}: ${state.error.message}`);
158
+ }
159
+ return Object.freeze({
160
+ ...base,
161
+ revision: base.revision + 1,
162
+ state,
163
+ spawnObservedAt: event.type === 'hook-spawn' && base.revision === 0 ? observedAt : base.spawnObservedAt,
160
164
  updatedAt: observedAt
161
165
  });
162
- writeRecord(file, next, current.revision);
163
- return Object.freeze({ path: file, revision: next.revision, state: next.state });
164
166
  });
167
+ return Object.freeze({ path: lifecyclePath(taskDir), revision: next.revision, state: next.state });
165
168
  }
166
- function applyToSpawn(identity, event) {
167
- return withWriteLock(() => {
168
- const file = path.join(root, `${digest(`${identity.sessionId}\0${identity.turnId}\0${identity.toolUseId}`)}.json`);
169
- if (!fs.existsSync(file))
170
- throw new Error('Codex lifecycle spawn identity was not found');
171
- const current = readRecord(file);
172
- if (current.consumer)
173
- throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
174
- if (current.state.spawn?.sessionId !== identity.sessionId
175
- || current.state.spawn.turnId !== identity.turnId
176
- || current.state.spawn.toolUseId !== identity.toolUseId)
169
+ function applyToSpawn(identity, event, expectedRevision) {
170
+ const key = recordKey(identity.sessionId, identity.turnId, identity.toolUseId);
171
+ const current = allRecords(taskDir).find((record) => record.state.spawn
172
+ && recordKey(record.state.spawn.sessionId, record.state.spawn.turnId, record.state.spawn.toolUseId) === key);
173
+ if (!current)
174
+ throw new Error('Codex lifecycle spawn identity was not found');
175
+ const next = update(identity.taskBinding, key, (latest) => {
176
+ if (!latest || JSON.stringify(latest.taskBinding) !== JSON.stringify(identity.taskBinding)) {
177
+ throw new Error('Codex lifecycle task binding does not match the stored spawn');
178
+ }
179
+ if (latest.revision !== expectedRevision) {
180
+ throw new Error(`Codex lifecycle revision changed from ${expectedRevision} to ${latest.revision}`);
181
+ }
182
+ if (latest.consumer)
183
+ throw new Error(`Codex lifecycle evidence was already consumed by '${latest.consumer}'`);
184
+ if (latest.state.spawn?.sessionId !== identity.sessionId
185
+ || latest.state.spawn.turnId !== identity.turnId
186
+ || latest.state.spawn.toolUseId !== identity.toolUseId
187
+ || latest.state.spawn.nativeAgent !== event.nativeAgent
188
+ || event.sessionId !== identity.sessionId
189
+ || event.parentThreadId !== identity.sessionId) {
177
190
  throw new Error('Codex lifecycle spawn identity does not match the stored event');
178
- const nextState = reduceCodexLifecycleEvent(current.state, event);
179
- const next = Object.freeze({
180
- ...current,
181
- revision: current.revision + 1,
182
- state: nextState,
183
- updatedAt: now()
184
- });
185
- writeRecord(file, next, current.revision);
186
- return Object.freeze({ path: file, revision: next.revision, state: next.state });
191
+ }
192
+ const state = reduceCodexLifecycleEvent(latest.state, event);
193
+ if (state.status === 'invalid' && state.error && NON_PERSISTENT_FAILURES.has(state.error.code)) {
194
+ throw new Error(`${state.error.code}: ${state.error.message}`);
195
+ }
196
+ return state === latest.state ? latest : Object.freeze({ ...latest, revision: latest.revision + 1, state, updatedAt: now() });
187
197
  });
198
+ return Object.freeze({ path: lifecyclePath(taskDir), revision: next.revision, state: next.state });
188
199
  }
189
200
  function findByParent(parentThreadId) {
190
- const matches = recordFiles(root)
191
- .map(readRecord)
192
- .filter((record) => record.state.startEvidence?.parentThreadId === parentThreadId
201
+ const matches = allRecords(taskDir).filter((record) => record.state.startEvidence?.parentThreadId === parentThreadId
193
202
  && ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status));
194
203
  const unconsumed = matches.filter((record) => !record.consumer);
195
204
  if (unconsumed.length > 1)
196
205
  throw new Error(`Codex lifecycle parent '${parentThreadId}' has ambiguous active children`);
197
- if (unconsumed.length === 1)
198
- return Object.freeze(unconsumed);
199
- return Object.freeze(matches
200
- .filter((record) => record.consumer && record.state.status === 'stop-ready')
201
- .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)));
206
+ return Object.freeze(unconsumed.length === 1 ? unconsumed : matches.filter((record) => record.consumer));
207
+ }
208
+ function findByTaskBinding(binding) {
209
+ return Object.freeze(allRecords(taskDir).filter((record) => record.taskBinding?.runId === binding.runId
210
+ && record.taskBinding.receiptId === binding.receiptId));
202
211
  }
203
212
  function read(childThreadId) {
204
213
  const matches = findByChild(childThreadId);
205
- if (matches.length === 0)
214
+ if (matches.length !== 1)
206
215
  throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
207
- if (matches.length > 1)
208
- throw new Error(`Codex lifecycle child '${childThreadId}' is ambiguous`);
209
- return readRecord(matches[0]);
216
+ return matches[0];
210
217
  }
211
- function consumeInternal(childThreadId, consumer, expectedHookDefinitionHash) {
218
+ function consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, taskLockHeld) {
212
219
  if (!consumer.trim())
213
220
  throw new Error('Codex lifecycle consumer is required');
214
- return withWriteLock(() => {
215
- const matches = findByChild(childThreadId);
216
- if (matches.length !== 1)
217
- throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
218
- const file = matches[0];
219
- const current = readRecord(file);
220
- if (current.consumer) {
221
- if (current.consumer !== consumer) {
222
- throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
223
- }
224
- if (expectedHookDefinitionHash
225
- && current.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash)
226
- throw new Error('Codex lifecycle hook definition hash is stale');
227
- return current;
221
+ const current = read(childThreadId);
222
+ const binding = expectedTaskBinding ?? current.taskBinding;
223
+ if (!binding)
224
+ throw new Error('Codex lifecycle task receipt binding is missing');
225
+ const spawn = current.state.spawn;
226
+ if (!spawn) {
227
+ throw new Error(taskLockHeld
228
+ ? 'Codex lifecycle task receipt binding is missing'
229
+ : 'Codex lifecycle spawn evidence is missing');
230
+ }
231
+ const key = recordKey(spawn.sessionId, spawn.turnId, spawn.toolUseId);
232
+ return update(binding, key, (latest) => {
233
+ if (!latest || expectedTaskBinding && JSON.stringify(latest.taskBinding) !== JSON.stringify(expectedTaskBinding)) {
234
+ throw new Error('Codex lifecycle task binding does not match the expected receipt');
228
235
  }
229
- if (current.state.status !== 'stop-ready')
236
+ if (latest.consumer && latest.consumer !== consumer) {
237
+ throw new Error(`Codex lifecycle evidence was already consumed by '${latest.consumer}'`);
238
+ }
239
+ if (latest.state.status !== 'stop-ready')
230
240
  throw new Error('Codex lifecycle evidence is not stop-ready');
231
- if (expectedHookDefinitionHash
232
- && current.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash)
241
+ if (expectedHookDefinitionHash && latest.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash) {
233
242
  throw new Error('Codex lifecycle hook definition hash is stale');
234
- const next = Object.freeze({
235
- ...current,
236
- revision: current.revision + 1,
237
- consumer,
238
- consumedAt: now(),
239
- updatedAt: now()
240
- });
241
- writeRecord(file, next, current.revision);
242
- return next;
243
- });
243
+ }
244
+ if (latest.consumer)
245
+ return latest;
246
+ return Object.freeze({ ...latest, revision: latest.revision + 1, consumer, consumedAt: now(), updatedAt: now() });
247
+ }, taskLockHeld);
244
248
  }
245
- function consume(childThreadId, consumer, expectedHookDefinitionHash) {
246
- if (!consumer.trim())
247
- throw new Error('Codex lifecycle consumer is required');
248
- return consumeInternal(childThreadId, consumer, expectedHookDefinitionHash);
249
+ function consume(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding) {
250
+ return consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, false);
251
+ }
252
+ function consumeWithinTaskLock(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding) {
253
+ return consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, true);
249
254
  }
250
255
  function expireBefore(cutoff) {
251
- return withWriteLock(() => {
252
- let changed = 0;
253
- for (const file of recordFiles(root)) {
254
- const current = readRecord(file);
255
- if (current.updatedAt >= cutoff)
256
- continue;
257
- if (current.consumer || ['invalid', 'expired', 'stop-ready'].includes(current.state.status)) {
258
- fs.unlinkSync(file);
259
- changed += 1;
260
- continue;
261
- }
262
- const expiredState = expireCodexLifecycleState(current.state);
263
- if (expiredState === current.state)
264
- continue;
265
- writeRecord(file, Object.freeze({
266
- ...current,
267
- revision: current.revision + 1,
268
- state: expiredState,
269
- updatedAt: now()
270
- }), current.revision);
256
+ let changed = 0;
257
+ for (const record of allRecords(taskDir)) {
258
+ if (record.updatedAt >= cutoff || !record.taskBinding?.receiptId || !record.state.spawn)
259
+ continue;
260
+ const key = recordKey(record.state.spawn.sessionId, record.state.spawn.turnId, record.state.spawn.toolUseId);
261
+ update(record.taskBinding, key, (latest) => {
262
+ if (!latest || latest.updatedAt >= cutoff || latest.consumer
263
+ || ['invalid', 'expired', 'stop-ready'].includes(latest.state.status))
264
+ return latest ?? record;
265
+ const state = expireCodexLifecycleState(latest.state);
266
+ if (state === latest.state)
267
+ return latest;
271
268
  changed += 1;
272
- }
273
- return changed;
274
- });
269
+ return Object.freeze({ ...latest, revision: latest.revision + 1, state, updatedAt: now() });
270
+ });
271
+ }
272
+ return changed;
275
273
  }
276
- return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
274
+ return Object.freeze({ taskId: options.taskId, root: path.join(taskDir, '.runtime'), apply, applyToSpawn, consume, consumeWithinTaskLock, expireBefore, findByParent, findByTaskBinding, read });
277
275
  }
278
- export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
276
+ export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence, readCodexLifecycleActivationEvidence };
279
277
  //# sourceMappingURL=store.js.map