@fitlab-ai/agent-infra 0.9.4 → 0.9.6
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +13 -3
- package/dist/lib/agent-clients/adapter.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
- package/dist/lib/agent-clients/adapters/codex.js +3 -3
- package/dist/lib/git/workflow.js +0 -1
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +300 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +56 -0
- package/dist/lib/internal/task-orchestration.js +71 -31
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +16 -16
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +318 -89
- package/dist/lib/sandbox/control/client.js +152 -22
- package/dist/lib/sandbox/control/executor.js +136 -0
- package/dist/lib/sandbox/control/lifecycle.js +348 -0
- package/dist/lib/sandbox/control/protocol.js +73 -18
- package/dist/lib/sandbox/control/server.js +235 -80
- package/dist/lib/sandbox/control/state.js +181 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +35 -24
- package/dist/lib/sandbox/recovery.js +262 -45
- package/dist/lib/sandbox/workspace-view.js +80 -11
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/process-control.js +6 -0
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/artifact-lifecycle.js +5 -4
- package/dist/lib/task/codex-orchestration.js +161 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +50 -12
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/delegation-receipts.js +25 -0
- package/dist/lib/task/events.js +10 -4
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/orchestration.js +283 -18
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/verification-engine.js +38 -6
- package/dist/lib/task/verification.js +1 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +569 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
- package/lib/agent-clients/adapters/codex.ts +3 -3
- package/lib/git/workflow.ts +0 -1
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +308 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +61 -0
- package/lib/internal/task-orchestration.ts +57 -31
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +18 -21
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +368 -103
- package/lib/sandbox/control/client.ts +173 -25
- package/lib/sandbox/control/executor.ts +163 -0
- package/lib/sandbox/control/lifecycle.ts +342 -0
- package/lib/sandbox/control/protocol.ts +110 -48
- package/lib/sandbox/control/server.ts +252 -85
- package/lib/sandbox/control/state.ts +196 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +31 -13
- package/lib/sandbox/recovery.ts +301 -60
- package/lib/sandbox/workspace-view.ts +89 -12
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/process-control.ts +11 -0
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/artifact-lifecycle.ts +5 -4
- package/lib/task/codex-orchestration.ts +233 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +75 -12
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/delegation-receipts.ts +52 -3
- package/lib/task/events.ts +9 -4
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/orchestration.ts +381 -22
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/verification-engine.ts +42 -8
- package/lib/task/verification.ts +2 -1
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/task-management.en.md +1 -0
- package/templates/.agents/rules/task-management.zh-CN.md +1 -0
- package/templates/.agents/skills/commit/SKILL.en.md +3 -9
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +50 -0
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
package/README.md
CHANGED
|
@@ -176,7 +176,7 @@ The most-used lifecycle commands, in delivery order. The command prefix varies b
|
|
|
176
176
|
| Command | Purpose |
|
|
177
177
|
|---------|---------|
|
|
178
178
|
| `create-task` / `import-issue` | Start a task from a description or a GitHub Issue |
|
|
179
|
-
| `run-task` | Resume the lifecycle with fresh isolated executors/reviewers when the selected client exposes verified actual model/effort evidence;
|
|
179
|
+
| `run-task` | Resume the lifecycle with fresh isolated executors/reviewers when the selected client exposes verified actual model/effort evidence; Codex now has an experimental Hooks + App Server evidence channel, but orchestration remains disabled until that channel is connected to delegation receipts |
|
|
180
180
|
| `analyze-task` → `review-analysis` | Capture scope and risks, then review the analysis |
|
|
181
181
|
| `plan-task` → `review-plan` | Design the approach, then review the plan |
|
|
182
182
|
| `code-task` → `review-code` | Implement and test, then run a structured code review |
|
package/README.zh-CN.md
CHANGED
|
@@ -175,7 +175,7 @@ ai agent-client configure
|
|
|
175
175
|
| 命令 | 用途 |
|
|
176
176
|
|------|------|
|
|
177
177
|
| `create-task` / `import-issue` | 从描述或 GitHub Issue 创建任务 |
|
|
178
|
-
| `run-task` | 当所选客户端能提供已验证的 actual model/effort 证据时,用 fresh 隔离 executor/reviewer
|
|
178
|
+
| `run-task` | 当所选客户端能提供已验证的 actual model/effort 证据时,用 fresh 隔离 executor/reviewer 续跑生命周期;Codex 现已具备实验性的 Hooks + App Server 证据通道,但在该通道接入 delegation receipt 前仍不会启用 orchestration |
|
|
179
179
|
| `analyze-task` → `review-analysis` | 明确范围与风险,再审查分析 |
|
|
180
180
|
| `plan-task` → `review-plan` | 设计实现路径,再审查方案 |
|
|
181
181
|
| `code-task` → `review-code` | 实现并测试,再执行结构化代码审查 |
|
package/dist/bin/internal-cli.js
CHANGED
|
@@ -8,13 +8,18 @@ const command = process.argv[2] || '';
|
|
|
8
8
|
if (process.env.AGENT_INFRA_CONTROL_TOKEN
|
|
9
9
|
&& (command === 'task-lifecycle' || command === 'task-orchestration')) {
|
|
10
10
|
const { sandboxControl } = await import('../lib/internal/sandbox-control.js');
|
|
11
|
-
sandboxControl(['client', command, ...process.argv.slice(3)]);
|
|
11
|
+
await sandboxControl(['client', command, ...process.argv.slice(3)]);
|
|
12
12
|
}
|
|
13
13
|
else
|
|
14
14
|
switch (command) {
|
|
15
|
+
case 'task-create': {
|
|
16
|
+
const { taskCreate } = await import('../lib/internal/task-create.js');
|
|
17
|
+
taskCreate(process.argv.slice(3));
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
15
20
|
case 'sandbox-control': {
|
|
16
21
|
const { sandboxControl } = await import('../lib/internal/sandbox-control.js');
|
|
17
|
-
sandboxControl(process.argv.slice(3));
|
|
22
|
+
await sandboxControl(process.argv.slice(3));
|
|
18
23
|
break;
|
|
19
24
|
}
|
|
20
25
|
case 'agent-client': {
|
|
@@ -22,6 +27,11 @@ else
|
|
|
22
27
|
agentClient(process.argv.slice(3));
|
|
23
28
|
break;
|
|
24
29
|
}
|
|
30
|
+
case 'codex-lifecycle': {
|
|
31
|
+
const { codexLifecycle } = await import('../lib/internal/codex-lifecycle.js');
|
|
32
|
+
await codexLifecycle(process.argv.slice(3));
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
25
35
|
case 'git-workflow': {
|
|
26
36
|
const { gitWorkflow } = await import('../lib/internal/git-workflow.js');
|
|
27
37
|
gitWorkflow(process.argv.slice(3));
|
|
@@ -99,7 +109,7 @@ else
|
|
|
99
109
|
}
|
|
100
110
|
case 'task-orchestration': {
|
|
101
111
|
const { taskOrchestration } = await import('../lib/internal/task-orchestration.js');
|
|
102
|
-
taskOrchestration(process.argv.slice(3));
|
|
112
|
+
await taskOrchestration(process.argv.slice(3));
|
|
103
113
|
break;
|
|
104
114
|
}
|
|
105
115
|
case 'task-review': {
|
|
@@ -190,8 +190,8 @@ function defineAgentClientAdapter(candidate) {
|
|
|
190
190
|
}
|
|
191
191
|
const evidence = candidate.delegationEvidence;
|
|
192
192
|
if (!evidence
|
|
193
|
-
|| !['host-event', 'unavailable'].includes(evidence.actualModel)
|
|
194
|
-
|| !['host-event', 'spawn-ack', 'unavailable'].includes(evidence.actualReasoningEffort)) {
|
|
193
|
+
|| !['host-event', 'app-server', 'unavailable'].includes(evidence.actualModel)
|
|
194
|
+
|| !['host-event', 'app-server', 'spawn-ack', 'unavailable'].includes(evidence.actualReasoningEffort)) {
|
|
195
195
|
throw new Error(`Agent Client '${candidate.id}' has invalid delegation evidence`);
|
|
196
196
|
}
|
|
197
197
|
if (typeof candidate.project !== 'object'
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import semver from 'semver';
|
|
7
|
+
const LIFECYCLE_HOOKS = Object.freeze([
|
|
8
|
+
Object.freeze({ event: 'PreToolUse', matcher: '^collaborationspawn_agent$', phase: 'pre-tool' }),
|
|
9
|
+
Object.freeze({ event: 'PostToolUse', matcher: '', phase: 'post-tool' }),
|
|
10
|
+
Object.freeze({ event: 'SubagentStart', matcher: '', phase: 'subagent-start' }),
|
|
11
|
+
Object.freeze({ event: 'SubagentStop', matcher: '', phase: 'subagent-stop' })
|
|
12
|
+
]);
|
|
13
|
+
const DEFAULT_ROLLOUT_READ_ATTEMPTS = 81;
|
|
14
|
+
const DEFAULT_ROLLOUT_RETRY_MS = 100;
|
|
15
|
+
function object(value) {
|
|
16
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
17
|
+
? value
|
|
18
|
+
: null;
|
|
19
|
+
}
|
|
20
|
+
function nonEmpty(value) {
|
|
21
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
22
|
+
}
|
|
23
|
+
function resolveCodexSpawnedChild(transcriptPath, expected) {
|
|
24
|
+
const name = path.basename(transcriptPath);
|
|
25
|
+
if (!transcriptPath || (!name.endsWith(`-${expected.sessionId}.jsonl`) && name !== `${expected.sessionId}.jsonl`)) {
|
|
26
|
+
throw new Error('Codex parent rollout path is invalid');
|
|
27
|
+
}
|
|
28
|
+
const stat = fs.lstatSync(transcriptPath);
|
|
29
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
30
|
+
throw new Error('Codex parent rollout is not a regular file');
|
|
31
|
+
const maxBytes = 8 * 1024 * 1024;
|
|
32
|
+
const offset = Math.max(0, stat.size - maxBytes);
|
|
33
|
+
const buffer = Buffer.alloc(stat.size - offset);
|
|
34
|
+
const descriptor = fs.openSync(transcriptPath, 'r');
|
|
35
|
+
try {
|
|
36
|
+
fs.readSync(descriptor, buffer, 0, buffer.length, offset);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
fs.closeSync(descriptor);
|
|
40
|
+
}
|
|
41
|
+
let text = buffer.toString('utf8');
|
|
42
|
+
if (offset)
|
|
43
|
+
text = text.slice(text.indexOf('\n') + 1);
|
|
44
|
+
const records = text.split(/\r?\n/).filter(Boolean).map((line) => object(JSON.parse(line))).filter(Boolean);
|
|
45
|
+
const calls = records.filter((record) => {
|
|
46
|
+
const payload = object(record?.payload);
|
|
47
|
+
return record?.type === 'response_item'
|
|
48
|
+
&& payload?.type === 'function_call'
|
|
49
|
+
&& payload.call_id === expected.toolUseId
|
|
50
|
+
&& payload.namespace === 'collaboration'
|
|
51
|
+
&& payload.name === 'spawn_agent';
|
|
52
|
+
});
|
|
53
|
+
if (calls.length !== 1)
|
|
54
|
+
throw new Error('Codex parent rollout spawn call was not found uniquely');
|
|
55
|
+
const call = object(calls[0]?.payload);
|
|
56
|
+
const args = object(JSON.parse(nonEmpty(call?.arguments) ?? '{}'));
|
|
57
|
+
if (args?.agent_type !== expected.nativeAgent
|
|
58
|
+
|| args.task_name !== expected.taskName
|
|
59
|
+
|| (expected.requestedModel && args.model !== expected.requestedModel)
|
|
60
|
+
|| (expected.requestedReasoningEffort && args.reasoning_effort !== expected.requestedReasoningEffort))
|
|
61
|
+
throw new Error('Codex parent rollout spawn identity does not match the hook event');
|
|
62
|
+
const activities = records.filter((record) => {
|
|
63
|
+
const payload = object(record?.payload);
|
|
64
|
+
return record?.type === 'event_msg'
|
|
65
|
+
&& payload?.type === 'sub_agent_activity'
|
|
66
|
+
&& payload.event_id === expected.toolUseId
|
|
67
|
+
&& payload.kind === 'started'
|
|
68
|
+
&& payload.agent_path === `/root/${expected.taskName}`
|
|
69
|
+
&& nonEmpty(payload.agent_thread_id);
|
|
70
|
+
});
|
|
71
|
+
if (activities.length !== 1)
|
|
72
|
+
throw new Error('Codex parent rollout child activity was not found uniquely');
|
|
73
|
+
return nonEmpty(object(activities[0]?.payload)?.agent_thread_id);
|
|
74
|
+
}
|
|
75
|
+
function validateCodexLifecycleHookConfig(value) {
|
|
76
|
+
const hooks = object(object(value)?.hooks);
|
|
77
|
+
const valid = hooks && LIFECYCLE_HOOKS.every(({ event, matcher, phase }) => {
|
|
78
|
+
const entries = hooks[event];
|
|
79
|
+
if (!Array.isArray(entries))
|
|
80
|
+
return false;
|
|
81
|
+
return entries.some((entryValue) => {
|
|
82
|
+
const entry = object(entryValue);
|
|
83
|
+
if (entry?.matcher !== matcher || !Array.isArray(entry.hooks))
|
|
84
|
+
return false;
|
|
85
|
+
return entry.hooks.some((hookValue) => {
|
|
86
|
+
const hook = object(hookValue);
|
|
87
|
+
return hook?.type === 'command'
|
|
88
|
+
&& hook.timeout === 15
|
|
89
|
+
&& hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`;
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
if (!valid)
|
|
94
|
+
throw new Error('CODEX_PREFLIGHT_HOOKS_INVALID: lifecycle hooks are invalid');
|
|
95
|
+
}
|
|
96
|
+
function parseCodexHooksList(value, repoRoot) {
|
|
97
|
+
const root = object(value);
|
|
98
|
+
const entries = Array.isArray(root?.data) ? root.data : [];
|
|
99
|
+
const entry = entries.map(object).find((candidate) => candidate?.cwd === repoRoot);
|
|
100
|
+
const errors = Array.isArray(entry?.errors) ? entry.errors : [];
|
|
101
|
+
if (errors.length)
|
|
102
|
+
throw new Error(`CODEX_PREFLIGHT_HOOK_DISCOVERY_FAILED: ${errors.map(String).join('; ')}`);
|
|
103
|
+
const hooks = (Array.isArray(entry?.hooks) ? entry.hooks : [])
|
|
104
|
+
.map(object)
|
|
105
|
+
.filter((hook) => hook !== null)
|
|
106
|
+
.map((hook) => Object.freeze({
|
|
107
|
+
eventName: String(hook.eventName ?? ''),
|
|
108
|
+
matcher: typeof hook.matcher === 'string' ? hook.matcher : null,
|
|
109
|
+
command: String(hook.command ?? ''),
|
|
110
|
+
enabled: hook.enabled === true
|
|
111
|
+
}));
|
|
112
|
+
const valid = LIFECYCLE_HOOKS.every(({ event, matcher, phase }) => hooks.some((hook) => hook.eventName === event.replace(/^./, (character) => character.toLowerCase())
|
|
113
|
+
&& hook.matcher === matcher
|
|
114
|
+
&& hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`
|
|
115
|
+
&& hook.enabled));
|
|
116
|
+
if (!valid)
|
|
117
|
+
throw new Error('CODEX_PREFLIGHT_HOOKS_NOT_LOADED: lifecycle hooks are not loaded for this workspace');
|
|
118
|
+
return Object.freeze(hooks);
|
|
119
|
+
}
|
|
120
|
+
function hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, identity) {
|
|
121
|
+
if (!identity || !fs.existsSync(runtimeRoot))
|
|
122
|
+
return false;
|
|
123
|
+
return fs.readdirSync(runtimeRoot)
|
|
124
|
+
.filter((name) => /^[a-f0-9]{64}\.json$/.test(name))
|
|
125
|
+
.some((name) => {
|
|
126
|
+
try {
|
|
127
|
+
const record = object(JSON.parse(fs.readFileSync(path.join(runtimeRoot, name), 'utf8')));
|
|
128
|
+
const state = object(record?.state);
|
|
129
|
+
const spawnEvent = object(state?.spawn);
|
|
130
|
+
return !['invalid', 'expired'].includes(String(state?.status))
|
|
131
|
+
&& spawnEvent?.hookDefinitionHash === hookDefinitionHash
|
|
132
|
+
&& spawnEvent.sessionId === identity.sessionId
|
|
133
|
+
&& spawnEvent.turnId === identity.turnId
|
|
134
|
+
&& spawnEvent.toolUseId === identity.toolUseId;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function parseCodexThreadResolution(readValue, rolloutRecords) {
|
|
142
|
+
const readRoot = object(readValue);
|
|
143
|
+
const thread = object(readRoot?.thread);
|
|
144
|
+
const source = object(thread?.source);
|
|
145
|
+
const subAgent = object(source?.subAgent);
|
|
146
|
+
const threadSpawn = object(subAgent?.thread_spawn);
|
|
147
|
+
const sessionMeta = rolloutRecords
|
|
148
|
+
.map(object)
|
|
149
|
+
.find((record) => record?.type === 'session_meta');
|
|
150
|
+
const sessionPayload = object(sessionMeta?.payload);
|
|
151
|
+
const turnContext = rolloutRecords
|
|
152
|
+
.map(object)
|
|
153
|
+
.filter((record) => record?.type === 'turn_context')
|
|
154
|
+
.at(-1);
|
|
155
|
+
const turnPayload = object(turnContext?.payload);
|
|
156
|
+
const childThreadId = nonEmpty(thread?.id);
|
|
157
|
+
const parentThreadId = nonEmpty(thread?.parentThreadId);
|
|
158
|
+
const sourceParentThreadId = nonEmpty(threadSpawn?.parent_thread_id);
|
|
159
|
+
const rolloutChildThreadId = nonEmpty(sessionPayload?.id);
|
|
160
|
+
const rolloutParentThreadId = nonEmpty(sessionPayload?.parent_thread_id);
|
|
161
|
+
const agentRole = nonEmpty(sessionPayload?.agent_role);
|
|
162
|
+
const model = nonEmpty(turnPayload?.model);
|
|
163
|
+
const reasoningEffort = nonEmpty(turnPayload?.effort);
|
|
164
|
+
const forkedFromId = thread?.forkedFromId;
|
|
165
|
+
if (!childThreadId
|
|
166
|
+
|| rolloutChildThreadId !== childThreadId
|
|
167
|
+
|| !parentThreadId
|
|
168
|
+
|| !sourceParentThreadId
|
|
169
|
+
|| rolloutParentThreadId !== parentThreadId
|
|
170
|
+
|| rolloutParentThreadId !== sourceParentThreadId
|
|
171
|
+
|| !/^agent-infra-lifecycle-(executor|reviewer)$/.test(agentRole ?? '')
|
|
172
|
+
|| !model
|
|
173
|
+
|| !reasoningEffort
|
|
174
|
+
|| (forkedFromId !== null && typeof forkedFromId !== 'string')) {
|
|
175
|
+
throw new Error('Codex App Server thread resolution is invalid');
|
|
176
|
+
}
|
|
177
|
+
return Object.freeze({
|
|
178
|
+
thread: Object.freeze({
|
|
179
|
+
type: 'app-thread',
|
|
180
|
+
childThreadId,
|
|
181
|
+
parentThreadId,
|
|
182
|
+
forkedFromId,
|
|
183
|
+
sourceParentThreadId,
|
|
184
|
+
nativeAgent: agentRole
|
|
185
|
+
}),
|
|
186
|
+
settings: Object.freeze({
|
|
187
|
+
type: 'app-settings',
|
|
188
|
+
childThreadId,
|
|
189
|
+
model,
|
|
190
|
+
reasoningEffort
|
|
191
|
+
})
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function readCodexRolloutRecords(readValue, childThreadId) {
|
|
195
|
+
const thread = object(object(readValue)?.thread);
|
|
196
|
+
const rolloutPath = nonEmpty(thread?.path);
|
|
197
|
+
const rolloutName = rolloutPath ? path.basename(rolloutPath) : '';
|
|
198
|
+
if (!rolloutPath || (rolloutName !== `${childThreadId}.jsonl` && !rolloutName.endsWith(`-${childThreadId}.jsonl`))) {
|
|
199
|
+
throw new Error('Codex App Server rollout path is invalid');
|
|
200
|
+
}
|
|
201
|
+
const stat = fs.statSync(rolloutPath);
|
|
202
|
+
if (!stat.isFile())
|
|
203
|
+
throw new Error('Codex App Server rollout path is not a regular file');
|
|
204
|
+
const maxBytes = 1024 * 1024;
|
|
205
|
+
const bytes = Math.min(stat.size, maxBytes);
|
|
206
|
+
const descriptor = fs.openSync(rolloutPath, 'r');
|
|
207
|
+
try {
|
|
208
|
+
const buffer = Buffer.alloc(bytes);
|
|
209
|
+
fs.readSync(descriptor, buffer, 0, bytes, 0);
|
|
210
|
+
const text = buffer.toString('utf8');
|
|
211
|
+
const completeText = stat.size <= maxBytes ? text : text.slice(0, text.lastIndexOf('\n'));
|
|
212
|
+
return Object.freeze(completeText.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line)));
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
fs.closeSync(descriptor);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function resolveCodexRolloutRecords(readValue, childThreadId, attempts, retryMs) {
|
|
219
|
+
let lastError;
|
|
220
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
221
|
+
try {
|
|
222
|
+
const records = readCodexRolloutRecords(readValue, childThreadId);
|
|
223
|
+
parseCodexThreadResolution(readValue, records);
|
|
224
|
+
return records;
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
lastError = error;
|
|
228
|
+
if (attempt + 1 < attempts)
|
|
229
|
+
await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
throw lastError;
|
|
233
|
+
}
|
|
234
|
+
function parseCodexTurnCompleted(value) {
|
|
235
|
+
const root = object(value);
|
|
236
|
+
const turn = object(root?.turn);
|
|
237
|
+
const childThreadId = nonEmpty(root?.threadId);
|
|
238
|
+
const turnId = nonEmpty(turn?.id);
|
|
239
|
+
const status = turn?.status;
|
|
240
|
+
if (!childThreadId || !turnId || !['completed', 'interrupted', 'failed'].includes(String(status))) {
|
|
241
|
+
throw new Error('Codex App Server turn completion is invalid');
|
|
242
|
+
}
|
|
243
|
+
return Object.freeze({
|
|
244
|
+
type: 'app-terminal',
|
|
245
|
+
childThreadId,
|
|
246
|
+
turnId,
|
|
247
|
+
status: status
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function parseCodexModelReroute(value) {
|
|
251
|
+
const root = object(value);
|
|
252
|
+
const childThreadId = nonEmpty(root?.threadId);
|
|
253
|
+
const turnId = nonEmpty(root?.turnId);
|
|
254
|
+
const fromModel = nonEmpty(root?.fromModel);
|
|
255
|
+
const toModel = nonEmpty(root?.toModel);
|
|
256
|
+
const reasonValue = root?.reason;
|
|
257
|
+
const reason = nonEmpty(reasonValue) ?? (object(reasonValue) ? JSON.stringify(reasonValue) : null);
|
|
258
|
+
if (!childThreadId || !turnId || !fromModel || !toModel || !reason) {
|
|
259
|
+
throw new Error('Codex App Server model reroute is invalid');
|
|
260
|
+
}
|
|
261
|
+
return Object.freeze({ type: 'app-reroute', childThreadId, turnId, fromModel, toModel, reason });
|
|
262
|
+
}
|
|
263
|
+
class CodexAppServerTransport {
|
|
264
|
+
#options;
|
|
265
|
+
#child = null;
|
|
266
|
+
#buffer = '';
|
|
267
|
+
#stderr = '';
|
|
268
|
+
#nextId = 1;
|
|
269
|
+
#pending = new Map();
|
|
270
|
+
#notifications = [];
|
|
271
|
+
constructor(options = {}) {
|
|
272
|
+
this.#options = {
|
|
273
|
+
...options,
|
|
274
|
+
command: options.command ?? 'codex',
|
|
275
|
+
args: options.args ?? ['app-server', '--stdio'],
|
|
276
|
+
timeoutMs: options.timeoutMs ?? 5_000
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
get diagnostics() {
|
|
280
|
+
return Object.freeze(this.#stderr.split(/\r?\n/).filter(Boolean));
|
|
281
|
+
}
|
|
282
|
+
get notifications() {
|
|
283
|
+
return Object.freeze([...this.#notifications]);
|
|
284
|
+
}
|
|
285
|
+
start() {
|
|
286
|
+
if (this.#child)
|
|
287
|
+
throw new Error('Codex App Server transport is already started');
|
|
288
|
+
const child = spawn(this.#options.command, [...this.#options.args], {
|
|
289
|
+
cwd: this.#options.cwd,
|
|
290
|
+
env: this.#options.env,
|
|
291
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
292
|
+
});
|
|
293
|
+
this.#child = child;
|
|
294
|
+
child.stdout.setEncoding('utf8');
|
|
295
|
+
child.stderr.setEncoding('utf8');
|
|
296
|
+
child.stdout.on('data', (chunk) => this.#onData(chunk));
|
|
297
|
+
child.stderr.on('data', (chunk) => {
|
|
298
|
+
this.#stderr = `${this.#stderr}${chunk}`.slice(-32 * 1024);
|
|
299
|
+
});
|
|
300
|
+
child.on('error', (error) => this.#rejectAll(error));
|
|
301
|
+
child.on('exit', (code, signal) => {
|
|
302
|
+
if (this.#pending.size) {
|
|
303
|
+
this.#rejectAll(new Error(`Codex App Server exited before responding (code=${code}, signal=${signal})`));
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
#onData(chunk) {
|
|
308
|
+
this.#buffer += chunk;
|
|
309
|
+
for (;;) {
|
|
310
|
+
const newline = this.#buffer.indexOf('\n');
|
|
311
|
+
if (newline < 0)
|
|
312
|
+
return;
|
|
313
|
+
const line = this.#buffer.slice(0, newline).trim();
|
|
314
|
+
this.#buffer = this.#buffer.slice(newline + 1);
|
|
315
|
+
if (!line)
|
|
316
|
+
continue;
|
|
317
|
+
let message;
|
|
318
|
+
try {
|
|
319
|
+
const parsed = object(JSON.parse(line));
|
|
320
|
+
if (!parsed)
|
|
321
|
+
throw new Error('message is not an object');
|
|
322
|
+
message = parsed;
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
this.#rejectAll(new Error(`Codex App Server returned invalid JSON: ${String(error)}`));
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (typeof message.id === 'number') {
|
|
329
|
+
const pending = this.#pending.get(message.id);
|
|
330
|
+
if (!pending)
|
|
331
|
+
continue;
|
|
332
|
+
clearTimeout(pending.timer);
|
|
333
|
+
this.#pending.delete(message.id);
|
|
334
|
+
if (message.error)
|
|
335
|
+
pending.reject(new Error(`Codex App Server request failed: ${JSON.stringify(message.error)}`));
|
|
336
|
+
else
|
|
337
|
+
pending.resolve(message.result);
|
|
338
|
+
}
|
|
339
|
+
else if (typeof message.method === 'string') {
|
|
340
|
+
this.#notifications.push(Object.freeze({ method: message.method, params: message.params }));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
#rejectAll(error) {
|
|
345
|
+
for (const [id, pending] of this.#pending) {
|
|
346
|
+
clearTimeout(pending.timer);
|
|
347
|
+
this.#pending.delete(id);
|
|
348
|
+
pending.reject(error);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
request(method, params) {
|
|
352
|
+
if (!this.#child || !this.#child.stdin.writable) {
|
|
353
|
+
return Promise.reject(new Error('Codex App Server transport is not running'));
|
|
354
|
+
}
|
|
355
|
+
const id = this.#nextId++;
|
|
356
|
+
return new Promise((resolve, reject) => {
|
|
357
|
+
const timer = setTimeout(() => {
|
|
358
|
+
this.#pending.delete(id);
|
|
359
|
+
reject(new Error(`Codex App Server request '${method}' timed out`));
|
|
360
|
+
}, this.#options.timeoutMs);
|
|
361
|
+
this.#pending.set(id, { resolve, reject, timer });
|
|
362
|
+
this.#child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
notify(method, params = {}) {
|
|
366
|
+
if (!this.#child || !this.#child.stdin.writable)
|
|
367
|
+
throw new Error('Codex App Server transport is not running');
|
|
368
|
+
this.#child.stdin.write(`${JSON.stringify({ method, params })}\n`);
|
|
369
|
+
}
|
|
370
|
+
async initialize() {
|
|
371
|
+
const result = await this.request('initialize', {
|
|
372
|
+
clientInfo: { name: 'agent-infra', title: 'agent-infra', version: '0.9.5' },
|
|
373
|
+
capabilities: null
|
|
374
|
+
});
|
|
375
|
+
this.notify('initialized');
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
close() {
|
|
379
|
+
if (!this.#child)
|
|
380
|
+
return;
|
|
381
|
+
this.#child.stdin.end();
|
|
382
|
+
this.#child.kill();
|
|
383
|
+
this.#child = null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async function resolveCodexThread(childThreadId, options = {}) {
|
|
387
|
+
if (!nonEmpty(childThreadId))
|
|
388
|
+
throw new Error('Codex child thread id is required');
|
|
389
|
+
const transport = new CodexAppServerTransport(options);
|
|
390
|
+
transport.start();
|
|
391
|
+
try {
|
|
392
|
+
await transport.initialize();
|
|
393
|
+
const readResult = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: false }));
|
|
394
|
+
const readThread = object(readResult?.thread);
|
|
395
|
+
if (nonEmpty(readThread?.id) !== childThreadId)
|
|
396
|
+
throw new Error('Codex App Server returned the wrong child thread');
|
|
397
|
+
let resolution;
|
|
398
|
+
try {
|
|
399
|
+
const rolloutRecords = await resolveCodexRolloutRecords(readResult, childThreadId, options.rolloutReadAttempts ?? DEFAULT_ROLLOUT_READ_ATTEMPTS, options.rolloutRetryMs ?? DEFAULT_ROLLOUT_RETRY_MS);
|
|
400
|
+
resolution = parseCodexThreadResolution(readResult, rolloutRecords);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
throw new Error('Codex App Server rollout metadata is unavailable');
|
|
404
|
+
}
|
|
405
|
+
const reroutes = transport.notifications
|
|
406
|
+
.filter((entry) => entry.method === 'model/rerouted')
|
|
407
|
+
.map((entry) => parseCodexModelReroute(entry.params))
|
|
408
|
+
.filter((event) => event.childThreadId === childThreadId);
|
|
409
|
+
return Object.freeze({ resolution, reroutes: Object.freeze(reroutes), diagnostics: transport.diagnostics });
|
|
410
|
+
}
|
|
411
|
+
finally {
|
|
412
|
+
try {
|
|
413
|
+
await transport.request('thread/unsubscribe', { threadId: childThreadId });
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// Closing the short-lived transport still removes the subscription.
|
|
417
|
+
}
|
|
418
|
+
transport.close();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async function resolveCodexTerminal(childThreadId, options = {}) {
|
|
422
|
+
const transport = new CodexAppServerTransport(options);
|
|
423
|
+
transport.start();
|
|
424
|
+
try {
|
|
425
|
+
await transport.initialize();
|
|
426
|
+
const result = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: true }));
|
|
427
|
+
const thread = object(result?.thread);
|
|
428
|
+
if (nonEmpty(thread?.id) !== childThreadId)
|
|
429
|
+
throw new Error('Codex App Server returned the wrong child thread');
|
|
430
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
431
|
+
const turn = turns.at(-1);
|
|
432
|
+
const turnValue = object(turn);
|
|
433
|
+
if (!turnValue || turnValue.status === 'inProgress')
|
|
434
|
+
throw new Error('CODEX_TURN_NOT_TERMINAL');
|
|
435
|
+
return parseCodexTurnCompleted({ threadId: childThreadId, turn });
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
transport.close();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async function preflightCodexLifecycleEvidence(repoRoot = process.cwd(), runtimeIdentity) {
|
|
442
|
+
const versionRun = spawnSync('codex', ['--version'], { encoding: 'utf8' });
|
|
443
|
+
const match = /codex-cli\s+(\d+\.\d+\.\d+)/.exec(versionRun.stdout ?? '');
|
|
444
|
+
if (versionRun.status !== 0 || !match?.[1] || !semver.gte(match[1], '0.147.0')) {
|
|
445
|
+
throw new Error('CODEX_PREFLIGHT_VERSION_UNSUPPORTED');
|
|
446
|
+
}
|
|
447
|
+
const featuresRun = spawnSync('codex', ['features', 'list'], { encoding: 'utf8' });
|
|
448
|
+
if (featuresRun.status !== 0
|
|
449
|
+
|| !/^hooks\s+\S+\s+true$/m.test(featuresRun.stdout)
|
|
450
|
+
|| !/^multi_agent\s+\S+\s+true$/m.test(featuresRun.stdout))
|
|
451
|
+
throw new Error('CODEX_PREFLIGHT_FEATURE_DISABLED');
|
|
452
|
+
const hooksPath = path.join(repoRoot, '.codex', 'hooks.json');
|
|
453
|
+
const hooksRaw = fs.readFileSync(hooksPath, 'utf8');
|
|
454
|
+
validateCodexLifecycleHookConfig(JSON.parse(hooksRaw));
|
|
455
|
+
const schemaDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-codex-schema-'));
|
|
456
|
+
try {
|
|
457
|
+
const schemaRun = spawnSync('codex', ['app-server', 'generate-json-schema', '--out', schemaDir], { encoding: 'utf8' });
|
|
458
|
+
if (schemaRun.status !== 0)
|
|
459
|
+
throw new Error('CODEX_PREFLIGHT_SCHEMA_UNAVAILABLE');
|
|
460
|
+
const schema = fs.readFileSync(path.join(schemaDir, 'codex_app_server_protocol.v2.schemas.json'), 'utf8');
|
|
461
|
+
for (const field of ['parentThreadId', 'forkedFromId', 'ThreadSettings', 'turn/completed', 'model/rerouted']) {
|
|
462
|
+
if (!schema.includes(field))
|
|
463
|
+
throw new Error(`CODEX_PREFLIGHT_SCHEMA_MISSING:${field}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
finally {
|
|
467
|
+
fs.rmSync(schemaDir, { recursive: true, force: true });
|
|
468
|
+
}
|
|
469
|
+
const hookDefinitionHash = crypto.createHash('sha256').update(hooksRaw).digest('hex');
|
|
470
|
+
const runtimeRoot = path.join(repoRoot, '.agents', 'workspace', '.runtime', 'codex-lifecycle');
|
|
471
|
+
const runtimeLiveness = hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, runtimeIdentity);
|
|
472
|
+
const transport = new CodexAppServerTransport();
|
|
473
|
+
transport.start();
|
|
474
|
+
let discoveredHooks;
|
|
475
|
+
try {
|
|
476
|
+
await transport.initialize();
|
|
477
|
+
discoveredHooks = parseCodexHooksList(await transport.request('hooks/list', { cwds: [repoRoot] }), repoRoot);
|
|
478
|
+
}
|
|
479
|
+
finally {
|
|
480
|
+
transport.close();
|
|
481
|
+
}
|
|
482
|
+
return Object.freeze({
|
|
483
|
+
cliVersion: match[1],
|
|
484
|
+
hookDefinitionHash,
|
|
485
|
+
staticReady: true,
|
|
486
|
+
discoveredHooks,
|
|
487
|
+
runtimeLiveness,
|
|
488
|
+
diagnostics: transport.diagnostics
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
export { CodexAppServerTransport, hasCodexRuntimeLiveness, parseCodexModelReroute, parseCodexHooksList, parseCodexThreadResolution, parseCodexTurnCompleted, resolveCodexSpawnedChild, preflightCodexLifecycleEvidence, resolveCodexTerminal, resolveCodexThread, validateCodexLifecycleHookConfig };
|
|
492
|
+
//# sourceMappingURL=app-server.js.map
|