@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.
- package/dist/bin/internal-cli.js +2 -2
- package/dist/lib/agent-clients/adapter.js +7 -1
- package/dist/lib/agent-clients/adapters/claude-code.js +5 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +78 -63
- package/dist/lib/agent-clients/adapters/codex-lifecycle/binding.js +114 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +447 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +130 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +131 -0
- package/dist/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +48 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +124 -62
- package/dist/lib/{internal/codex-sandbox-controller.js → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js} +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -213
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +468 -0
- package/dist/lib/agent-clients/adapters/codex.js +40 -1
- package/dist/lib/agent-clients/adapters/index.js +2 -10
- package/dist/lib/agent-clients/tokens.js +3 -2
- package/dist/lib/internal/agent-client.js +132 -72
- package/dist/lib/internal/cli-route-inventory.js +2 -2
- package/dist/lib/internal/sandbox-control.js +1 -81
- package/dist/lib/internal/task-finalization.js +26 -19
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +1 -1
- package/dist/lib/internal/task-validate.js +1 -1
- package/dist/lib/platform/github-client.js +33 -3
- package/dist/lib/platform/issues.js +18 -6
- package/dist/lib/runtime/agent-runtime.js +6 -29
- package/dist/lib/sandbox/commands/control-status.js +1 -0
- package/dist/lib/sandbox/commands/create.js +7 -2
- package/dist/lib/sandbox/commands/ls.js +4 -5
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/control/agent-operation.js +34 -0
- package/dist/lib/sandbox/control/client.js +14 -201
- package/dist/lib/sandbox/control/executor.js +12 -125
- package/dist/lib/sandbox/control/lifecycle.js +66 -4
- package/dist/lib/sandbox/control/protocol.js +15 -54
- package/dist/lib/sandbox/control/server.js +216 -281
- package/dist/lib/sandbox/control/task-view.js +0 -13
- package/dist/lib/sandbox/mounts.js +9 -1
- package/dist/lib/sandbox/recovery.js +31 -10
- package/dist/lib/sandbox/removal.js +28 -5
- package/dist/lib/sandbox/workspace-identity.js +18 -6
- package/dist/lib/sandbox/workspace-view.js +56 -19
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/control-authority.js +56 -264
- package/dist/lib/task/control-recovery.js +2 -3
- package/dist/lib/task/create.js +2 -2
- package/dist/lib/task/delegation-receipts.js +74 -161
- package/dist/lib/task/events.js +88 -44
- package/dist/lib/task/finalization.js +35 -6
- package/dist/lib/task/lifecycle.js +274 -115
- package/dist/lib/task/orchestration.js +201 -194
- package/dist/lib/task/runtime-paths.js +10 -0
- package/dist/lib/task/short-id.js +37 -27
- package/dist/lib/task/verification-engine.js +32 -26
- package/dist/lib/task/workspace-snapshot.js +1 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +39 -0
- package/lib/agent-clients/adapters/claude-code.ts +5 -1
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +105 -80
- package/lib/agent-clients/adapters/codex-lifecycle/binding.ts +147 -0
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +510 -0
- package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +158 -0
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +1 -1
- package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +151 -0
- package/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +43 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +8 -5
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +190 -65
- package/lib/{internal/codex-sandbox-controller.ts → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts} +2 -2
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -5
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +270 -224
- package/lib/agent-clients/adapters/codex-orchestration.ts +632 -0
- package/lib/agent-clients/adapters/codex.ts +44 -1
- package/lib/agent-clients/adapters/index.ts +7 -20
- package/lib/agent-clients/tokens.ts +4 -3
- package/lib/internal/agent-client.ts +142 -73
- package/lib/internal/cli-route-inventory.ts +2 -2
- package/lib/internal/sandbox-control.ts +0 -97
- package/lib/internal/task-finalization.ts +30 -18
- package/lib/internal/task-lifecycle.ts +1 -1
- package/lib/internal/task-operation-registry.ts +1 -1
- package/lib/internal/task-validate.ts +1 -1
- package/lib/platform/github-client.ts +34 -3
- package/lib/platform/issues.ts +18 -6
- package/lib/runtime/agent-runtime.ts +6 -41
- package/lib/sandbox/commands/control-status.ts +1 -0
- package/lib/sandbox/commands/create.ts +7 -1
- package/lib/sandbox/commands/ls.ts +4 -5
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/control/agent-operation.ts +49 -0
- package/lib/sandbox/control/client.ts +20 -254
- package/lib/sandbox/control/executor.ts +13 -136
- package/lib/sandbox/control/lifecycle.ts +67 -4
- package/lib/sandbox/control/protocol.ts +22 -66
- package/lib/sandbox/control/server.ts +21 -95
- package/lib/sandbox/control/task-view.ts +0 -19
- package/lib/sandbox/mounts.ts +10 -1
- package/lib/sandbox/recovery.ts +38 -17
- package/lib/sandbox/removal.ts +33 -7
- package/lib/sandbox/workspace-identity.ts +16 -6
- package/lib/sandbox/workspace-view.ts +50 -18
- package/lib/task/commands/ls.ts +3 -3
- package/lib/task/control-authority.ts +66 -378
- package/lib/task/control-recovery.ts +3 -4
- package/lib/task/create.ts +3 -3
- package/lib/task/delegation-receipts.ts +87 -231
- package/lib/task/events.ts +100 -42
- package/lib/task/finalization.ts +37 -7
- package/lib/task/lifecycle.ts +257 -109
- package/lib/task/orchestration.ts +260 -237
- package/lib/task/runtime-paths.ts +12 -0
- package/lib/task/short-id.ts +33 -40
- package/lib/task/verification-engine.ts +30 -25
- package/lib/task/workspace-snapshot.ts +1 -1
- package/package.json +1 -1
- package/templates/.agents/hooks/lifecycle-delegation.js +1 -25
- package/templates/.agents/rules/README.en.md +2 -1
- package/templates/.agents/rules/README.zh-CN.md +2 -1
- package/templates/.agents/rules/next-step-output.en.md +6 -19
- package/templates/.agents/rules/next-step-output.zh-CN.md +6 -19
- package/templates/.agents/rules/strategy-pattern.en.md +25 -0
- package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +0 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +0 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +5 -29
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -26
- package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +0 -546
- package/dist/lib/internal/codex-lifecycle.js +0 -385
- package/dist/lib/task/codex-orchestration.js +0 -206
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +0 -776
- package/lib/internal/codex-lifecycle.ts +0 -390
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return Object.freeze({
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
126
|
+
taskDir: string,
|
|
77
127
|
query: ActiveCodexLifecycleEvidenceQuery
|
|
78
128
|
): boolean {
|
|
79
|
-
return
|
|
80
|
-
const evidence = record.state.startEvidence;
|
|
129
|
+
return allRecords(taskDir).some((record) => {
|
|
81
130
|
return record.consumer === null
|
|
82
|
-
&& ['
|
|
83
|
-
&&
|
|
84
|
-
&&
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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):
|
|
144
|
-
return
|
|
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):
|
|
162
|
+
function locate(event: CodexLifecycleEvent): StoredCodexLifecycle {
|
|
151
163
|
if (event.type === 'hook-spawn') {
|
|
152
|
-
|
|
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 =
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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:
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
state
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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 =
|
|
238
|
-
.
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
252
|
-
|
|
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
|
|
310
|
+
function consumeWithLockMode(
|
|
257
311
|
childThreadId: string,
|
|
258
312
|
consumer: string,
|
|
259
|
-
expectedHookDefinitionHash
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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 (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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 };
|