mcp-codex-subagent 2.0.8 → 2.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/event/bus.d.ts +53 -0
- package/dist/event/bus.d.ts.map +1 -0
- package/dist/event/bus.js +94 -0
- package/dist/event/bus.js.map +1 -0
- package/dist/event/throttle.d.ts +36 -0
- package/dist/event/throttle.d.ts.map +1 -0
- package/dist/event/throttle.js +66 -0
- package/dist/event/throttle.js.map +1 -0
- package/dist/index.js +32 -1
- package/dist/index.js.map +1 -1
- package/dist/process/event-parser.d.ts +37 -0
- package/dist/process/event-parser.d.ts.map +1 -0
- package/dist/process/event-parser.js +141 -0
- package/dist/process/event-parser.js.map +1 -0
- package/dist/process/runner.d.ts +48 -0
- package/dist/process/runner.d.ts.map +1 -0
- package/dist/process/runner.js +227 -0
- package/dist/process/runner.js.map +1 -0
- package/dist/process/types.d.ts +74 -0
- package/dist/process/types.d.ts.map +1 -0
- package/dist/process/types.js +5 -0
- package/dist/process/types.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +128 -36
- package/dist/server.js.map +1 -1
- package/dist/services/account-rotator.d.ts +28 -0
- package/dist/services/account-rotator.d.ts.map +1 -0
- package/dist/services/account-rotator.js +216 -0
- package/dist/services/account-rotator.js.map +1 -0
- package/dist/services/output-file.d.ts.map +1 -1
- package/dist/services/output-file.js +80 -36
- package/dist/services/output-file.js.map +1 -1
- package/dist/services/task-manager.d.ts +6 -0
- package/dist/services/task-manager.d.ts.map +1 -1
- package/dist/services/task-manager.js +24 -1
- package/dist/services/task-manager.js.map +1 -1
- package/dist/services/template-init.d.ts +10 -0
- package/dist/services/template-init.d.ts.map +1 -0
- package/dist/services/template-init.js +41 -0
- package/dist/services/template-init.js.map +1 -0
- package/dist/session/file-storage.d.ts +27 -0
- package/dist/session/file-storage.d.ts.map +1 -0
- package/dist/session/file-storage.js +281 -0
- package/dist/session/file-storage.js.map +1 -0
- package/dist/session/storage.js +1 -1
- package/dist/session/storage.js.map +1 -1
- package/dist/task/state-machine.d.ts +27 -0
- package/dist/task/state-machine.d.ts.map +1 -0
- package/dist/task/state-machine.js +59 -0
- package/dist/task/state-machine.js.map +1 -0
- package/dist/task/store.d.ts +91 -0
- package/dist/task/store.d.ts.map +1 -0
- package/dist/task/store.js +317 -0
- package/dist/task/store.js.map +1 -0
- package/dist/task/types.d.ts +72 -0
- package/dist/task/types.d.ts.map +1 -0
- package/dist/task/types.js +13 -0
- package/dist/task/types.js.map +1 -0
- package/dist/templates/index.d.ts +16 -0
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +57 -5
- package/dist/templates/index.js.map +1 -1
- package/dist/tools/definitions.d.ts +5 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +253 -179
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/description-builder.d.ts +18 -0
- package/dist/tools/description-builder.d.ts.map +1 -0
- package/dist/tools/description-builder.js +88 -0
- package/dist/tools/description-builder.js.map +1 -0
- package/dist/tools/handlers.d.ts +19 -17
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +287 -341
- package/dist/tools/handlers.js.map +1 -1
- package/dist/types.d.ts +5 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -10
- package/dist/types.js.map +1 -1
- package/dist/utils/ring-buffer.d.ts +41 -0
- package/dist/utils/ring-buffer.d.ts.map +1 -0
- package/dist/utils/ring-buffer.js +83 -0
- package/dist/utils/ring-buffer.js.map +1 -0
- package/dist/wave/dag.d.ts +32 -0
- package/dist/wave/dag.d.ts.map +1 -0
- package/dist/wave/dag.js +186 -0
- package/dist/wave/dag.js.map +1 -0
- package/dist/wave/git.d.ts +57 -0
- package/dist/wave/git.d.ts.map +1 -0
- package/dist/wave/git.js +227 -0
- package/dist/wave/git.js.map +1 -0
- package/dist/wave/orchestrator.d.ts +15 -0
- package/dist/wave/orchestrator.d.ts.map +1 -0
- package/dist/wave/orchestrator.js +565 -0
- package/dist/wave/orchestrator.js.map +1 -0
- package/dist/wave/progress.d.ts +51 -0
- package/dist/wave/progress.d.ts.map +1 -0
- package/dist/wave/progress.js +176 -0
- package/dist/wave/progress.js.map +1 -0
- package/dist/wave/registry.d.ts +66 -0
- package/dist/wave/registry.d.ts.map +1 -0
- package/dist/wave/registry.js +340 -0
- package/dist/wave/registry.js.map +1 -0
- package/dist/wave/semaphore.d.ts +42 -0
- package/dist/wave/semaphore.d.ts.map +1 -0
- package/dist/wave/semaphore.js +119 -0
- package/dist/wave/semaphore.js.map +1 -0
- package/dist/wave/types.d.ts +197 -0
- package/dist/wave/types.d.ts.map +1 -0
- package/dist/wave/types.js +147 -0
- package/dist/wave/types.js.map +1 -0
- package/package.json +15 -15
package/dist/tools/handlers.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
1
|
import { readFile } from 'fs/promises';
|
|
3
|
-
import { TOOLS, PINNED_CODEX_MODEL, ROLE_TO_TASK_TYPE, CodexToolSchema, ReviewToolSchema,
|
|
4
|
-
import {
|
|
2
|
+
import { TOOLS, PINNED_CODEX_MODEL, ROLE_TO_TASK_TYPE, CodexToolSchema, ReviewToolSchema, } from '../types.js';
|
|
3
|
+
import { SpawnAgentGroupSchema, GROUP_TIMEOUT_MS, } from '../wave/types.js';
|
|
4
|
+
import { validateDag } from '../wave/dag.js';
|
|
5
|
+
import { groupRegistry } from '../wave/registry.js';
|
|
6
|
+
import { orchestrate } from '../wave/orchestrator.js';
|
|
7
|
+
import { FileSessionStorage } from '../session/file-storage.js';
|
|
5
8
|
import { ToolExecutionError, ValidationError } from '../errors.js';
|
|
6
9
|
import { executeCommand, executeCommandStreaming } from '../utils/command.js';
|
|
7
10
|
import { ZodError } from 'zod';
|
|
8
11
|
import { applyTemplate, isValidTaskType } from '../templates/index.js';
|
|
9
12
|
import { mcpText, mcpValidationError } from '../utils/format.js';
|
|
10
13
|
import { validateBrief, formatBriefValidationError, assemblePromptWithContext, } from '../utils/brief-validator.js';
|
|
11
|
-
import { createTask, appendOutput, completeTask, failTask, setProcess, } from '../services/task-manager.js';
|
|
12
14
|
import { getLastMessagePath } from '../services/output-file.js';
|
|
15
|
+
import { CodexProcessRunner } from '../process/runner.js';
|
|
16
|
+
import { taskStore } from '../task/store.js';
|
|
17
|
+
import { taskEventBus } from '../event/bus.js';
|
|
18
|
+
import { NotificationThrottle } from '../event/throttle.js';
|
|
19
|
+
import { rotateAccount } from '../services/account-rotator.js';
|
|
13
20
|
// Default no-op context for handlers that don't need progress
|
|
14
21
|
const defaultContext = {
|
|
15
22
|
sendProgress: async () => { },
|
|
@@ -20,162 +27,40 @@ const isStructuredContentEnabled = () => {
|
|
|
20
27
|
return false;
|
|
21
28
|
return ['1', 'true', 'yes', 'on'].includes(raw.toLowerCase());
|
|
22
29
|
};
|
|
23
|
-
// Notification
|
|
24
|
-
let
|
|
25
|
-
export function setNotificationSender(sender) {
|
|
26
|
-
notificationSender = sender;
|
|
27
|
-
}
|
|
30
|
+
// --- Notification throttle (replaces direct notifyResourceChanged) ---
|
|
31
|
+
let notificationThrottle = null;
|
|
28
32
|
/**
|
|
29
|
-
*
|
|
33
|
+
* Set the notification sender. Creates a throttle that coalesces
|
|
34
|
+
* resource-update notifications.
|
|
30
35
|
*/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
export function setNotificationSender(sender) {
|
|
37
|
+
const intervalMs = parseInt(process.env.CODEX_NOTIFICATION_INTERVAL_MS || '', 10);
|
|
38
|
+
notificationThrottle = new NotificationThrottle({
|
|
39
|
+
intervalMs: Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : 500,
|
|
40
|
+
send: async () => {
|
|
41
|
+
try {
|
|
42
|
+
await sender.sendNotification({
|
|
43
|
+
method: 'notifications/resources/updated',
|
|
44
|
+
params: {},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Non-fatal
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
43
52
|
}
|
|
44
53
|
/**
|
|
45
|
-
*
|
|
46
|
-
* These are non-actionable internal diagnostics that pollute output.
|
|
47
|
-
* Matched via substring — order doesn't matter.
|
|
48
|
-
*/
|
|
49
|
-
const STDERR_NOISE_PATTERNS = [
|
|
50
|
-
// State DB rollout errors — emitted once per old session on every startup
|
|
51
|
-
'state db missing rollout path',
|
|
52
|
-
'missing rollout path for thread',
|
|
53
|
-
'codex_core::rollout::list',
|
|
54
|
-
// Internal tracing noise
|
|
55
|
-
'codex_core::config_watcher',
|
|
56
|
-
'codex_core::telemetry',
|
|
57
|
-
// Model provider progress (not errors)
|
|
58
|
-
'Refreshing model list',
|
|
59
|
-
'model list refreshed',
|
|
60
|
-
];
|
|
61
|
-
/**
|
|
62
|
-
* Test whether a stderr chunk is just noise (should be dropped).
|
|
54
|
+
* Queue a resource-changed notification (throttled).
|
|
63
55
|
*/
|
|
64
|
-
function
|
|
65
|
-
|
|
56
|
+
export function notifyResourceChanged() {
|
|
57
|
+
notificationThrottle?.notify();
|
|
66
58
|
}
|
|
67
59
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* Design:
|
|
71
|
-
* - Skips streaming intermediates (item.started, thread.started, turn.started)
|
|
72
|
-
* - Extracts completed items as clean JSONL
|
|
73
|
-
* - Preserves turn.completed for usage stats
|
|
74
|
-
* - Preserves turn.failed and item.failed for error visibility
|
|
75
|
-
* - Normalises legacy event shapes to the modern item schema
|
|
76
|
-
*
|
|
77
|
-
* Every non-null return value is a single valid JSON string (proper JSONL).
|
|
78
|
-
*
|
|
79
|
-
* Ref: https://developers.openai.com/codex/noninteractive/
|
|
60
|
+
* Flush pending notifications (for shutdown).
|
|
80
61
|
*/
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
if (!trimmed)
|
|
84
|
-
return null;
|
|
85
|
-
try {
|
|
86
|
-
const event = JSON.parse(trimmed);
|
|
87
|
-
// ------------------------------------------------------------------
|
|
88
|
-
// 1. Skip lifecycle / streaming intermediate events
|
|
89
|
-
// ------------------------------------------------------------------
|
|
90
|
-
if (event.type === 'thread.started' ||
|
|
91
|
-
event.type === 'turn.started' ||
|
|
92
|
-
event.type === 'item.started') {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
// ------------------------------------------------------------------
|
|
96
|
-
// 2. Turn completion — emit usage stats (token counts)
|
|
97
|
-
// Schema: { type, usage: { input_tokens, cached_input_tokens, output_tokens } }
|
|
98
|
-
// ------------------------------------------------------------------
|
|
99
|
-
if (event.type === 'turn.completed') {
|
|
100
|
-
if (event.usage) {
|
|
101
|
-
return JSON.stringify({ type: 'turn_usage', ...event.usage });
|
|
102
|
-
}
|
|
103
|
-
return null; // no usage data → skip
|
|
104
|
-
}
|
|
105
|
-
// ------------------------------------------------------------------
|
|
106
|
-
// 3. Turn failure — surface error
|
|
107
|
-
// Schema: { type, error: { message?, code? } }
|
|
108
|
-
// ------------------------------------------------------------------
|
|
109
|
-
if (event.type === 'turn.failed') {
|
|
110
|
-
const err = event.error;
|
|
111
|
-
return JSON.stringify({
|
|
112
|
-
type: 'turn_failed',
|
|
113
|
-
code: err?.code ?? err?.codexErrorInfo?.type ?? null,
|
|
114
|
-
message: err?.message || 'Unknown turn failure',
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
// ------------------------------------------------------------------
|
|
118
|
-
// 4. Item failed — emit item with _failed marker
|
|
119
|
-
// ------------------------------------------------------------------
|
|
120
|
-
if (event.type === 'item.failed' && event.item) {
|
|
121
|
-
return JSON.stringify({ ...event.item, _failed: true });
|
|
122
|
-
}
|
|
123
|
-
// ------------------------------------------------------------------
|
|
124
|
-
// 5. Item completed — the primary event. Extract item payload.
|
|
125
|
-
// Item types: agent_message, reasoning, command_execution,
|
|
126
|
-
// file_change, mcp_tool_call, web_search, plan_update
|
|
127
|
-
// ------------------------------------------------------------------
|
|
128
|
-
if (event.type === 'item.completed' && event.item) {
|
|
129
|
-
return JSON.stringify(event.item);
|
|
130
|
-
}
|
|
131
|
-
// ------------------------------------------------------------------
|
|
132
|
-
// 6. Top-level error
|
|
133
|
-
// Schema: { type: "error", error: { code, message } }
|
|
134
|
-
// ------------------------------------------------------------------
|
|
135
|
-
if (event.type === 'error') {
|
|
136
|
-
const err = event.error ?? event;
|
|
137
|
-
return JSON.stringify({
|
|
138
|
-
type: 'error',
|
|
139
|
-
code: err.code ?? null,
|
|
140
|
-
message: err.message || trimmed,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
// ------------------------------------------------------------------
|
|
144
|
-
// 7. Legacy / alternate event shapes (pre-2025 codex versions)
|
|
145
|
-
// ------------------------------------------------------------------
|
|
146
|
-
if (event.type === 'message' && event.content) {
|
|
147
|
-
return JSON.stringify({
|
|
148
|
-
id: event.id ?? null,
|
|
149
|
-
type: 'agent_message',
|
|
150
|
-
text: event.content,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
if (event.type === 'function_call') {
|
|
154
|
-
return JSON.stringify({
|
|
155
|
-
id: event.id ?? null,
|
|
156
|
-
type: 'function_call',
|
|
157
|
-
name: event.name,
|
|
158
|
-
arguments: event.arguments,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
if (event.type === 'function_call_output') {
|
|
162
|
-
return JSON.stringify({
|
|
163
|
-
id: event.id ?? null,
|
|
164
|
-
type: 'function_call_output',
|
|
165
|
-
output: typeof event.output === 'string'
|
|
166
|
-
? event.output
|
|
167
|
-
: JSON.stringify(event.output),
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
// ------------------------------------------------------------------
|
|
171
|
-
// 8. Unknown event — pass through as-is
|
|
172
|
-
// ------------------------------------------------------------------
|
|
173
|
-
return JSON.stringify(event);
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
// Not valid JSON — skip malformed lines
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
62
|
+
export async function flushNotifications() {
|
|
63
|
+
await notificationThrottle?.flush();
|
|
179
64
|
}
|
|
180
65
|
export class CodexToolHandler {
|
|
181
66
|
sessionStorage;
|
|
@@ -234,10 +119,10 @@ export class CodexToolHandler {
|
|
|
234
119
|
enrichedPrompt = applyTemplate(taskType, enrichedPrompt, specialization);
|
|
235
120
|
}
|
|
236
121
|
}
|
|
237
|
-
// --- Step 4: Create task ---
|
|
122
|
+
// --- Step 4: Create task in store ---
|
|
238
123
|
const selectedModel = PINNED_CODEX_MODEL;
|
|
239
124
|
const effort = reasoningEffort ?? 'xhigh';
|
|
240
|
-
const task = await createTask({
|
|
125
|
+
const task = await taskStore.createTask({
|
|
241
126
|
prompt,
|
|
242
127
|
role,
|
|
243
128
|
specialization,
|
|
@@ -249,117 +134,141 @@ export class CodexToolHandler {
|
|
|
249
134
|
const lastMessagePath = getLastMessagePath(cwd, task.id);
|
|
250
135
|
let cmdArgs;
|
|
251
136
|
if (useResume && codexConversationId) {
|
|
252
|
-
cmdArgs = [
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
137
|
+
cmdArgs = [
|
|
138
|
+
'exec',
|
|
139
|
+
'--yolo',
|
|
140
|
+
'--search',
|
|
141
|
+
'--skip-git-repo-check',
|
|
142
|
+
'--json',
|
|
143
|
+
'-o',
|
|
144
|
+
lastMessagePath,
|
|
145
|
+
'-c',
|
|
146
|
+
`model="${selectedModel}"`,
|
|
147
|
+
'-c',
|
|
148
|
+
`model_reasoning_effort="${effort}"`,
|
|
149
|
+
'resume',
|
|
150
|
+
codexConversationId,
|
|
151
|
+
enrichedPrompt,
|
|
152
|
+
];
|
|
257
153
|
}
|
|
258
154
|
else {
|
|
259
|
-
cmdArgs = [
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
155
|
+
cmdArgs = [
|
|
156
|
+
'exec',
|
|
157
|
+
'--yolo',
|
|
158
|
+
'--search',
|
|
159
|
+
'--json',
|
|
160
|
+
'-o',
|
|
161
|
+
lastMessagePath,
|
|
162
|
+
'--model',
|
|
163
|
+
selectedModel,
|
|
164
|
+
'-c',
|
|
165
|
+
`model_reasoning_effort="${effort}"`,
|
|
166
|
+
'-C',
|
|
167
|
+
cwd,
|
|
168
|
+
'--skip-git-repo-check',
|
|
169
|
+
enrichedPrompt,
|
|
170
|
+
];
|
|
268
171
|
}
|
|
269
|
-
// --- Step 6: Spawn
|
|
270
|
-
const isWindows = process.platform === 'win32';
|
|
172
|
+
// --- Step 6: Spawn via ProcessRunner ---
|
|
271
173
|
const env = effectiveCallbackUri
|
|
272
174
|
? { ...process.env, CODEX_MCP_CALLBACK_URI: effectiveCallbackUri }
|
|
273
|
-
:
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const lines = stdoutBuffer.split('\n');
|
|
287
|
-
// Keep incomplete last line in buffer
|
|
288
|
-
stdoutBuffer = lines.pop() || '';
|
|
289
|
-
for (const line of lines) {
|
|
290
|
-
const parsed = parseCodexEvent(line);
|
|
291
|
-
if (parsed) {
|
|
292
|
-
appendOutput(task.id, parsed);
|
|
175
|
+
: undefined;
|
|
176
|
+
const runner = new CodexProcessRunner({
|
|
177
|
+
onEvent: (processEvent) => {
|
|
178
|
+
// Store raw JSON in ring buffer + output file
|
|
179
|
+
taskStore.appendOutput(task.id, processEvent.raw);
|
|
180
|
+
// Emit parsed event to subscribers
|
|
181
|
+
taskEventBus.emit(task.id, processEvent.parsed);
|
|
182
|
+
},
|
|
183
|
+
onExit: async (exitInfo) => {
|
|
184
|
+
// Read last-message file if available
|
|
185
|
+
let lastMessage;
|
|
186
|
+
try {
|
|
187
|
+
lastMessage = await readFile(lastMessagePath, 'utf-8');
|
|
293
188
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// Capture stderr — filter out known codex-core noise patterns
|
|
297
|
-
child.stderr?.on('data', (data) => {
|
|
298
|
-
const chunk = data.toString().trim();
|
|
299
|
-
if (!chunk)
|
|
300
|
-
return;
|
|
301
|
-
if (isStderrNoise(chunk))
|
|
302
|
-
return;
|
|
303
|
-
appendOutput(task.id, JSON.stringify({ type: 'stderr', text: chunk.slice(0, 500) }));
|
|
304
|
-
});
|
|
305
|
-
// --- Step 8: Wire completion handler ---
|
|
306
|
-
child.on('close', async (code) => {
|
|
307
|
-
// Process remaining buffer
|
|
308
|
-
if (stdoutBuffer.trim()) {
|
|
309
|
-
const parsed = parseCodexEvent(stdoutBuffer);
|
|
310
|
-
if (parsed) {
|
|
311
|
-
await appendOutput(task.id, parsed);
|
|
189
|
+
catch {
|
|
190
|
+
// File may not exist if codex didn't write it
|
|
312
191
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
// Extract conversation ID from output for session resume
|
|
323
|
-
if (activeSessionId && !useResume) {
|
|
324
|
-
// Check task output for conversation ID patterns
|
|
325
|
-
const allOutput = task.output.join('\n');
|
|
326
|
-
const conversationIdMatch = allOutput.match(/(conversation|session)\s*id\s*:\s*([a-zA-Z0-9-]+)/i);
|
|
327
|
-
if (conversationIdMatch) {
|
|
328
|
-
this.sessionStorage.setCodexConversationId(activeSessionId, conversationIdMatch[2]);
|
|
192
|
+
// Extract conversation ID from output for session resume
|
|
193
|
+
if (activeSessionId && !useResume) {
|
|
194
|
+
const outputArr = task.output.toArray();
|
|
195
|
+
const allOutput = outputArr.join('\n');
|
|
196
|
+
const conversationIdMatch = allOutput.match(/(conversation|session)\s*id\s*:\s*([a-zA-Z0-9-]+)/i);
|
|
197
|
+
if (conversationIdMatch) {
|
|
198
|
+
this.sessionStorage.setCodexConversationId(activeSessionId, conversationIdMatch[2]);
|
|
199
|
+
}
|
|
329
200
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
201
|
+
// Save turn if using a session
|
|
202
|
+
if (activeSessionId) {
|
|
203
|
+
const outputArr = task.output.toArray();
|
|
204
|
+
const turn = {
|
|
205
|
+
prompt,
|
|
206
|
+
response: lastMessage || outputArr.slice(-10).join('\n') || 'No output',
|
|
207
|
+
timestamp: new Date(),
|
|
208
|
+
};
|
|
209
|
+
this.sessionStorage.addTurn(activeSessionId, turn);
|
|
210
|
+
}
|
|
211
|
+
// Transition to terminal state
|
|
212
|
+
if (exitInfo.exitCode === 0 || lastMessage) {
|
|
213
|
+
await taskStore.transitionTo(task.id, 'completed', {
|
|
214
|
+
metadata: {
|
|
215
|
+
exitCode: exitInfo.exitCode,
|
|
216
|
+
...(lastMessage && {
|
|
217
|
+
lastMessage: lastMessage.slice(0, 5000),
|
|
218
|
+
}),
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
await taskStore.transitionTo(task.id, 'failed', {
|
|
224
|
+
error: `Codex exited with code ${exitInfo.exitCode}`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// Clean up event bus subscriptions for this task
|
|
228
|
+
taskEventBus.removeTaskSubscriptions(task.id);
|
|
229
|
+
// Notify resource subscribers (throttled)
|
|
230
|
+
notifyResourceChanged();
|
|
231
|
+
},
|
|
357
232
|
});
|
|
358
|
-
//
|
|
233
|
+
// Rotate account before spawning
|
|
234
|
+
try {
|
|
235
|
+
await rotateAccount();
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
239
|
+
await taskStore.transitionTo(task.id, 'failed', {
|
|
240
|
+
error: `Account rotation failed: ${msg}`,
|
|
241
|
+
});
|
|
242
|
+
notifyResourceChanged();
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
let handle;
|
|
246
|
+
try {
|
|
247
|
+
handle = runner.spawn({
|
|
248
|
+
args: cmdArgs,
|
|
249
|
+
cwd,
|
|
250
|
+
env,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
255
|
+
await taskStore.transitionTo(task.id, 'failed', {
|
|
256
|
+
error: `Failed to spawn Codex process: ${msg}`,
|
|
257
|
+
});
|
|
258
|
+
taskEventBus.removeTaskSubscriptions(task.id);
|
|
259
|
+
notifyResourceChanged();
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
// Store process reference and transition to running
|
|
263
|
+
taskStore.setProcess(task.id, handle.childProcess);
|
|
264
|
+
await taskStore.transitionTo(task.id, 'running');
|
|
265
|
+
// --- Step 7: Return immediately ---
|
|
266
|
+
const pidStr = handle.pid ? `\npid: \`${handle.pid}\`` : '';
|
|
359
267
|
const parts = [
|
|
360
|
-
`**Task launched** (
|
|
268
|
+
`**Task launched** (spawn_subagent)`,
|
|
361
269
|
`task_id: \`${task.id}\``,
|
|
362
270
|
task.outputFilePath ? `output_file: \`${task.outputFilePath}\`` : null,
|
|
271
|
+
pidStr ? pidStr : null,
|
|
363
272
|
'',
|
|
364
273
|
'The agent is working in the background. MCP notifications will alert on completion—no need to poll.',
|
|
365
274
|
'Output is clean JSONL — read resource `task:///' +
|
|
@@ -373,9 +282,9 @@ export class CodexToolHandler {
|
|
|
373
282
|
throw error;
|
|
374
283
|
}
|
|
375
284
|
if (error instanceof ZodError) {
|
|
376
|
-
throw new ValidationError(TOOLS.
|
|
285
|
+
throw new ValidationError(TOOLS.SPAWN_SUBAGENT, error.message);
|
|
377
286
|
}
|
|
378
|
-
throw new ToolExecutionError(TOOLS.
|
|
287
|
+
throw new ToolExecutionError(TOOLS.SPAWN_SUBAGENT, 'Failed to execute codex command', error);
|
|
379
288
|
}
|
|
380
289
|
}
|
|
381
290
|
buildEnhancedPrompt(turns, newPrompt) {
|
|
@@ -394,89 +303,12 @@ export class CodexToolHandler {
|
|
|
394
303
|
return `${contextualInfo}\n\nTask: ${newPrompt}`;
|
|
395
304
|
}
|
|
396
305
|
}
|
|
397
|
-
export class PingToolHandler {
|
|
398
|
-
async execute(args, _context = defaultContext) {
|
|
399
|
-
try {
|
|
400
|
-
const { message = 'pong' } = PingToolSchema.parse(args);
|
|
401
|
-
return {
|
|
402
|
-
content: [
|
|
403
|
-
{
|
|
404
|
-
type: 'text',
|
|
405
|
-
text: message,
|
|
406
|
-
},
|
|
407
|
-
],
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
catch (error) {
|
|
411
|
-
if (error instanceof ZodError) {
|
|
412
|
-
throw new ValidationError(TOOLS.PING, error.message);
|
|
413
|
-
}
|
|
414
|
-
throw new ToolExecutionError(TOOLS.PING, 'Failed to execute ping command', error);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
export class HelpToolHandler {
|
|
419
|
-
async execute(args, _context = defaultContext) {
|
|
420
|
-
try {
|
|
421
|
-
HelpToolSchema.parse(args);
|
|
422
|
-
const result = await executeCommand('codex', ['--help']);
|
|
423
|
-
return {
|
|
424
|
-
content: [
|
|
425
|
-
{
|
|
426
|
-
type: 'text',
|
|
427
|
-
text: result.stdout || 'No help information available',
|
|
428
|
-
},
|
|
429
|
-
],
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
catch (error) {
|
|
433
|
-
if (error instanceof ZodError) {
|
|
434
|
-
throw new ValidationError(TOOLS.HELP, error.message);
|
|
435
|
-
}
|
|
436
|
-
throw new ToolExecutionError(TOOLS.HELP, 'Failed to execute help command', error);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
export class ListSessionsToolHandler {
|
|
441
|
-
sessionStorage;
|
|
442
|
-
constructor(sessionStorage) {
|
|
443
|
-
this.sessionStorage = sessionStorage;
|
|
444
|
-
}
|
|
445
|
-
async execute(args, _context = defaultContext) {
|
|
446
|
-
try {
|
|
447
|
-
ListSessionsToolSchema.parse(args);
|
|
448
|
-
const sessions = this.sessionStorage.listSessions();
|
|
449
|
-
const sessionInfo = sessions.map((session) => ({
|
|
450
|
-
id: session.id,
|
|
451
|
-
createdAt: session.createdAt.toISOString(),
|
|
452
|
-
lastAccessedAt: session.lastAccessedAt.toISOString(),
|
|
453
|
-
turnCount: session.turns.length,
|
|
454
|
-
}));
|
|
455
|
-
return {
|
|
456
|
-
content: [
|
|
457
|
-
{
|
|
458
|
-
type: 'text',
|
|
459
|
-
text: sessionInfo.length > 0
|
|
460
|
-
? JSON.stringify(sessionInfo, null, 2)
|
|
461
|
-
: 'No active sessions',
|
|
462
|
-
},
|
|
463
|
-
],
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
catch (error) {
|
|
467
|
-
if (error instanceof ZodError) {
|
|
468
|
-
throw new ValidationError(TOOLS.LIST_SESSIONS, error.message);
|
|
469
|
-
}
|
|
470
|
-
throw new ToolExecutionError(TOOLS.LIST_SESSIONS, 'Failed to list sessions', error);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
306
|
export class ReviewToolHandler {
|
|
475
307
|
async execute(args, context = defaultContext) {
|
|
476
308
|
try {
|
|
477
309
|
const { prompt, uncommitted, base, commit, title, workingDirectory, } = ReviewToolSchema.parse(args);
|
|
478
310
|
if (prompt && uncommitted) {
|
|
479
|
-
throw new ValidationError(TOOLS.
|
|
311
|
+
throw new ValidationError(TOOLS.CODE_REVIEW, 'The review prompt cannot be combined with uncommitted=true. Use a base/commit review or omit the prompt.');
|
|
480
312
|
}
|
|
481
313
|
const cmdArgs = [];
|
|
482
314
|
if (workingDirectory) {
|
|
@@ -528,22 +360,136 @@ export class ReviewToolHandler {
|
|
|
528
360
|
}
|
|
529
361
|
catch (error) {
|
|
530
362
|
if (error instanceof ZodError) {
|
|
531
|
-
throw new ValidationError(TOOLS.
|
|
363
|
+
throw new ValidationError(TOOLS.CODE_REVIEW, error.message);
|
|
364
|
+
}
|
|
365
|
+
if (error instanceof ValidationError) {
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
throw new ToolExecutionError(TOOLS.CODE_REVIEW, 'Failed to execute code review', error);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
export class GroupToolHandler {
|
|
373
|
+
async execute(args, context = defaultContext) {
|
|
374
|
+
try {
|
|
375
|
+
const parsed = SpawnAgentGroupSchema.parse(args);
|
|
376
|
+
const { agents, commonContext, commonContextFiles, dependsOn, append, appendToGroupTaskId, groupName, workingDirectory, callbackUri, } = parsed;
|
|
377
|
+
const cwd = workingDirectory || process.cwd();
|
|
378
|
+
// --- Validation ---
|
|
379
|
+
// 1. Unique aliases
|
|
380
|
+
const aliases = agents.map((a) => a.alias);
|
|
381
|
+
const uniqueAliases = new Set(aliases);
|
|
382
|
+
if (uniqueAliases.size !== aliases.length) {
|
|
383
|
+
const dupes = aliases.filter((a, i) => aliases.indexOf(a) !== i);
|
|
384
|
+
return mcpValidationError(`Duplicate agent aliases: ${[...new Set(dupes)].join(', ')}`);
|
|
385
|
+
}
|
|
386
|
+
// 2. DAG validation
|
|
387
|
+
const dagResult = validateDag(agents);
|
|
388
|
+
if (!dagResult.valid) {
|
|
389
|
+
return mcpValidationError(`DAG validation failed:\n${dagResult.errors.join('\n')}`);
|
|
390
|
+
}
|
|
391
|
+
// 3. Append validation
|
|
392
|
+
if (append && !appendToGroupTaskId) {
|
|
393
|
+
return mcpValidationError('appendToGroupTaskId is required when append=true');
|
|
394
|
+
}
|
|
395
|
+
if (append && appendToGroupTaskId) {
|
|
396
|
+
const existing = groupRegistry.getGroup(appendToGroupTaskId);
|
|
397
|
+
if (!existing) {
|
|
398
|
+
return mcpValidationError(`Group "${appendToGroupTaskId}" not found for append`);
|
|
399
|
+
}
|
|
400
|
+
if (existing.state === 'running' ||
|
|
401
|
+
existing.state === 'creating_worktree' ||
|
|
402
|
+
existing.state === 'waiting_deps' ||
|
|
403
|
+
existing.state === 'committing') {
|
|
404
|
+
return mcpValidationError(`Cannot append to group "${appendToGroupTaskId}" — still running (state: ${existing.state})`);
|
|
405
|
+
}
|
|
532
406
|
}
|
|
407
|
+
// 4. Group dependency validation
|
|
408
|
+
if (dependsOn && dependsOn.length > 0) {
|
|
409
|
+
for (const depId of dependsOn) {
|
|
410
|
+
const dep = groupRegistry.getGroup(depId);
|
|
411
|
+
if (!dep) {
|
|
412
|
+
return mcpValidationError(`Dependency group "${depId}" not found`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// --- Create group ---
|
|
417
|
+
const group = groupRegistry.createGroup({
|
|
418
|
+
agents,
|
|
419
|
+
baseCwd: cwd,
|
|
420
|
+
dependsOn: dependsOn ?? [],
|
|
421
|
+
groupName,
|
|
422
|
+
});
|
|
423
|
+
// --- Orchestrate (blocks until done) ---
|
|
424
|
+
const result = await orchestrate({
|
|
425
|
+
groupId: group.id,
|
|
426
|
+
baseCwd: cwd,
|
|
427
|
+
agents,
|
|
428
|
+
commonContext,
|
|
429
|
+
commonContextFiles,
|
|
430
|
+
dependsOn: dependsOn ?? [],
|
|
431
|
+
append: append ?? false,
|
|
432
|
+
appendToGroupTaskId,
|
|
433
|
+
groupName,
|
|
434
|
+
callbackUri,
|
|
435
|
+
timeoutMs: GROUP_TIMEOUT_MS,
|
|
436
|
+
sendProgress: context.sendProgress,
|
|
437
|
+
});
|
|
438
|
+
// --- Build response ---
|
|
439
|
+
const stateLabel = result.state === 'done_success'
|
|
440
|
+
? 'SUCCESS'
|
|
441
|
+
: result.state === 'done_timeout'
|
|
442
|
+
? 'TIMEOUT'
|
|
443
|
+
: 'FAILED';
|
|
444
|
+
const agentRows = result.agentResults.map((a) => {
|
|
445
|
+
const taskIdStr = a.taskId ? `\`${a.taskId}\`` : '-';
|
|
446
|
+
const errorStr = a.error ? a.error.slice(0, 60) : '-';
|
|
447
|
+
return `| ${a.alias} | ${a.state} | ${taskIdStr} | ${a.attempts} | ${errorStr} |`;
|
|
448
|
+
});
|
|
449
|
+
const parts = [
|
|
450
|
+
`**Agent Group [${stateLabel}]** (spawn_agent_group)`,
|
|
451
|
+
`group_id: \`${result.groupId}\``,
|
|
452
|
+
`branch: \`${result.branch}\``,
|
|
453
|
+
`worktree: \`${result.worktreePath}\``,
|
|
454
|
+
result.baseCommit ? `base_commit: \`${result.baseCommit}\`` : null,
|
|
455
|
+
result.commitSha ? `commit: \`${result.commitSha}\`` : null,
|
|
456
|
+
result.error ? `error: ${result.error}` : null,
|
|
457
|
+
`duration: ${(result.durationMs / 1000).toFixed(1)}s`,
|
|
458
|
+
'',
|
|
459
|
+
'| Agent | Status | Task ID | Attempts | Error |',
|
|
460
|
+
'|-------|--------|---------|----------|-------|',
|
|
461
|
+
...agentRows,
|
|
462
|
+
'',
|
|
463
|
+
'**Review commands:**',
|
|
464
|
+
` git -C ${result.worktreePath} status`,
|
|
465
|
+
` git -C ${result.worktreePath} log --oneline -5`,
|
|
466
|
+
result.baseCommit
|
|
467
|
+
? ` git -C ${result.worktreePath} diff ${result.baseCommit}...HEAD`
|
|
468
|
+
: null,
|
|
469
|
+
result.baseCommit
|
|
470
|
+
? ` git diff ${result.baseCommit}..${result.branch}`
|
|
471
|
+
: null,
|
|
472
|
+
'',
|
|
473
|
+
'Not merged into main. Review diff and merge manually.',
|
|
474
|
+
].filter(Boolean);
|
|
475
|
+
return mcpText(parts.join('\n'));
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
533
478
|
if (error instanceof ValidationError) {
|
|
534
479
|
throw error;
|
|
535
480
|
}
|
|
536
|
-
|
|
481
|
+
if (error instanceof ZodError) {
|
|
482
|
+
throw new ValidationError(TOOLS.SPAWN_AGENT_GROUP, error.message);
|
|
483
|
+
}
|
|
484
|
+
throw new ToolExecutionError(TOOLS.SPAWN_AGENT_GROUP, 'Failed to execute agent group', error);
|
|
537
485
|
}
|
|
538
486
|
}
|
|
539
487
|
}
|
|
540
488
|
// Tool handler registry
|
|
541
|
-
const sessionStorage = new
|
|
489
|
+
const sessionStorage = new FileSessionStorage();
|
|
542
490
|
export const toolHandlers = {
|
|
543
|
-
[TOOLS.
|
|
544
|
-
[TOOLS.
|
|
545
|
-
[TOOLS.
|
|
546
|
-
[TOOLS.HELP]: new HelpToolHandler(),
|
|
547
|
-
[TOOLS.LIST_SESSIONS]: new ListSessionsToolHandler(sessionStorage),
|
|
491
|
+
[TOOLS.SPAWN_SUBAGENT]: new CodexToolHandler(sessionStorage),
|
|
492
|
+
[TOOLS.CODE_REVIEW]: new ReviewToolHandler(),
|
|
493
|
+
[TOOLS.SPAWN_AGENT_GROUP]: new GroupToolHandler(),
|
|
548
494
|
};
|
|
549
495
|
//# sourceMappingURL=handlers.js.map
|