remote-codex 0.1.5 → 0.1.7
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/apps/supervisor-api/dist/index.js +7749 -5501
- package/apps/supervisor-web/dist/assets/{highlighted-body-OFNGDK62-D-RjOTTL.js → highlighted-body-OFNGDK62-0cYcfOfd.js} +1 -1
- package/apps/supervisor-web/dist/assets/index-CbIt0KnL.css +32 -0
- package/apps/supervisor-web/dist/assets/index-nH6a8Wwn.js +377 -0
- package/apps/supervisor-web/dist/assets/{xterm-D8iZbRww.js → xterm-DisVWgDR.js} +1 -1
- package/apps/supervisor-web/dist/index.html +2 -2
- package/package.json +5 -1
- package/packages/agent-runtime/src/index.ts +2 -0
- package/packages/agent-runtime/src/registry.ts +44 -0
- package/packages/agent-runtime/src/types.ts +531 -0
- package/packages/codex/src/appServerManager.test.ts +328 -0
- package/packages/codex/src/appServerManager.ts +656 -0
- package/packages/codex/src/historyItems.ts +1185 -0
- package/packages/codex/src/hookHistory.ts +224 -0
- package/packages/codex/src/index.ts +6 -0
- package/packages/codex/src/jsonrpc.test.ts +58 -0
- package/packages/codex/src/jsonrpc.ts +198 -0
- package/packages/codex/src/requestMapper.test.ts +127 -0
- package/packages/codex/src/requestMapper.ts +511 -0
- package/packages/codex/src/runtimeAdapter.ts +692 -0
- package/packages/codex/src/types.ts +403 -0
- package/packages/db/migrations/0014_thread_history_items.sql +12 -0
- package/packages/db/migrations/0015_agent_provider_fields.sql +14 -0
- package/packages/db/migrations/0016_remove_codex_thread_goal_id.sql +46 -0
- package/packages/db/migrations/0017_remove_codex_thread_columns.sql +85 -0
- package/packages/db/src/client.ts +53 -0
- package/packages/db/src/index.ts +5 -0
- package/packages/db/src/migrate.test.ts +36 -0
- package/packages/db/src/migrate.ts +84 -0
- package/packages/db/src/repositories.ts +893 -0
- package/packages/db/src/schema.ts +177 -0
- package/packages/db/src/seed.ts +51 -0
- package/packages/shared/src/index.ts +878 -0
- package/scripts/service-manager.mjs +6 -4
- package/apps/supervisor-web/dist/assets/index-CdG3ogmZ.js +0 -376
- package/apps/supervisor-web/dist/assets/index-QM8NQf3e.css +0 -32
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
|
|
4
|
+
import { JsonRpcClient, JsonRpcClientError } from './jsonrpc';
|
|
5
|
+
import {
|
|
6
|
+
AppServerStatusSnapshot,
|
|
7
|
+
CodexClientInfo,
|
|
8
|
+
CodexHookTrustInput,
|
|
9
|
+
CodexHooksListEntry,
|
|
10
|
+
CodexHookRecord,
|
|
11
|
+
CodexMcpServerRecord,
|
|
12
|
+
CodexModelRecord,
|
|
13
|
+
CodexServerRequest,
|
|
14
|
+
CodexServerEvent,
|
|
15
|
+
CodexSkillsListEntry,
|
|
16
|
+
CodexThreadGoalRecord,
|
|
17
|
+
CodexThreadRecord,
|
|
18
|
+
CodexTurnRecord,
|
|
19
|
+
ReasoningEffort,
|
|
20
|
+
ThreadGoalSetInput,
|
|
21
|
+
ThreadForkInput,
|
|
22
|
+
ThreadRollbackInput,
|
|
23
|
+
ThreadResumeInput,
|
|
24
|
+
ThreadStartInput,
|
|
25
|
+
TurnStartInput,
|
|
26
|
+
TurnSteerInput
|
|
27
|
+
} from './types';
|
|
28
|
+
|
|
29
|
+
interface SpawnedChild {
|
|
30
|
+
stdout: NodeJS.ReadableStream;
|
|
31
|
+
stdin: NodeJS.WritableStream;
|
|
32
|
+
stderr: NodeJS.ReadableStream;
|
|
33
|
+
once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
|
34
|
+
once(event: 'error', listener: (error: Error) => void): this;
|
|
35
|
+
kill(signal?: NodeJS.Signals): boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CodexAppServerManagerOptions {
|
|
39
|
+
command: string;
|
|
40
|
+
startupTimeoutMs: number;
|
|
41
|
+
clientInfo: CodexClientInfo;
|
|
42
|
+
maxRestarts?: number;
|
|
43
|
+
spawnProcess?: (command: string, args: string[]) => SpawnedChild;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mapThread(record: any): CodexThreadRecord {
|
|
47
|
+
return {
|
|
48
|
+
id: record.id,
|
|
49
|
+
preview: record.preview ?? '',
|
|
50
|
+
createdAt: record.createdAt,
|
|
51
|
+
updatedAt: record.updatedAt,
|
|
52
|
+
status: record.status,
|
|
53
|
+
cwd: record.cwd,
|
|
54
|
+
name: record.name ?? null,
|
|
55
|
+
turns: Array.isArray(record.turns) ? record.turns.map(mapTurn) : []
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mapTurn(record: any): CodexTurnRecord {
|
|
60
|
+
return {
|
|
61
|
+
id: record.id,
|
|
62
|
+
status: record.status,
|
|
63
|
+
error: record.error ?? null,
|
|
64
|
+
items: Array.isArray(record.items) ? record.items : []
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function mapModel(record: any): CodexModelRecord {
|
|
69
|
+
return {
|
|
70
|
+
id: record.id,
|
|
71
|
+
model: record.model,
|
|
72
|
+
displayName: record.displayName,
|
|
73
|
+
description: record.description,
|
|
74
|
+
hidden: record.hidden,
|
|
75
|
+
isDefault: record.isDefault,
|
|
76
|
+
supportedReasoningEfforts: Array.isArray(record.supportedReasoningEfforts)
|
|
77
|
+
? record.supportedReasoningEfforts.map((entry: any) => ({
|
|
78
|
+
reasoningEffort: entry.reasoningEffort,
|
|
79
|
+
description: entry.description
|
|
80
|
+
}))
|
|
81
|
+
: [],
|
|
82
|
+
defaultReasoningEffort: record.defaultReasoningEffort ?? 'medium'
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function mapSkillsListEntry(record: any): CodexSkillsListEntry {
|
|
87
|
+
return {
|
|
88
|
+
cwd: record.cwd,
|
|
89
|
+
skills: Array.isArray(record.skills)
|
|
90
|
+
? record.skills.map((skill: any) => ({
|
|
91
|
+
name: skill.name,
|
|
92
|
+
description: skill.description ?? '',
|
|
93
|
+
shortDescription: skill.shortDescription ?? null,
|
|
94
|
+
interface: skill.interface
|
|
95
|
+
? {
|
|
96
|
+
displayName: skill.interface.displayName ?? null,
|
|
97
|
+
shortDescription: skill.interface.shortDescription ?? null,
|
|
98
|
+
brandColor: skill.interface.brandColor ?? null,
|
|
99
|
+
defaultPrompt: skill.interface.defaultPrompt ?? null,
|
|
100
|
+
}
|
|
101
|
+
: null,
|
|
102
|
+
path: skill.path,
|
|
103
|
+
scope: skill.scope,
|
|
104
|
+
enabled: skill.enabled === true,
|
|
105
|
+
}))
|
|
106
|
+
: [],
|
|
107
|
+
errors: Array.isArray(record.errors)
|
|
108
|
+
? record.errors.map((error: any) => ({
|
|
109
|
+
path: error.path,
|
|
110
|
+
message: error.message,
|
|
111
|
+
}))
|
|
112
|
+
: [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function mapMcpServer(record: any): CodexMcpServerRecord {
|
|
117
|
+
const tools = record.tools ?? {};
|
|
118
|
+
return {
|
|
119
|
+
name: record.name,
|
|
120
|
+
authStatus: record.authStatus ?? record.auth_status ?? 'unsupported',
|
|
121
|
+
tools: Object.values(tools).map((tool: any) => ({
|
|
122
|
+
name: tool.name,
|
|
123
|
+
title: tool.title ?? null,
|
|
124
|
+
description: tool.description ?? null,
|
|
125
|
+
})),
|
|
126
|
+
resourceCount: Array.isArray(record.resources) ? record.resources.length : 0,
|
|
127
|
+
resourceTemplateCount: Array.isArray(record.resourceTemplates)
|
|
128
|
+
? record.resourceTemplates.length
|
|
129
|
+
: 0,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function numberFromProtocolInteger(value: unknown, fallback = 0) {
|
|
134
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
if (typeof value === 'bigint') {
|
|
138
|
+
return Number(value);
|
|
139
|
+
}
|
|
140
|
+
if (typeof value === 'string') {
|
|
141
|
+
const numericValue = Number(value);
|
|
142
|
+
return Number.isFinite(numericValue) ? numericValue : fallback;
|
|
143
|
+
}
|
|
144
|
+
return fallback;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function mapHook(record: any): CodexHookRecord {
|
|
148
|
+
return {
|
|
149
|
+
key: record.key,
|
|
150
|
+
eventName: record.eventName,
|
|
151
|
+
handlerType: record.handlerType,
|
|
152
|
+
matcher: record.matcher ?? null,
|
|
153
|
+
command: record.command ?? null,
|
|
154
|
+
timeoutSec: numberFromProtocolInteger(record.timeoutSec ?? record.timeout_sec, 600),
|
|
155
|
+
statusMessage: record.statusMessage ?? record.status_message ?? null,
|
|
156
|
+
sourcePath: String(record.sourcePath ?? record.source_path ?? ''),
|
|
157
|
+
source: record.source ?? 'unknown',
|
|
158
|
+
pluginId: record.pluginId ?? record.plugin_id ?? null,
|
|
159
|
+
displayOrder: numberFromProtocolInteger(record.displayOrder ?? record.display_order),
|
|
160
|
+
enabled: record.enabled === true,
|
|
161
|
+
isManaged: record.isManaged === true || record.is_managed === true,
|
|
162
|
+
currentHash: record.currentHash ?? record.current_hash ?? '',
|
|
163
|
+
trustStatus: record.trustStatus ?? record.trust_status ?? 'untrusted',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function mapHooksListEntry(record: any): CodexHooksListEntry {
|
|
168
|
+
return {
|
|
169
|
+
cwd: record.cwd,
|
|
170
|
+
hooks: Array.isArray(record.hooks) ? record.hooks.map(mapHook) : [],
|
|
171
|
+
warnings: Array.isArray(record.warnings) ? record.warnings.map(String) : [],
|
|
172
|
+
errors: Array.isArray(record.errors)
|
|
173
|
+
? record.errors.map((error: any) => ({
|
|
174
|
+
path: String(error.path ?? ''),
|
|
175
|
+
message: String(error.message ?? ''),
|
|
176
|
+
}))
|
|
177
|
+
: [],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseGoalTimestamp(value: unknown): number {
|
|
182
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (typeof value === 'string') {
|
|
187
|
+
const numericValue = Number(value);
|
|
188
|
+
if (Number.isFinite(numericValue) && value.trim() !== '') {
|
|
189
|
+
return numericValue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const parsed = Date.parse(value);
|
|
193
|
+
if (Number.isFinite(parsed)) {
|
|
194
|
+
return Math.floor(parsed / 1000);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return Math.floor(Date.now() / 1000);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function mapThreadGoal(record: any): CodexThreadGoalRecord {
|
|
202
|
+
return {
|
|
203
|
+
threadId: record.threadId,
|
|
204
|
+
objective: record.objective,
|
|
205
|
+
status: record.status,
|
|
206
|
+
tokenBudget: record.tokenBudget ?? null,
|
|
207
|
+
tokensUsed: record.tokensUsed ?? 0,
|
|
208
|
+
timeUsedSeconds: record.timeUsedSeconds ?? 0,
|
|
209
|
+
createdAt: parseGoalTimestamp(record.createdAt),
|
|
210
|
+
updatedAt: parseGoalTimestamp(record.updatedAt),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export class CodexAppServerManager extends EventEmitter {
|
|
215
|
+
private readonly maxRestarts: number;
|
|
216
|
+
private readonly spawnProcess: (command: string, args: string[]) => SpawnedChild;
|
|
217
|
+
private process: SpawnedChild | null = null;
|
|
218
|
+
private client: JsonRpcClient | null = null;
|
|
219
|
+
private readonly intentionallyStopping = new Set<SpawnedChild>();
|
|
220
|
+
private status: AppServerStatusSnapshot = {
|
|
221
|
+
state: 'stopped',
|
|
222
|
+
transport: 'stdio',
|
|
223
|
+
lastStartedAt: null,
|
|
224
|
+
lastError: null,
|
|
225
|
+
restartCount: 0
|
|
226
|
+
};
|
|
227
|
+
private startPromise: Promise<void> | null = null;
|
|
228
|
+
private intentionalStop = false;
|
|
229
|
+
|
|
230
|
+
constructor(private readonly options: CodexAppServerManagerOptions) {
|
|
231
|
+
super();
|
|
232
|
+
this.maxRestarts = options.maxRestarts ?? 3;
|
|
233
|
+
this.spawnProcess =
|
|
234
|
+
options.spawnProcess ??
|
|
235
|
+
((command: string, args: string[]) =>
|
|
236
|
+
spawn(command, args, {
|
|
237
|
+
stdio: 'pipe'
|
|
238
|
+
}) as unknown as ChildProcessWithoutNullStreams);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getStatus(): AppServerStatusSnapshot {
|
|
242
|
+
return { ...this.status };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async start(): Promise<void> {
|
|
246
|
+
if (this.status.state === 'ready') {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (this.startPromise) {
|
|
251
|
+
return this.startPromise;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.intentionalStop = false;
|
|
255
|
+
this.setStatus('starting', null);
|
|
256
|
+
|
|
257
|
+
this.startPromise = this.doStart().finally(() => {
|
|
258
|
+
this.startPromise = null;
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return this.startPromise;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async stop(): Promise<void> {
|
|
265
|
+
this.intentionalStop = true;
|
|
266
|
+
const client = this.client;
|
|
267
|
+
const process = this.process;
|
|
268
|
+
|
|
269
|
+
client?.close();
|
|
270
|
+
if (this.client === client) {
|
|
271
|
+
this.client = null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (process) {
|
|
275
|
+
this.intentionallyStopping.add(process);
|
|
276
|
+
process.kill('SIGTERM');
|
|
277
|
+
if (this.process === process) {
|
|
278
|
+
this.process = null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this.setStatus('stopped', null);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async ensureReady(): Promise<void> {
|
|
286
|
+
if (this.status.state !== 'ready') {
|
|
287
|
+
await this.start();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (this.status.state !== 'ready' || !this.client) {
|
|
291
|
+
throw new JsonRpcClientError(
|
|
292
|
+
this.status.lastError ?? 'Codex app-server is unavailable.',
|
|
293
|
+
'app_server_unavailable'
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async listModels(): Promise<CodexModelRecord[]> {
|
|
299
|
+
await this.ensureReady();
|
|
300
|
+
const response = await this.client!.request<{ data: any[] }>('model/list', {
|
|
301
|
+
includeHidden: false
|
|
302
|
+
});
|
|
303
|
+
return response.data.map(mapModel);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async listThreads(): Promise<CodexThreadRecord[]> {
|
|
307
|
+
await this.ensureReady();
|
|
308
|
+
const response = await this.client!.request<{ data: any[] }>('thread/list', {
|
|
309
|
+
archived: false
|
|
310
|
+
});
|
|
311
|
+
return response.data.map(mapThread);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async listLoadedThreads(): Promise<string[]> {
|
|
315
|
+
await this.ensureReady();
|
|
316
|
+
const response = await this.client!.request<{ data: string[] }>('thread/loaded/list', {});
|
|
317
|
+
return response.data;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async listSkills(input: { cwds?: string[]; forceReload?: boolean } = {}) {
|
|
321
|
+
await this.ensureReady();
|
|
322
|
+
const response = await this.client!.request<{ data: any[] }>('skills/list', {
|
|
323
|
+
...(input.cwds && input.cwds.length > 0 ? { cwds: input.cwds } : {}),
|
|
324
|
+
...(input.forceReload !== undefined ? { forceReload: input.forceReload } : {}),
|
|
325
|
+
});
|
|
326
|
+
return response.data.map(mapSkillsListEntry);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async listMcpServers() {
|
|
330
|
+
await this.ensureReady();
|
|
331
|
+
const servers: CodexMcpServerRecord[] = [];
|
|
332
|
+
let cursor: string | null = null;
|
|
333
|
+
|
|
334
|
+
do {
|
|
335
|
+
const response: {
|
|
336
|
+
data: any[];
|
|
337
|
+
nextCursor?: string | null;
|
|
338
|
+
next_cursor?: string | null;
|
|
339
|
+
} = await this.client!.request('mcpServerStatus/list', {
|
|
340
|
+
cursor,
|
|
341
|
+
limit: 100,
|
|
342
|
+
detail: 'full',
|
|
343
|
+
});
|
|
344
|
+
servers.push(...response.data.map(mapMcpServer));
|
|
345
|
+
cursor = response.nextCursor ?? response.next_cursor ?? null;
|
|
346
|
+
} while (cursor);
|
|
347
|
+
|
|
348
|
+
return servers;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async listHooks(input: { cwds?: string[] } = {}) {
|
|
352
|
+
await this.ensureReady();
|
|
353
|
+
const response = await this.client!.request<{ data: any[] }>('hooks/list', {
|
|
354
|
+
...(input.cwds && input.cwds.length > 0 ? { cwds: input.cwds } : {}),
|
|
355
|
+
});
|
|
356
|
+
return response.data.map(mapHooksListEntry);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async setHookTrust(input: CodexHookTrustInput) {
|
|
360
|
+
await this.ensureReady();
|
|
361
|
+
await this.client!.request('config/batchWrite', {
|
|
362
|
+
edits: [
|
|
363
|
+
{
|
|
364
|
+
keyPath: 'hooks.state',
|
|
365
|
+
value: {
|
|
366
|
+
[input.key]: {
|
|
367
|
+
trusted_hash: input.trustedHash ?? '',
|
|
368
|
+
...(input.trustedHash ? { enabled: true } : {}),
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
mergeStrategy: 'upsert',
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
reloadUserConfig: true,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async startThread(input: ThreadStartInput) {
|
|
379
|
+
await this.ensureReady();
|
|
380
|
+
const response = await this.client!.request<{ thread: any; model: string; reasoningEffort?: ReasoningEffort | null; sandbox?: string | null }>('thread/start', {
|
|
381
|
+
cwd: input.cwd,
|
|
382
|
+
model: input.model,
|
|
383
|
+
serviceTier: input.serviceTier,
|
|
384
|
+
approvalPolicy: input.approvalPolicy,
|
|
385
|
+
sandbox: input.sandbox ?? null,
|
|
386
|
+
experimentalRawEvents: false,
|
|
387
|
+
persistExtendedHistory: true
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
thread: mapThread(response.thread),
|
|
392
|
+
model: response.model,
|
|
393
|
+
reasoningEffort: response.reasoningEffort ?? null,
|
|
394
|
+
sandbox: response.sandbox ?? null,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async readThread(threadId: string) {
|
|
399
|
+
await this.ensureReady();
|
|
400
|
+
const response = await this.client!.request<{ thread: any }>('thread/read', {
|
|
401
|
+
threadId,
|
|
402
|
+
includeTurns: true
|
|
403
|
+
});
|
|
404
|
+
return mapThread(response.thread);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async resumeThread(input: ThreadResumeInput) {
|
|
408
|
+
await this.ensureReady();
|
|
409
|
+
const response = await this.client!.request<{ thread: any; model: string; reasoningEffort?: ReasoningEffort | null; sandbox?: string | null }>('thread/resume', {
|
|
410
|
+
threadId: input.threadId,
|
|
411
|
+
model: input.model ?? null,
|
|
412
|
+
serviceTier: input.serviceTier,
|
|
413
|
+
sandbox: input.sandbox ?? null,
|
|
414
|
+
persistExtendedHistory: true
|
|
415
|
+
});
|
|
416
|
+
return {
|
|
417
|
+
thread: mapThread(response.thread),
|
|
418
|
+
model: response.model,
|
|
419
|
+
reasoningEffort: response.reasoningEffort ?? null,
|
|
420
|
+
sandbox: response.sandbox ?? null,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async forkThread(input: ThreadForkInput) {
|
|
425
|
+
await this.ensureReady();
|
|
426
|
+
const response = await this.client!.request<{ thread: any }>('thread/fork', {
|
|
427
|
+
threadId: input.threadId,
|
|
428
|
+
});
|
|
429
|
+
return mapThread(response.thread);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async rollbackThread(input: ThreadRollbackInput) {
|
|
433
|
+
await this.ensureReady();
|
|
434
|
+
const response = await this.client!.request<{ thread: any }>('thread/rollback', {
|
|
435
|
+
threadId: input.threadId,
|
|
436
|
+
count: input.count,
|
|
437
|
+
});
|
|
438
|
+
return mapThread(response.thread);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async startTurn(input: TurnStartInput) {
|
|
442
|
+
await this.ensureReady();
|
|
443
|
+
const response = await this.client!.request<{ turn: any }>('turn/start', {
|
|
444
|
+
threadId: input.threadId,
|
|
445
|
+
input: [
|
|
446
|
+
{
|
|
447
|
+
type: 'text',
|
|
448
|
+
text: input.prompt,
|
|
449
|
+
text_elements: []
|
|
450
|
+
}
|
|
451
|
+
],
|
|
452
|
+
model: input.model ?? null,
|
|
453
|
+
serviceTier:
|
|
454
|
+
input.serviceTier === undefined ? undefined : input.serviceTier,
|
|
455
|
+
effort: input.effort ?? null,
|
|
456
|
+
sandboxPolicy: input.sandboxPolicy ?? null,
|
|
457
|
+
collaborationMode: input.collaborationMode
|
|
458
|
+
? {
|
|
459
|
+
mode: input.collaborationMode,
|
|
460
|
+
settings: {
|
|
461
|
+
model: input.model ?? '',
|
|
462
|
+
reasoning_effort: input.effort ?? null,
|
|
463
|
+
developer_instructions: null
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
: null
|
|
467
|
+
});
|
|
468
|
+
return mapTurn(response.turn);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async steerTurn(input: TurnSteerInput) {
|
|
472
|
+
await this.ensureReady();
|
|
473
|
+
const response = await this.client!.request<{ turn?: any }>('turn/steer', {
|
|
474
|
+
threadId: input.threadId,
|
|
475
|
+
expectedTurnId: input.turnId,
|
|
476
|
+
input: [
|
|
477
|
+
{
|
|
478
|
+
type: 'text',
|
|
479
|
+
text: input.prompt,
|
|
480
|
+
text_elements: []
|
|
481
|
+
}
|
|
482
|
+
]
|
|
483
|
+
});
|
|
484
|
+
return response.turn ? mapTurn(response.turn) : null;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async compactThread(threadId: string) {
|
|
488
|
+
await this.ensureReady();
|
|
489
|
+
await this.client!.request<unknown>('thread/compact/start', {
|
|
490
|
+
threadId,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async getThreadGoal(threadId: string) {
|
|
495
|
+
await this.ensureReady();
|
|
496
|
+
const response = await this.client!.request<{ goal: any | null }>('thread/goal/get', {
|
|
497
|
+
threadId,
|
|
498
|
+
});
|
|
499
|
+
return response.goal ? mapThreadGoal(response.goal) : null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async setThreadGoal(input: ThreadGoalSetInput) {
|
|
503
|
+
await this.ensureReady();
|
|
504
|
+
const response = await this.client!.request<{ goal: any }>('thread/goal/set', {
|
|
505
|
+
threadId: input.threadId,
|
|
506
|
+
...(input.objective !== undefined ? { objective: input.objective } : {}),
|
|
507
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
508
|
+
...(input.tokenBudget !== undefined ? { tokenBudget: input.tokenBudget } : {}),
|
|
509
|
+
});
|
|
510
|
+
return mapThreadGoal(response.goal);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async clearThreadGoal(threadId: string) {
|
|
514
|
+
await this.ensureReady();
|
|
515
|
+
const response = await this.client!.request<{ cleared: boolean }>('thread/goal/clear', {
|
|
516
|
+
threadId,
|
|
517
|
+
});
|
|
518
|
+
return response.cleared;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async setExperimentalFeatureEnablement(enablement: Record<string, boolean>) {
|
|
522
|
+
await this.ensureReady();
|
|
523
|
+
await this.client!.request<unknown>('experimentalFeature/enablement/set', {
|
|
524
|
+
enablement,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async interruptTurn(threadId: string, turnId: string) {
|
|
529
|
+
await this.ensureReady();
|
|
530
|
+
const response = await this.client!.request<{ turn?: any }>('turn/interrupt', {
|
|
531
|
+
threadId,
|
|
532
|
+
turnId
|
|
533
|
+
});
|
|
534
|
+
return response.turn ? mapTurn(response.turn) : null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
respondToServerRequest(id: number, result: unknown) {
|
|
538
|
+
if (!this.client) {
|
|
539
|
+
throw new JsonRpcClientError('Codex app-server is unavailable.', 'app_server_unavailable');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
this.client.respond(id, result);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private async doStart() {
|
|
546
|
+
const child = this.spawnProcess(this.options.command, ['app-server', '--listen', 'stdio://']);
|
|
547
|
+
this.process = child;
|
|
548
|
+
this.status.lastStartedAt = new Date().toISOString();
|
|
549
|
+
const startupError = new Promise<never>((_, reject) => {
|
|
550
|
+
child.once('error', (error) => {
|
|
551
|
+
reject(
|
|
552
|
+
new JsonRpcClientError(
|
|
553
|
+
`Failed to spawn Codex app-server: ${error.message}`,
|
|
554
|
+
'spawn_failed'
|
|
555
|
+
)
|
|
556
|
+
);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
child.stderr.on('data', (chunk) => {
|
|
561
|
+
const message = chunk.toString().trim();
|
|
562
|
+
if (!message) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
this.emit('stderr', message);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
child.once('exit', (code, signal) => {
|
|
570
|
+
const intentionallyStopping = this.intentionallyStopping.delete(child);
|
|
571
|
+
const isCurrentClient = this.client === client;
|
|
572
|
+
const isCurrentProcess = this.process === child;
|
|
573
|
+
|
|
574
|
+
if (isCurrentClient) {
|
|
575
|
+
this.client?.close();
|
|
576
|
+
this.client = null;
|
|
577
|
+
}
|
|
578
|
+
if (isCurrentProcess) {
|
|
579
|
+
this.process = null;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (!isCurrentClient && !isCurrentProcess) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (intentionallyStopping || this.intentionalStop) {
|
|
587
|
+
if (isCurrentProcess || isCurrentClient) {
|
|
588
|
+
this.setStatus('stopped', null);
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const reason = `Codex app-server exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'}).`;
|
|
594
|
+
if (this.status.restartCount < this.maxRestarts) {
|
|
595
|
+
this.status.restartCount += 1;
|
|
596
|
+
this.setStatus('degraded', reason);
|
|
597
|
+
void this.start();
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
this.setStatus('failed', reason);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const client = new JsonRpcClient(child.stdout as any, child.stdin as any);
|
|
605
|
+
this.client = client;
|
|
606
|
+
|
|
607
|
+
client.on('notification', (notification) => {
|
|
608
|
+
this.emit('notification', notification as CodexServerEvent);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
client.on('request', (request) => {
|
|
612
|
+
this.emit('request', request as CodexServerRequest);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
client.on('warning', (warning) => {
|
|
616
|
+
this.emit('warning', warning);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
await Promise.race([
|
|
620
|
+
client.request('initialize', {
|
|
621
|
+
clientInfo: this.options.clientInfo,
|
|
622
|
+
capabilities: {
|
|
623
|
+
experimentalApi: true
|
|
624
|
+
}
|
|
625
|
+
}),
|
|
626
|
+
startupError,
|
|
627
|
+
new Promise((_, reject) => {
|
|
628
|
+
setTimeout(() => {
|
|
629
|
+
reject(
|
|
630
|
+
new JsonRpcClientError(
|
|
631
|
+
'Codex app-server initialize handshake timed out.',
|
|
632
|
+
'initialize_timeout'
|
|
633
|
+
)
|
|
634
|
+
);
|
|
635
|
+
}, this.options.startupTimeoutMs);
|
|
636
|
+
})
|
|
637
|
+
]).catch((error) => {
|
|
638
|
+
this.client = null;
|
|
639
|
+
this.process?.kill('SIGTERM');
|
|
640
|
+
this.process = null;
|
|
641
|
+
this.setStatus('failed', error instanceof Error ? error.message : String(error));
|
|
642
|
+
throw error;
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
this.setStatus('ready', null);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private setStatus(state: AppServerStatusSnapshot['state'], lastError: string | null) {
|
|
649
|
+
this.status = {
|
|
650
|
+
...this.status,
|
|
651
|
+
state,
|
|
652
|
+
lastError
|
|
653
|
+
};
|
|
654
|
+
this.emit('status', this.getStatus());
|
|
655
|
+
}
|
|
656
|
+
}
|