codex-worker 0.1.1
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/LICENSE +21 -0
- package/README.md +102 -0
- package/bin/codex-worker.mjs +9 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +579 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/core/failure-classifier.d.ts +2 -0
- package/dist/src/core/failure-classifier.js +70 -0
- package/dist/src/core/failure-classifier.js.map +1 -0
- package/dist/src/core/fleet-mode.d.ts +3 -0
- package/dist/src/core/fleet-mode.js +21 -0
- package/dist/src/core/fleet-mode.js.map +1 -0
- package/dist/src/core/ids.d.ts +1 -0
- package/dist/src/core/ids.js +5 -0
- package/dist/src/core/ids.js.map +1 -0
- package/dist/src/core/markdown.d.ts +5 -0
- package/dist/src/core/markdown.js +14 -0
- package/dist/src/core/markdown.js.map +1 -0
- package/dist/src/core/model-catalog.d.ts +21 -0
- package/dist/src/core/model-catalog.js +47 -0
- package/dist/src/core/model-catalog.js.map +1 -0
- package/dist/src/core/paths.d.ts +18 -0
- package/dist/src/core/paths.js +43 -0
- package/dist/src/core/paths.js.map +1 -0
- package/dist/src/core/profile-faults.d.ts +15 -0
- package/dist/src/core/profile-faults.js +110 -0
- package/dist/src/core/profile-faults.js.map +1 -0
- package/dist/src/core/profile-manager.d.ts +25 -0
- package/dist/src/core/profile-manager.js +162 -0
- package/dist/src/core/profile-manager.js.map +1 -0
- package/dist/src/core/store.d.ts +33 -0
- package/dist/src/core/store.js +184 -0
- package/dist/src/core/store.js.map +1 -0
- package/dist/src/core/types.d.ts +97 -0
- package/dist/src/core/types.js +2 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/daemon/client.d.ts +6 -0
- package/dist/src/daemon/client.js +117 -0
- package/dist/src/daemon/client.js.map +1 -0
- package/dist/src/daemon/server.d.ts +1 -0
- package/dist/src/daemon/server.js +176 -0
- package/dist/src/daemon/server.js.map +1 -0
- package/dist/src/daemon/service.d.ts +86 -0
- package/dist/src/daemon/service.js +973 -0
- package/dist/src/daemon/service.js.map +1 -0
- package/dist/src/doctor.d.ts +1 -0
- package/dist/src/doctor.js +28 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/output.d.ts +9 -0
- package/dist/src/output.js +49 -0
- package/dist/src/output.js.map +1 -0
- package/dist/src/runtime/app-server.d.ts +44 -0
- package/dist/src/runtime/app-server.js +194 -0
- package/dist/src/runtime/app-server.js.map +1 -0
- package/dist/src/smoke.d.ts +1 -0
- package/dist/src/smoke.js +55 -0
- package/dist/src/smoke.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { classifyWorkerFailure } from '../core/failure-classifier.js';
|
|
5
|
+
import { appendFleetDeveloperInstructions } from '../core/fleet-mode.js';
|
|
6
|
+
import { buildModelCatalog, describeAllowedModels, resolveRequestedModel, } from '../core/model-catalog.js';
|
|
7
|
+
import { ensureStateRoot, logPath, transcriptPath } from '../core/paths.js';
|
|
8
|
+
import { ProfileFaultPlanner } from '../core/profile-faults.js';
|
|
9
|
+
import { ProfileManager } from '../core/profile-manager.js';
|
|
10
|
+
import { PersistentStore } from '../core/store.js';
|
|
11
|
+
import { AppServerClient, } from '../runtime/app-server.js';
|
|
12
|
+
const DEFAULT_MODEL = 'gpt-5.4';
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
14
|
+
function nowIso() {
|
|
15
|
+
return new Date().toISOString();
|
|
16
|
+
}
|
|
17
|
+
function sleep(ms) {
|
|
18
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
}
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function stringValue(value) {
|
|
24
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
25
|
+
}
|
|
26
|
+
function commandVersion(command) {
|
|
27
|
+
const result = spawnSync(command, ['--version'], { encoding: 'utf8' });
|
|
28
|
+
if (result.status !== 0) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return (result.stdout || result.stderr).trim() || null;
|
|
32
|
+
}
|
|
33
|
+
function connectionKey(cwd, codexHome) {
|
|
34
|
+
return `${cwd}::${codexHome}`;
|
|
35
|
+
}
|
|
36
|
+
function promptPreview(prompt) {
|
|
37
|
+
return prompt.trim().replace(/\s+/g, ' ').slice(0, 120);
|
|
38
|
+
}
|
|
39
|
+
function requestIdText(id) {
|
|
40
|
+
return String(id);
|
|
41
|
+
}
|
|
42
|
+
function buildTextInput(text) {
|
|
43
|
+
return [{ type: 'text', text }];
|
|
44
|
+
}
|
|
45
|
+
function parseJsonObject(value) {
|
|
46
|
+
if (!value) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const parsed = JSON.parse(value);
|
|
50
|
+
if (!isRecord(parsed)) {
|
|
51
|
+
throw new Error('Expected a JSON object.');
|
|
52
|
+
}
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
async function readTailLines(filePath, limit) {
|
|
56
|
+
try {
|
|
57
|
+
const raw = await readFile(filePath, 'utf8');
|
|
58
|
+
return raw
|
|
59
|
+
.split(/\r?\n/g)
|
|
60
|
+
.map((line) => line.trimEnd())
|
|
61
|
+
.filter((line) => line.length > 0)
|
|
62
|
+
.slice(-limit);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function readTailJson(filePath, limit) {
|
|
69
|
+
const lines = await readTailLines(filePath, limit);
|
|
70
|
+
const parsed = [];
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
try {
|
|
73
|
+
const value = JSON.parse(line);
|
|
74
|
+
if (isRecord(value)) {
|
|
75
|
+
parsed.push(value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// ignore malformed lines
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
function buildDisplayLog(recentEvents, rawLogTail) {
|
|
85
|
+
const lines = [];
|
|
86
|
+
let assistantBuffer = '';
|
|
87
|
+
const pushLine = (line) => {
|
|
88
|
+
if (line.length === 0) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (lines.at(-1) === line) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
lines.push(line);
|
|
95
|
+
};
|
|
96
|
+
const flushAssistant = () => {
|
|
97
|
+
if (!assistantBuffer) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
pushLine(assistantBuffer);
|
|
101
|
+
assistantBuffer = '';
|
|
102
|
+
};
|
|
103
|
+
for (const event of recentEvents) {
|
|
104
|
+
if (event.type === 'assistant.delta' && typeof event.delta === 'string') {
|
|
105
|
+
assistantBuffer += event.delta;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
flushAssistant();
|
|
109
|
+
if (event.type === 'user') {
|
|
110
|
+
const prompt = typeof event.prompt === 'string'
|
|
111
|
+
? event.prompt
|
|
112
|
+
: (typeof event.text === 'string' ? event.text : undefined);
|
|
113
|
+
if (prompt) {
|
|
114
|
+
pushLine(`> ${prompt}`);
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (event.type === 'request' && typeof event.method === 'string') {
|
|
119
|
+
pushLine(`request: ${event.method}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (event.type === 'notification' && event.method === 'item/completed') {
|
|
123
|
+
const params = isRecord(event.params) ? event.params : undefined;
|
|
124
|
+
const item = params && isRecord(params.item) ? params.item : undefined;
|
|
125
|
+
if (item?.type === 'agentMessage' && typeof item.text === 'string' && item.text.length > 0) {
|
|
126
|
+
assistantBuffer = '';
|
|
127
|
+
pushLine(item.text);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
flushAssistant();
|
|
132
|
+
if (lines.length > 0) {
|
|
133
|
+
return lines;
|
|
134
|
+
}
|
|
135
|
+
return rawLogTail;
|
|
136
|
+
}
|
|
137
|
+
export class CliCodexWorkerService {
|
|
138
|
+
store = new PersistentStore();
|
|
139
|
+
activeExecutions = new Map();
|
|
140
|
+
connectionFactory;
|
|
141
|
+
profileManager;
|
|
142
|
+
faultPlanner;
|
|
143
|
+
constructor(options = {}) {
|
|
144
|
+
this.connectionFactory = options.connectionFactory ?? ((cwd, codexHome) => (new AppServerClient(cwd, codexHome)));
|
|
145
|
+
}
|
|
146
|
+
async initialize() {
|
|
147
|
+
await this.store.load();
|
|
148
|
+
this.profileManager = ProfileManager.fromEnvironment(this.store.getProfiles());
|
|
149
|
+
this.faultPlanner = ProfileFaultPlanner.fromEnvironment();
|
|
150
|
+
this.store.setProfiles(this.profileManager.toPersistedState());
|
|
151
|
+
await this.store.persist();
|
|
152
|
+
}
|
|
153
|
+
async daemonStatus() {
|
|
154
|
+
const { daemonMetaPath, socketPath } = ensureStateRoot();
|
|
155
|
+
return {
|
|
156
|
+
status: 'running',
|
|
157
|
+
socketPath,
|
|
158
|
+
daemonMetaPath,
|
|
159
|
+
profiles: this.profileManager.getProfiles(),
|
|
160
|
+
activeExecutions: this.activeExecutions.size,
|
|
161
|
+
threads: this.store.listThreads().length,
|
|
162
|
+
pendingRequests: this.store.listPendingRequests('pending').length,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async shutdown() {
|
|
166
|
+
for (const execution of this.activeExecutions.values()) {
|
|
167
|
+
await execution.client.stop().catch(() => { });
|
|
168
|
+
}
|
|
169
|
+
this.activeExecutions.clear();
|
|
170
|
+
return { status: 'stopped' };
|
|
171
|
+
}
|
|
172
|
+
async threadStart(args = {}, _writer) {
|
|
173
|
+
const cwd = stringValue(args.cwd) ?? process.cwd();
|
|
174
|
+
const resolution = await this.resolveModelForCwd(cwd, stringValue(args.model));
|
|
175
|
+
const started = await this.startThreadOnHealthyProfile(cwd, resolution.resolved, {
|
|
176
|
+
developerInstructions: stringValue(args.developerInstructions),
|
|
177
|
+
baseInstructions: stringValue(args.baseInstructions),
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
thread: started.thread,
|
|
181
|
+
model: started.thread.model,
|
|
182
|
+
modelProvider: started.thread.modelProvider,
|
|
183
|
+
remappedFrom: resolution.remappedFrom,
|
|
184
|
+
actions: this.buildActions(started.thread.id),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
async threadResume(args) {
|
|
188
|
+
const threadId = stringValue(args.threadId);
|
|
189
|
+
if (!threadId) {
|
|
190
|
+
throw new Error('thread.resume requires threadId');
|
|
191
|
+
}
|
|
192
|
+
const localThread = this.resolveThread(threadId);
|
|
193
|
+
const cwd = stringValue(args.cwd);
|
|
194
|
+
const started = await this.startClientForThread(localThread, cwd);
|
|
195
|
+
try {
|
|
196
|
+
const params = {
|
|
197
|
+
threadId: localThread.id,
|
|
198
|
+
modelProvider: 'openai',
|
|
199
|
+
approvalPolicy: 'on-request',
|
|
200
|
+
sandbox: 'workspace-write',
|
|
201
|
+
persistExtendedHistory: false,
|
|
202
|
+
};
|
|
203
|
+
let remappedFrom;
|
|
204
|
+
if (stringValue(args.model)) {
|
|
205
|
+
const resolution = await this.resolveModelForCwd(cwd ?? localThread.cwd, stringValue(args.model));
|
|
206
|
+
params.model = resolution.resolved;
|
|
207
|
+
remappedFrom = resolution.remappedFrom;
|
|
208
|
+
}
|
|
209
|
+
if (cwd) {
|
|
210
|
+
params.cwd = cwd;
|
|
211
|
+
}
|
|
212
|
+
if (stringValue(args.developerInstructions)) {
|
|
213
|
+
params.developerInstructions = appendFleetDeveloperInstructions(stringValue(args.developerInstructions));
|
|
214
|
+
}
|
|
215
|
+
const response = await started.client.request('thread/resume', params);
|
|
216
|
+
const remoteThread = isRecord(response.thread) ? response.thread : {};
|
|
217
|
+
const thread = this.store.upsertThread({
|
|
218
|
+
...localThread,
|
|
219
|
+
cwd: stringValue(remoteThread.cwd) ?? cwd ?? localThread.cwd,
|
|
220
|
+
model: stringValue(response.model) ?? params.model ?? localThread.model,
|
|
221
|
+
modelProvider: stringValue(response.modelProvider) ?? 'openai',
|
|
222
|
+
updatedAt: nowIso(),
|
|
223
|
+
});
|
|
224
|
+
await this.persistProfiles();
|
|
225
|
+
return {
|
|
226
|
+
thread,
|
|
227
|
+
model: thread.model,
|
|
228
|
+
modelProvider: thread.modelProvider,
|
|
229
|
+
remappedFrom,
|
|
230
|
+
actions: this.buildActions(thread.id),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
await started.client.stop().catch(() => { });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async threadRead(args) {
|
|
238
|
+
const threadId = stringValue(args.threadId);
|
|
239
|
+
if (!threadId) {
|
|
240
|
+
throw new Error('thread.read requires threadId');
|
|
241
|
+
}
|
|
242
|
+
const localThread = this.resolveThread(threadId);
|
|
243
|
+
let remoteThread;
|
|
244
|
+
try {
|
|
245
|
+
const started = await this.startClientForThread(localThread);
|
|
246
|
+
try {
|
|
247
|
+
const response = await started.client.request('thread/read', {
|
|
248
|
+
threadId: localThread.id,
|
|
249
|
+
includeTurns: typeof args.includeTurns === 'boolean' ? args.includeTurns : true,
|
|
250
|
+
});
|
|
251
|
+
remoteThread = isRecord(response.thread) ? response.thread : undefined;
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
await started.client.stop().catch(() => { });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
remoteThread = undefined;
|
|
259
|
+
}
|
|
260
|
+
const tailLines = typeof args.tailLines === 'number' && Number.isFinite(args.tailLines)
|
|
261
|
+
? Math.max(1, Math.min(200, Math.trunc(args.tailLines)))
|
|
262
|
+
: 20;
|
|
263
|
+
const transcriptFilePath = transcriptPath(localThread.cwd, localThread.id);
|
|
264
|
+
const logFilePath = logPath(localThread.cwd, localThread.id);
|
|
265
|
+
const recentEvents = await readTailJson(transcriptFilePath, tailLines);
|
|
266
|
+
const rawLogTail = await readTailLines(logFilePath, tailLines);
|
|
267
|
+
return {
|
|
268
|
+
thread: remoteThread ?? localThread,
|
|
269
|
+
localThread,
|
|
270
|
+
turns: this.store.listTurns(localThread.id),
|
|
271
|
+
pendingRequests: this.store.listPendingRequests('pending').filter((entry) => entry.threadId === localThread.id),
|
|
272
|
+
artifacts: {
|
|
273
|
+
transcriptPath: transcriptFilePath,
|
|
274
|
+
logPath: logFilePath,
|
|
275
|
+
recentEvents,
|
|
276
|
+
logTail: rawLogTail,
|
|
277
|
+
displayLog: buildDisplayLog(recentEvents, rawLogTail),
|
|
278
|
+
},
|
|
279
|
+
actions: this.buildActions(localThread.id),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
async threadList(_args = {}) {
|
|
283
|
+
return {
|
|
284
|
+
data: this.store.listThreads(),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
async run(args, writer) {
|
|
288
|
+
const cwd = stringValue(args.cwd);
|
|
289
|
+
const content = stringValue(args.content);
|
|
290
|
+
const inputFilePath = stringValue(args.inputFilePath);
|
|
291
|
+
if (!cwd || !content || !inputFilePath) {
|
|
292
|
+
throw new Error('run requires cwd, content, and inputFilePath');
|
|
293
|
+
}
|
|
294
|
+
const resolution = await this.resolveModelForCwd(cwd, stringValue(args.model));
|
|
295
|
+
const started = await this.startThreadOnHealthyProfile(cwd, resolution.resolved, {
|
|
296
|
+
developerInstructions: [
|
|
297
|
+
'you are codex-worker.',
|
|
298
|
+
'execute the file-backed task exactly and avoid unrelated work.',
|
|
299
|
+
].join(' '),
|
|
300
|
+
taskPrompt: content,
|
|
301
|
+
});
|
|
302
|
+
const execution = await this.launchTurn({
|
|
303
|
+
client: started.client,
|
|
304
|
+
thread: started.thread,
|
|
305
|
+
source: 'alias/run',
|
|
306
|
+
alias: 'run',
|
|
307
|
+
prompt: content,
|
|
308
|
+
inputFilePath,
|
|
309
|
+
timeoutMs: typeof args.timeoutMs === 'number' ? args.timeoutMs : DEFAULT_TIMEOUT_MS,
|
|
310
|
+
writer,
|
|
311
|
+
startTurn: async () => await started.client.request('turn/start', {
|
|
312
|
+
threadId: started.thread.id,
|
|
313
|
+
model: started.thread.model,
|
|
314
|
+
input: buildTextInput(content),
|
|
315
|
+
}),
|
|
316
|
+
});
|
|
317
|
+
if (Boolean(args.async)) {
|
|
318
|
+
return {
|
|
319
|
+
...this.turnPayload(started.thread.id, execution.turn.id, execution.job.id),
|
|
320
|
+
remappedFrom: resolution.remappedFrom,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const visible = await execution.visible;
|
|
324
|
+
return {
|
|
325
|
+
...visible,
|
|
326
|
+
remappedFrom: resolution.remappedFrom,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async send(args, writer) {
|
|
330
|
+
const threadId = stringValue(args.threadId);
|
|
331
|
+
const content = stringValue(args.content);
|
|
332
|
+
const inputFilePath = stringValue(args.inputFilePath);
|
|
333
|
+
if (!threadId || !content || !inputFilePath) {
|
|
334
|
+
throw new Error('send requires threadId, content, and inputFilePath');
|
|
335
|
+
}
|
|
336
|
+
const thread = this.resolveThread(threadId);
|
|
337
|
+
const started = await this.startClientForThread(thread);
|
|
338
|
+
await started.client.request('thread/resume', {
|
|
339
|
+
threadId: thread.id,
|
|
340
|
+
modelProvider: 'openai',
|
|
341
|
+
approvalPolicy: 'on-request',
|
|
342
|
+
sandbox: 'workspace-write',
|
|
343
|
+
persistExtendedHistory: false,
|
|
344
|
+
});
|
|
345
|
+
const execution = await this.launchTurn({
|
|
346
|
+
client: started.client,
|
|
347
|
+
thread,
|
|
348
|
+
source: 'alias/send',
|
|
349
|
+
alias: 'send',
|
|
350
|
+
prompt: content,
|
|
351
|
+
inputFilePath,
|
|
352
|
+
timeoutMs: typeof args.timeoutMs === 'number' ? args.timeoutMs : DEFAULT_TIMEOUT_MS,
|
|
353
|
+
writer,
|
|
354
|
+
startTurn: async () => await started.client.request('turn/start', {
|
|
355
|
+
threadId: thread.id,
|
|
356
|
+
model: thread.model,
|
|
357
|
+
input: buildTextInput(content),
|
|
358
|
+
}),
|
|
359
|
+
});
|
|
360
|
+
if (Boolean(args.async)) {
|
|
361
|
+
return this.turnPayload(thread.id, execution.turn.id, execution.job.id);
|
|
362
|
+
}
|
|
363
|
+
return await execution.visible;
|
|
364
|
+
}
|
|
365
|
+
async turnStart(args, writer) {
|
|
366
|
+
return await this.send({
|
|
367
|
+
threadId: stringValue(args.threadId),
|
|
368
|
+
content: stringValue(args.prompt) ?? stringValue(args.content) ?? '',
|
|
369
|
+
inputFilePath: stringValue(args.inputFilePath) ?? 'prompt.md',
|
|
370
|
+
async: Boolean(args.async),
|
|
371
|
+
timeoutMs: typeof args.timeoutMs === 'number' ? args.timeoutMs : undefined,
|
|
372
|
+
}, writer);
|
|
373
|
+
}
|
|
374
|
+
async turnSteer(args, writer) {
|
|
375
|
+
const threadId = stringValue(args.threadId);
|
|
376
|
+
const expectedTurnId = stringValue(args.expectedTurnId);
|
|
377
|
+
if (!threadId || !expectedTurnId) {
|
|
378
|
+
throw new Error('turn.steer requires threadId and expectedTurnId');
|
|
379
|
+
}
|
|
380
|
+
const thread = this.resolveThread(threadId);
|
|
381
|
+
const prompt = stringValue(args.prompt) ?? stringValue(args.content) ?? '';
|
|
382
|
+
const started = await this.startClientForThread(thread);
|
|
383
|
+
await started.client.request('thread/resume', {
|
|
384
|
+
threadId: thread.id,
|
|
385
|
+
modelProvider: 'openai',
|
|
386
|
+
approvalPolicy: 'on-request',
|
|
387
|
+
sandbox: 'workspace-write',
|
|
388
|
+
persistExtendedHistory: false,
|
|
389
|
+
});
|
|
390
|
+
const execution = await this.launchTurn({
|
|
391
|
+
client: started.client,
|
|
392
|
+
thread,
|
|
393
|
+
source: 'turn/steer',
|
|
394
|
+
alias: 'send',
|
|
395
|
+
prompt,
|
|
396
|
+
inputFilePath: stringValue(args.inputFilePath) ?? 'prompt.md',
|
|
397
|
+
timeoutMs: typeof args.timeoutMs === 'number' ? args.timeoutMs : DEFAULT_TIMEOUT_MS,
|
|
398
|
+
writer,
|
|
399
|
+
startTurn: async () => await started.client.request('turn/steer', {
|
|
400
|
+
threadId: thread.id,
|
|
401
|
+
expectedTurnId,
|
|
402
|
+
model: thread.model,
|
|
403
|
+
input: buildTextInput(prompt),
|
|
404
|
+
}),
|
|
405
|
+
});
|
|
406
|
+
if (Boolean(args.async)) {
|
|
407
|
+
return this.turnPayload(thread.id, execution.turn.id, execution.job.id);
|
|
408
|
+
}
|
|
409
|
+
return await execution.visible;
|
|
410
|
+
}
|
|
411
|
+
async turnInterrupt(args) {
|
|
412
|
+
const threadId = stringValue(args.threadId);
|
|
413
|
+
if (!threadId) {
|
|
414
|
+
throw new Error('turn.interrupt requires threadId');
|
|
415
|
+
}
|
|
416
|
+
const execution = this.activeExecutions.get(threadId);
|
|
417
|
+
if (!execution) {
|
|
418
|
+
throw new Error(`Thread ${threadId} is not active.`);
|
|
419
|
+
}
|
|
420
|
+
await execution.client.request('turn/interrupt', {
|
|
421
|
+
threadId: execution.threadId,
|
|
422
|
+
...(stringValue(args.turnId) ? { turnId: stringValue(args.turnId) } : {}),
|
|
423
|
+
});
|
|
424
|
+
return {
|
|
425
|
+
threadId: execution.threadId,
|
|
426
|
+
turnId: execution.turnId,
|
|
427
|
+
status: 'interrupt_requested',
|
|
428
|
+
actions: this.buildActions(execution.threadId),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
async modelList(_args = {}) {
|
|
432
|
+
const catalog = await this.loadModelCatalog(process.cwd());
|
|
433
|
+
return {
|
|
434
|
+
data: catalog.visibleModelIds,
|
|
435
|
+
aliases: catalog.aliasMappings,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
async accountRead(args = {}) {
|
|
439
|
+
return await this.requestHealthyProfile('account/read', args);
|
|
440
|
+
}
|
|
441
|
+
async accountRateLimits(args = {}) {
|
|
442
|
+
return await this.requestHealthyProfile('account/rateLimits/read', args);
|
|
443
|
+
}
|
|
444
|
+
async skillsList(args = {}) {
|
|
445
|
+
return await this.requestHealthyProfile('skills/list', args);
|
|
446
|
+
}
|
|
447
|
+
async appList(args = {}) {
|
|
448
|
+
return await this.requestHealthyProfile('app/list', args);
|
|
449
|
+
}
|
|
450
|
+
async requestList(args = {}) {
|
|
451
|
+
return {
|
|
452
|
+
data: this.store.listPendingRequests(args.status),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
async requestRead(args) {
|
|
456
|
+
const requestId = String(args.requestId ?? '');
|
|
457
|
+
const request = this.store.getPendingRequest(requestId);
|
|
458
|
+
if (!request) {
|
|
459
|
+
throw new Error(`Pending request not found: ${requestId}`);
|
|
460
|
+
}
|
|
461
|
+
return { request };
|
|
462
|
+
}
|
|
463
|
+
async requestRespond(args) {
|
|
464
|
+
const requestId = String(args.requestId ?? '');
|
|
465
|
+
const request = this.store.getPendingRequest(requestId);
|
|
466
|
+
if (!request || request.status !== 'pending') {
|
|
467
|
+
throw new Error(`Pending request not found: ${requestId}`);
|
|
468
|
+
}
|
|
469
|
+
const execution = request.threadId ? this.activeExecutions.get(request.threadId) : undefined;
|
|
470
|
+
if (!execution) {
|
|
471
|
+
throw new Error(`Thread ${request.threadId ?? 'unknown'} is not waiting on a live request.`);
|
|
472
|
+
}
|
|
473
|
+
const payload = parseJsonObject(stringValue(args.json)) ?? this.buildRequestPayload(request, {
|
|
474
|
+
decision: (typeof args.decision === 'string' || isRecord(args.decision)) ? args.decision : undefined,
|
|
475
|
+
answer: stringValue(args.answer),
|
|
476
|
+
questionId: stringValue(args.questionId),
|
|
477
|
+
});
|
|
478
|
+
await execution.client.respond(request.requestId, payload);
|
|
479
|
+
this.store.resolvePendingRequest(request.id, payload);
|
|
480
|
+
this.store.updateThread(execution.threadId, { status: 'running' });
|
|
481
|
+
this.store.updateTurn(execution.turnId, { status: 'running' });
|
|
482
|
+
this.store.updateJob(execution.jobId, { status: 'running' });
|
|
483
|
+
await this.persistProfiles();
|
|
484
|
+
return {
|
|
485
|
+
status: 'responded',
|
|
486
|
+
requestId: request.id,
|
|
487
|
+
threadId: request.threadId ?? null,
|
|
488
|
+
actions: this.buildActions(execution.threadId),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
async wait(args, _writer) {
|
|
492
|
+
const timeoutMs = typeof args.timeoutMs === 'number' ? args.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
493
|
+
const started = Date.now();
|
|
494
|
+
while (Date.now() - started <= timeoutMs) {
|
|
495
|
+
if (stringValue(args.jobId)) {
|
|
496
|
+
const job = this.store.getJob(stringValue(args.jobId));
|
|
497
|
+
if (!job) {
|
|
498
|
+
throw new Error(`Job not found: ${String(args.jobId)}`);
|
|
499
|
+
}
|
|
500
|
+
if (job.status !== 'running') {
|
|
501
|
+
return this.turnPayload(job.threadId, job.turnId, job.id);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else if (stringValue(args.threadId)) {
|
|
505
|
+
const thread = this.resolveThread(stringValue(args.threadId));
|
|
506
|
+
if (thread.status !== 'running') {
|
|
507
|
+
return {
|
|
508
|
+
threadId: thread.id,
|
|
509
|
+
turnId: thread.latestTurnId ?? stringValue(args.turnId) ?? null,
|
|
510
|
+
status: thread.status,
|
|
511
|
+
actions: this.buildActions(thread.id),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
throw new Error('wait requires --job-id or --thread-id');
|
|
517
|
+
}
|
|
518
|
+
await sleep(500);
|
|
519
|
+
}
|
|
520
|
+
throw new Error('Timed out waiting for the requested Codex operation.');
|
|
521
|
+
}
|
|
522
|
+
async doctor() {
|
|
523
|
+
return {
|
|
524
|
+
node: process.version,
|
|
525
|
+
codex: commandVersion('codex'),
|
|
526
|
+
mcpc: commandVersion('mcpc'),
|
|
527
|
+
cwd: process.cwd(),
|
|
528
|
+
stateRoot: ensureStateRoot().rootDir,
|
|
529
|
+
profiles: this.profileManager.getProfiles(),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
async read(args) {
|
|
533
|
+
const threadId = stringValue(args.threadId);
|
|
534
|
+
if (!threadId) {
|
|
535
|
+
throw new Error('read requires threadId');
|
|
536
|
+
}
|
|
537
|
+
return await this.threadRead({
|
|
538
|
+
threadId,
|
|
539
|
+
includeTurns: true,
|
|
540
|
+
tailLines: typeof args.tailLines === 'number' ? args.tailLines : undefined,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
async writeDaemonMeta(socketPath, token) {
|
|
544
|
+
await writeFile(ensureStateRoot().daemonMetaPath, JSON.stringify({
|
|
545
|
+
pid: process.pid,
|
|
546
|
+
socketPath,
|
|
547
|
+
token,
|
|
548
|
+
startedAt: nowIso(),
|
|
549
|
+
}, null, 2));
|
|
550
|
+
}
|
|
551
|
+
async startThreadOnHealthyProfile(cwd, model, options = {}) {
|
|
552
|
+
const profiles = this.profileManager.getCandidateProfiles();
|
|
553
|
+
if (profiles.length === 0) {
|
|
554
|
+
throw new Error('No eligible CODEX_HOME directories are available.');
|
|
555
|
+
}
|
|
556
|
+
for (const profile of profiles) {
|
|
557
|
+
const injectedFault = this.faultPlanner.takeFault(profile);
|
|
558
|
+
if (injectedFault) {
|
|
559
|
+
this.profileManager.markFailure(profile.id, injectedFault.category, injectedFault.message ?? 'Injected profile fault');
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const client = this.connectionFactory(cwd, profile.codexHome);
|
|
563
|
+
try {
|
|
564
|
+
await client.start();
|
|
565
|
+
const response = await client.request('thread/start', {
|
|
566
|
+
cwd,
|
|
567
|
+
model,
|
|
568
|
+
modelProvider: 'openai',
|
|
569
|
+
approvalPolicy: 'on-request',
|
|
570
|
+
sandbox: 'workspace-write',
|
|
571
|
+
baseInstructions: options.baseInstructions,
|
|
572
|
+
developerInstructions: appendFleetDeveloperInstructions(options.developerInstructions)
|
|
573
|
+
?? (options.taskPrompt ? appendFleetDeveloperInstructions(options.taskPrompt) : undefined),
|
|
574
|
+
experimentalRawEvents: false,
|
|
575
|
+
persistExtendedHistory: false,
|
|
576
|
+
});
|
|
577
|
+
const remoteThread = isRecord(response.thread) ? response.thread : undefined;
|
|
578
|
+
if (!remoteThread || typeof remoteThread.id !== 'string') {
|
|
579
|
+
throw new Error('thread/start did not return a thread id.');
|
|
580
|
+
}
|
|
581
|
+
const timestamp = nowIso();
|
|
582
|
+
const thread = this.store.upsertThread({
|
|
583
|
+
id: remoteThread.id,
|
|
584
|
+
cwd: stringValue(remoteThread.cwd) ?? cwd,
|
|
585
|
+
codexHome: profile.codexHome,
|
|
586
|
+
model: stringValue(response.model) ?? model,
|
|
587
|
+
modelProvider: stringValue(response.modelProvider) ?? 'openai',
|
|
588
|
+
status: this.mapThreadStatus(remoteThread.status),
|
|
589
|
+
createdAt: timestamp,
|
|
590
|
+
updatedAt: timestamp,
|
|
591
|
+
});
|
|
592
|
+
await this.persistProfiles();
|
|
593
|
+
return { profile, client, thread };
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
const failure = classifyWorkerFailure(error);
|
|
597
|
+
this.profileManager.markFailure(profile.id, failure.category, failure.message);
|
|
598
|
+
await client.stop().catch(() => { });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
throw new Error('Unable to start thread on any configured CODEX_HOME.');
|
|
602
|
+
}
|
|
603
|
+
async startClientForThread(thread, cwdOverride) {
|
|
604
|
+
const profiles = this.prioritizedProfilesForThread(thread);
|
|
605
|
+
for (const profile of profiles) {
|
|
606
|
+
const client = this.connectionFactory(cwdOverride ?? thread.cwd, profile.codexHome);
|
|
607
|
+
try {
|
|
608
|
+
await client.start();
|
|
609
|
+
return { client, profile };
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
const failure = classifyWorkerFailure(error);
|
|
613
|
+
this.profileManager.markFailure(profile.id, failure.category, failure.message);
|
|
614
|
+
await client.stop().catch(() => { });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
throw new Error(`Unable to start Codex app-server for thread ${thread.id}.`);
|
|
618
|
+
}
|
|
619
|
+
async launchTurn(input) {
|
|
620
|
+
const response = await input.startTurn();
|
|
621
|
+
const turn = isRecord(response.turn) ? response.turn : undefined;
|
|
622
|
+
const turnId = stringValue(turn?.id);
|
|
623
|
+
if (!turnId) {
|
|
624
|
+
throw new Error('turn/start did not return a turn id.');
|
|
625
|
+
}
|
|
626
|
+
const timestamp = nowIso();
|
|
627
|
+
const turnRecord = this.store.upsertTurn({
|
|
628
|
+
id: turnId,
|
|
629
|
+
threadId: input.thread.id,
|
|
630
|
+
status: stringValue(turn?.status) ?? 'running',
|
|
631
|
+
source: input.source,
|
|
632
|
+
inputFilePath: input.inputFilePath,
|
|
633
|
+
promptPreview: promptPreview(input.prompt),
|
|
634
|
+
startedAt: timestamp,
|
|
635
|
+
});
|
|
636
|
+
const job = this.store.createJob({
|
|
637
|
+
alias: input.alias,
|
|
638
|
+
threadId: input.thread.id,
|
|
639
|
+
turnId,
|
|
640
|
+
status: 'running',
|
|
641
|
+
inputFilePath: input.inputFilePath,
|
|
642
|
+
outputLogPath: logPath(input.thread.cwd, input.thread.id),
|
|
643
|
+
});
|
|
644
|
+
this.store.updateThread(input.thread.id, {
|
|
645
|
+
latestTurnId: turnId,
|
|
646
|
+
status: 'running',
|
|
647
|
+
lastError: undefined,
|
|
648
|
+
});
|
|
649
|
+
let execution;
|
|
650
|
+
const visible = new Promise((resolve, reject) => {
|
|
651
|
+
const currentExecution = {
|
|
652
|
+
threadId: input.thread.id,
|
|
653
|
+
turnId,
|
|
654
|
+
jobId: job.id,
|
|
655
|
+
source: input.source,
|
|
656
|
+
connectionKey: connectionKey(input.thread.cwd, input.thread.codexHome),
|
|
657
|
+
cwd: input.thread.cwd,
|
|
658
|
+
codexHome: input.thread.codexHome,
|
|
659
|
+
client: input.client,
|
|
660
|
+
writer: input.writer,
|
|
661
|
+
settled: false,
|
|
662
|
+
detach: () => { },
|
|
663
|
+
resolve,
|
|
664
|
+
reject,
|
|
665
|
+
};
|
|
666
|
+
execution = currentExecution;
|
|
667
|
+
this.activeExecutions.set(input.thread.id, currentExecution);
|
|
668
|
+
const onNotification = (notification) => {
|
|
669
|
+
void this.handleNotification(currentExecution, notification);
|
|
670
|
+
};
|
|
671
|
+
const onServerRequest = (request) => {
|
|
672
|
+
void this.handleServerRequest(currentExecution, request);
|
|
673
|
+
};
|
|
674
|
+
const onExit = () => {
|
|
675
|
+
void this.failExecution(currentExecution, new Error('Codex app-server exited before the turn finished.'));
|
|
676
|
+
};
|
|
677
|
+
currentExecution.detach = () => {
|
|
678
|
+
input.client.off?.('notification', onNotification);
|
|
679
|
+
input.client.off?.('serverRequest', onServerRequest);
|
|
680
|
+
input.client.off?.('exit', onExit);
|
|
681
|
+
};
|
|
682
|
+
input.client.on('notification', onNotification);
|
|
683
|
+
input.client.on('serverRequest', onServerRequest);
|
|
684
|
+
input.client.on('exit', onExit);
|
|
685
|
+
setTimeout(() => {
|
|
686
|
+
if (!currentExecution.settled) {
|
|
687
|
+
void this.failExecution(currentExecution, new Error(`Timed out after ${input.timeoutMs}ms.`));
|
|
688
|
+
}
|
|
689
|
+
}, input.timeoutMs).unref();
|
|
690
|
+
});
|
|
691
|
+
try {
|
|
692
|
+
await this.store.appendThreadEvent({
|
|
693
|
+
cwd: input.thread.cwd,
|
|
694
|
+
threadId: input.thread.id,
|
|
695
|
+
payload: {
|
|
696
|
+
type: 'user',
|
|
697
|
+
turnId,
|
|
698
|
+
prompt: input.prompt,
|
|
699
|
+
source: input.source,
|
|
700
|
+
},
|
|
701
|
+
logLine: `> ${input.prompt}`,
|
|
702
|
+
});
|
|
703
|
+
await this.persistProfiles();
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
await this.failExecution(execution, error instanceof Error ? error : new Error(String(error)));
|
|
707
|
+
}
|
|
708
|
+
return { turn: turnRecord, job, visible };
|
|
709
|
+
}
|
|
710
|
+
async handleNotification(execution, notification) {
|
|
711
|
+
const params = isRecord(notification.params) ? notification.params : {};
|
|
712
|
+
execution.writer?.event(notification.method, params);
|
|
713
|
+
if (notification.method === 'item/agentMessage/delta') {
|
|
714
|
+
const delta = stringValue(params.delta);
|
|
715
|
+
if (delta) {
|
|
716
|
+
await this.store.appendThreadEvent({
|
|
717
|
+
cwd: execution.cwd,
|
|
718
|
+
threadId: execution.threadId,
|
|
719
|
+
payload: { type: 'assistant.delta', delta, turnId: execution.turnId },
|
|
720
|
+
logLine: delta,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (notification.method === 'item/commandExecution/outputDelta' || notification.method === 'item/fileChange/outputDelta') {
|
|
726
|
+
const delta = stringValue(params.delta);
|
|
727
|
+
if (delta) {
|
|
728
|
+
await this.store.appendThreadEvent({
|
|
729
|
+
cwd: execution.cwd,
|
|
730
|
+
threadId: execution.threadId,
|
|
731
|
+
payload: { type: notification.method, delta, turnId: execution.turnId },
|
|
732
|
+
logLine: delta,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (notification.method === 'account/rateLimits/updated') {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (notification.method === 'thread/status/changed') {
|
|
741
|
+
const thread = isRecord(params.thread) ? params.thread : undefined;
|
|
742
|
+
const status = this.mapThreadStatus(thread?.status ?? params.status);
|
|
743
|
+
this.store.updateThread(execution.threadId, { status });
|
|
744
|
+
await this.persistProfiles();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (notification.method === 'turn/completed') {
|
|
748
|
+
const turn = isRecord(params.turn) ? params.turn : undefined;
|
|
749
|
+
const status = stringValue(turn?.status) ?? 'completed';
|
|
750
|
+
if (status === 'completed') {
|
|
751
|
+
await this.completeExecution(execution, 'completed');
|
|
752
|
+
}
|
|
753
|
+
else if (status === 'interrupted') {
|
|
754
|
+
await this.completeExecution(execution, 'interrupted');
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
const message = isRecord(turn?.error) ? stringValue(turn.error.message) : undefined;
|
|
758
|
+
await this.failExecution(execution, new Error(message ?? `Turn finished with status ${status}.`));
|
|
759
|
+
}
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (notification.method === 'error') {
|
|
763
|
+
await this.failExecution(execution, new Error(stringValue(params.message) ?? 'Codex reported an error.'));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
await this.store.appendThreadEvent({
|
|
767
|
+
cwd: execution.cwd,
|
|
768
|
+
threadId: execution.threadId,
|
|
769
|
+
payload: { type: 'notification', method: notification.method, params },
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
async handleServerRequest(execution, request) {
|
|
773
|
+
const params = isRecord(request.params) ? request.params : {};
|
|
774
|
+
const pending = this.store.createPendingRequest({
|
|
775
|
+
requestId: requestIdText(request.id),
|
|
776
|
+
method: request.method,
|
|
777
|
+
threadId: execution.threadId,
|
|
778
|
+
turnId: execution.turnId,
|
|
779
|
+
connectionKey: execution.connectionKey,
|
|
780
|
+
codexHome: execution.codexHome,
|
|
781
|
+
cwd: execution.cwd,
|
|
782
|
+
params,
|
|
783
|
+
});
|
|
784
|
+
this.store.updateThread(execution.threadId, { status: 'waiting_request' });
|
|
785
|
+
this.store.updateTurn(execution.turnId, { status: 'waiting_request' });
|
|
786
|
+
this.store.updateJob(execution.jobId, { status: 'waiting_request' });
|
|
787
|
+
await this.store.appendThreadEvent({
|
|
788
|
+
cwd: execution.cwd,
|
|
789
|
+
threadId: execution.threadId,
|
|
790
|
+
payload: { type: 'request', requestId: pending.id, method: request.method, params },
|
|
791
|
+
logLine: `request: ${request.method}`,
|
|
792
|
+
});
|
|
793
|
+
await this.persistProfiles();
|
|
794
|
+
if (!execution.settled) {
|
|
795
|
+
execution.settled = true;
|
|
796
|
+
execution.resolve(this.turnPayload(execution.threadId, execution.turnId, execution.jobId));
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async completeExecution(execution, status) {
|
|
800
|
+
const shouldResolve = !execution.settled;
|
|
801
|
+
execution.settled = true;
|
|
802
|
+
execution.detach();
|
|
803
|
+
this.activeExecutions.delete(execution.threadId);
|
|
804
|
+
this.store.updateTurn(execution.turnId, {
|
|
805
|
+
status,
|
|
806
|
+
completedAt: nowIso(),
|
|
807
|
+
});
|
|
808
|
+
this.store.updateJob(execution.jobId, {
|
|
809
|
+
status: status === 'completed' ? 'completed' : 'interrupted',
|
|
810
|
+
});
|
|
811
|
+
this.store.updateThread(execution.threadId, {
|
|
812
|
+
status: status === 'completed' ? 'idle' : 'interrupted',
|
|
813
|
+
lastError: undefined,
|
|
814
|
+
});
|
|
815
|
+
await this.persistProfiles();
|
|
816
|
+
await execution.client.stop().catch(() => { });
|
|
817
|
+
if (shouldResolve) {
|
|
818
|
+
execution.resolve(this.turnPayload(execution.threadId, execution.turnId, execution.jobId));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
async failExecution(execution, error) {
|
|
822
|
+
if (execution.settled) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
execution.settled = true;
|
|
826
|
+
execution.detach();
|
|
827
|
+
this.activeExecutions.delete(execution.threadId);
|
|
828
|
+
this.store.updateTurn(execution.turnId, {
|
|
829
|
+
status: 'failed',
|
|
830
|
+
completedAt: nowIso(),
|
|
831
|
+
error: error.message,
|
|
832
|
+
});
|
|
833
|
+
this.store.updateJob(execution.jobId, {
|
|
834
|
+
status: 'failed',
|
|
835
|
+
error: error.message,
|
|
836
|
+
});
|
|
837
|
+
this.store.updateThread(execution.threadId, {
|
|
838
|
+
status: 'failed',
|
|
839
|
+
lastError: error.message,
|
|
840
|
+
});
|
|
841
|
+
await this.persistProfiles();
|
|
842
|
+
await execution.client.stop().catch(() => { });
|
|
843
|
+
execution.reject(error);
|
|
844
|
+
}
|
|
845
|
+
buildRequestPayload(request, args) {
|
|
846
|
+
if (isRecord(args.decision)) {
|
|
847
|
+
return args.decision;
|
|
848
|
+
}
|
|
849
|
+
if (request.method === 'item/tool/requestUserInput') {
|
|
850
|
+
const questionId = args.questionId
|
|
851
|
+
?? (Array.isArray(request.params.questions) && isRecord(request.params.questions[0])
|
|
852
|
+
? stringValue(request.params.questions[0].id)
|
|
853
|
+
: undefined)
|
|
854
|
+
?? 'q1';
|
|
855
|
+
return {
|
|
856
|
+
answers: {
|
|
857
|
+
[questionId]: {
|
|
858
|
+
answers: [args.answer ?? ''],
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
return {
|
|
864
|
+
decision: args.decision ?? 'accept',
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
async resolveModelForCwd(cwd, requestedModel) {
|
|
868
|
+
const catalog = await this.loadModelCatalog(cwd);
|
|
869
|
+
const resolution = resolveRequestedModel(requestedModel ?? DEFAULT_MODEL, catalog);
|
|
870
|
+
if (!resolution) {
|
|
871
|
+
throw new Error(`Unsupported model "${requestedModel ?? DEFAULT_MODEL}". ${describeAllowedModels(catalog)}`);
|
|
872
|
+
}
|
|
873
|
+
return resolution;
|
|
874
|
+
}
|
|
875
|
+
async loadModelCatalog(cwd) {
|
|
876
|
+
const models = [];
|
|
877
|
+
const profiles = this.profileManager.getCandidateProfiles();
|
|
878
|
+
for (const profile of profiles) {
|
|
879
|
+
const client = this.connectionFactory(cwd, profile.codexHome);
|
|
880
|
+
try {
|
|
881
|
+
await client.start();
|
|
882
|
+
const response = await client.request('model/list', { includeHidden: true });
|
|
883
|
+
const data = Array.isArray(response.data) ? response.data : [];
|
|
884
|
+
for (const rawEntry of data) {
|
|
885
|
+
if (!isRecord(rawEntry) || typeof rawEntry.id !== 'string') {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
models.push({
|
|
889
|
+
id: rawEntry.id,
|
|
890
|
+
hidden: Boolean(rawEntry.hidden),
|
|
891
|
+
upgrade: stringValue(rawEntry.upgrade) ?? null,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
const failure = classifyWorkerFailure(error);
|
|
897
|
+
this.profileManager.markFailure(profile.id, failure.category, failure.message);
|
|
898
|
+
}
|
|
899
|
+
finally {
|
|
900
|
+
await client.stop().catch(() => { });
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (models.length === 0) {
|
|
904
|
+
throw new Error('No selectable Codex models are available from the configured CODEX_HOME directories.');
|
|
905
|
+
}
|
|
906
|
+
return buildModelCatalog(models);
|
|
907
|
+
}
|
|
908
|
+
async requestHealthyProfile(method, params) {
|
|
909
|
+
const profiles = this.profileManager.getCandidateProfiles();
|
|
910
|
+
for (const profile of profiles) {
|
|
911
|
+
const client = this.connectionFactory(process.cwd(), profile.codexHome);
|
|
912
|
+
try {
|
|
913
|
+
await client.start();
|
|
914
|
+
return await client.request(method, params);
|
|
915
|
+
}
|
|
916
|
+
catch (error) {
|
|
917
|
+
const failure = classifyWorkerFailure(error);
|
|
918
|
+
this.profileManager.markFailure(profile.id, failure.category, failure.message);
|
|
919
|
+
}
|
|
920
|
+
finally {
|
|
921
|
+
await client.stop().catch(() => { });
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
throw new Error(`Unable to execute ${method} on any configured CODEX_HOME.`);
|
|
925
|
+
}
|
|
926
|
+
prioritizedProfilesForThread(thread) {
|
|
927
|
+
const profiles = this.profileManager.getCandidateProfiles();
|
|
928
|
+
const preferred = profiles.find((profile) => profile.codexHome === thread.codexHome);
|
|
929
|
+
return preferred
|
|
930
|
+
? [preferred, ...profiles.filter((profile) => profile.codexHome !== preferred.codexHome)]
|
|
931
|
+
: profiles;
|
|
932
|
+
}
|
|
933
|
+
resolveThread(threadId) {
|
|
934
|
+
const thread = this.store.getThread(threadId);
|
|
935
|
+
if (!thread) {
|
|
936
|
+
throw new Error(`Thread not found: ${threadId}`);
|
|
937
|
+
}
|
|
938
|
+
return thread;
|
|
939
|
+
}
|
|
940
|
+
turnPayload(threadId, turnId, jobId) {
|
|
941
|
+
const thread = this.resolveThread(threadId);
|
|
942
|
+
const turn = this.store.getTurn(turnId);
|
|
943
|
+
const job = this.store.getJob(jobId);
|
|
944
|
+
return {
|
|
945
|
+
threadId,
|
|
946
|
+
turnId,
|
|
947
|
+
status: turn?.status ?? job?.status ?? thread.status,
|
|
948
|
+
thread,
|
|
949
|
+
turn,
|
|
950
|
+
job,
|
|
951
|
+
pendingRequests: this.store.listPendingRequests('pending').filter((entry) => entry.threadId === threadId),
|
|
952
|
+
actions: this.buildActions(threadId),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
buildActions(threadId) {
|
|
956
|
+
return {
|
|
957
|
+
read: `codex-worker read ${threadId}`,
|
|
958
|
+
send: `codex-worker send ${threadId} prompt.md`,
|
|
959
|
+
requests: 'codex-worker request list',
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
mapThreadStatus(value) {
|
|
963
|
+
if (isRecord(value)) {
|
|
964
|
+
return stringValue(value.type) ?? 'unknown';
|
|
965
|
+
}
|
|
966
|
+
return stringValue(value) ?? 'unknown';
|
|
967
|
+
}
|
|
968
|
+
async persistProfiles() {
|
|
969
|
+
this.store.setProfiles(this.profileManager.toPersistedState());
|
|
970
|
+
await this.store.persist();
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
//# sourceMappingURL=service.js.map
|