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.
Files changed (111) hide show
  1. package/dist/event/bus.d.ts +53 -0
  2. package/dist/event/bus.d.ts.map +1 -0
  3. package/dist/event/bus.js +94 -0
  4. package/dist/event/bus.js.map +1 -0
  5. package/dist/event/throttle.d.ts +36 -0
  6. package/dist/event/throttle.d.ts.map +1 -0
  7. package/dist/event/throttle.js +66 -0
  8. package/dist/event/throttle.js.map +1 -0
  9. package/dist/index.js +32 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/process/event-parser.d.ts +37 -0
  12. package/dist/process/event-parser.d.ts.map +1 -0
  13. package/dist/process/event-parser.js +141 -0
  14. package/dist/process/event-parser.js.map +1 -0
  15. package/dist/process/runner.d.ts +48 -0
  16. package/dist/process/runner.d.ts.map +1 -0
  17. package/dist/process/runner.js +227 -0
  18. package/dist/process/runner.js.map +1 -0
  19. package/dist/process/types.d.ts +74 -0
  20. package/dist/process/types.d.ts.map +1 -0
  21. package/dist/process/types.js +5 -0
  22. package/dist/process/types.js.map +1 -0
  23. package/dist/server.d.ts.map +1 -1
  24. package/dist/server.js +128 -36
  25. package/dist/server.js.map +1 -1
  26. package/dist/services/account-rotator.d.ts +28 -0
  27. package/dist/services/account-rotator.d.ts.map +1 -0
  28. package/dist/services/account-rotator.js +216 -0
  29. package/dist/services/account-rotator.js.map +1 -0
  30. package/dist/services/output-file.d.ts.map +1 -1
  31. package/dist/services/output-file.js +80 -36
  32. package/dist/services/output-file.js.map +1 -1
  33. package/dist/services/task-manager.d.ts +6 -0
  34. package/dist/services/task-manager.d.ts.map +1 -1
  35. package/dist/services/task-manager.js +24 -1
  36. package/dist/services/task-manager.js.map +1 -1
  37. package/dist/services/template-init.d.ts +10 -0
  38. package/dist/services/template-init.d.ts.map +1 -0
  39. package/dist/services/template-init.js +41 -0
  40. package/dist/services/template-init.js.map +1 -0
  41. package/dist/session/file-storage.d.ts +27 -0
  42. package/dist/session/file-storage.d.ts.map +1 -0
  43. package/dist/session/file-storage.js +281 -0
  44. package/dist/session/file-storage.js.map +1 -0
  45. package/dist/session/storage.js +1 -1
  46. package/dist/session/storage.js.map +1 -1
  47. package/dist/task/state-machine.d.ts +27 -0
  48. package/dist/task/state-machine.d.ts.map +1 -0
  49. package/dist/task/state-machine.js +59 -0
  50. package/dist/task/state-machine.js.map +1 -0
  51. package/dist/task/store.d.ts +91 -0
  52. package/dist/task/store.d.ts.map +1 -0
  53. package/dist/task/store.js +317 -0
  54. package/dist/task/store.js.map +1 -0
  55. package/dist/task/types.d.ts +72 -0
  56. package/dist/task/types.d.ts.map +1 -0
  57. package/dist/task/types.js +13 -0
  58. package/dist/task/types.js.map +1 -0
  59. package/dist/templates/index.d.ts +16 -0
  60. package/dist/templates/index.d.ts.map +1 -1
  61. package/dist/templates/index.js +57 -5
  62. package/dist/templates/index.js.map +1 -1
  63. package/dist/tools/definitions.d.ts +5 -1
  64. package/dist/tools/definitions.d.ts.map +1 -1
  65. package/dist/tools/definitions.js +253 -179
  66. package/dist/tools/definitions.js.map +1 -1
  67. package/dist/tools/description-builder.d.ts +18 -0
  68. package/dist/tools/description-builder.d.ts.map +1 -0
  69. package/dist/tools/description-builder.js +88 -0
  70. package/dist/tools/description-builder.js.map +1 -0
  71. package/dist/tools/handlers.d.ts +19 -17
  72. package/dist/tools/handlers.d.ts.map +1 -1
  73. package/dist/tools/handlers.js +287 -341
  74. package/dist/tools/handlers.js.map +1 -1
  75. package/dist/types.d.ts +5 -12
  76. package/dist/types.d.ts.map +1 -1
  77. package/dist/types.js +7 -10
  78. package/dist/types.js.map +1 -1
  79. package/dist/utils/ring-buffer.d.ts +41 -0
  80. package/dist/utils/ring-buffer.d.ts.map +1 -0
  81. package/dist/utils/ring-buffer.js +83 -0
  82. package/dist/utils/ring-buffer.js.map +1 -0
  83. package/dist/wave/dag.d.ts +32 -0
  84. package/dist/wave/dag.d.ts.map +1 -0
  85. package/dist/wave/dag.js +186 -0
  86. package/dist/wave/dag.js.map +1 -0
  87. package/dist/wave/git.d.ts +57 -0
  88. package/dist/wave/git.d.ts.map +1 -0
  89. package/dist/wave/git.js +227 -0
  90. package/dist/wave/git.js.map +1 -0
  91. package/dist/wave/orchestrator.d.ts +15 -0
  92. package/dist/wave/orchestrator.d.ts.map +1 -0
  93. package/dist/wave/orchestrator.js +565 -0
  94. package/dist/wave/orchestrator.js.map +1 -0
  95. package/dist/wave/progress.d.ts +51 -0
  96. package/dist/wave/progress.d.ts.map +1 -0
  97. package/dist/wave/progress.js +176 -0
  98. package/dist/wave/progress.js.map +1 -0
  99. package/dist/wave/registry.d.ts +66 -0
  100. package/dist/wave/registry.d.ts.map +1 -0
  101. package/dist/wave/registry.js +340 -0
  102. package/dist/wave/registry.js.map +1 -0
  103. package/dist/wave/semaphore.d.ts +42 -0
  104. package/dist/wave/semaphore.d.ts.map +1 -0
  105. package/dist/wave/semaphore.js +119 -0
  106. package/dist/wave/semaphore.js.map +1 -0
  107. package/dist/wave/types.d.ts +197 -0
  108. package/dist/wave/types.d.ts.map +1 -0
  109. package/dist/wave/types.js +147 -0
  110. package/dist/wave/types.js.map +1 -0
  111. package/package.json +15 -15
@@ -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, PingToolSchema, HelpToolSchema, ListSessionsToolSchema, } from '../types.js';
4
- import { InMemorySessionStorage, } from '../session/storage.js';
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 sender reference set by server.ts after server init
24
- let notificationSender = null;
25
- export function setNotificationSender(sender) {
26
- notificationSender = sender;
27
- }
30
+ // --- Notification throttle (replaces direct notifyResourceChanged) ---
31
+ let notificationThrottle = null;
28
32
  /**
29
- * Send MCP resource-changed notification so clients refresh task resources.
33
+ * Set the notification sender. Creates a throttle that coalesces
34
+ * resource-update notifications.
30
35
  */
31
- async function notifyResourceChanged(uri) {
32
- if (!notificationSender)
33
- return;
34
- try {
35
- await notificationSender.sendNotification({
36
- method: 'notifications/resources/updated',
37
- params: { uri },
38
- });
39
- }
40
- catch {
41
- // Non-fatal — client may not support resource subscriptions
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
- * Known stderr noise patterns from codex-core internals.
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 isStderrNoise(chunk) {
65
- return STDERR_NOISE_PATTERNS.some((p) => chunk.includes(p));
56
+ export function notifyResourceChanged() {
57
+ notificationThrottle?.notify();
66
58
  }
67
59
  /**
68
- * Parse a JSONL line from codex --json output.
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 parseCodexEvent(line) {
82
- const trimmed = line.trim();
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 = ['exec', '--skip-git-repo-check', '--json'];
253
- cmdArgs.push('-o', lastMessagePath);
254
- cmdArgs.push('-c', `model="${selectedModel}"`);
255
- cmdArgs.push('-c', `model_reasoning_effort="${effort}"`);
256
- cmdArgs.push('resume', codexConversationId, enrichedPrompt);
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 = ['exec', '--json'];
260
- cmdArgs.push('-o', lastMessagePath);
261
- cmdArgs.push('--model', selectedModel);
262
- cmdArgs.push('-c', `model_reasoning_effort="${effort}"`);
263
- cmdArgs.push('--sandbox', sandbox ?? 'danger-full-access');
264
- cmdArgs.push('--dangerously-bypass-approvals-and-sandbox');
265
- cmdArgs.push('-C', cwd);
266
- cmdArgs.push('--skip-git-repo-check');
267
- cmdArgs.push(enrichedPrompt);
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 background process ---
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
- : process.env;
274
- const child = spawn('codex', cmdArgs, {
275
- shell: isWindows,
276
- env,
277
- stdio: ['pipe', 'pipe', 'pipe'],
278
- detached: false,
279
- });
280
- setProcess(task.id, child);
281
- // --- Step 7: Wire stdout for JSONL parsing ---
282
- // Only completed items are emitted as clean JSONL (no streaming intermediates).
283
- let stdoutBuffer = '';
284
- child.stdout?.on('data', (data) => {
285
- stdoutBuffer += data.toString();
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
- // Read last-message file if available
315
- let lastMessage;
316
- try {
317
- lastMessage = await readFile(lastMessagePath, 'utf-8');
318
- }
319
- catch {
320
- // File may not exist if codex didn't write it
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
- // Save turn if using a session
332
- if (activeSessionId) {
333
- const turn = {
334
- prompt,
335
- response: lastMessage || task.output.slice(-10).join('\n') || 'No output',
336
- timestamp: new Date(),
337
- };
338
- this.sessionStorage.addTurn(activeSessionId, turn);
339
- }
340
- if (code === 0 || lastMessage) {
341
- await completeTask(task.id, {
342
- exitCode: code,
343
- ...(lastMessage && { lastMessage: lastMessage.slice(0, 5000) }),
344
- });
345
- }
346
- else {
347
- await failTask(task.id, `Codex exited with code ${code}`);
348
- }
349
- // Notify resource subscribers
350
- await notifyResourceChanged(`task:///${task.id}`);
351
- await notifyResourceChanged('task:///all');
352
- });
353
- child.on('error', async (error) => {
354
- await failTask(task.id, error.message);
355
- await notifyResourceChanged(`task:///${task.id}`);
356
- await notifyResourceChanged('task:///all');
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
- // --- Step 9: Return immediately ---
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** (codex)`,
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.CODEX, error.message);
285
+ throw new ValidationError(TOOLS.SPAWN_SUBAGENT, error.message);
377
286
  }
378
- throw new ToolExecutionError(TOOLS.CODEX, 'Failed to execute codex command', error);
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.REVIEW, 'The review prompt cannot be combined with uncommitted=true. Use a base/commit review or omit the prompt.');
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.REVIEW, error.message);
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
- throw new ToolExecutionError(TOOLS.REVIEW, 'Failed to execute code review', error);
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 InMemorySessionStorage();
489
+ const sessionStorage = new FileSessionStorage();
542
490
  export const toolHandlers = {
543
- [TOOLS.CODEX]: new CodexToolHandler(sessionStorage),
544
- [TOOLS.REVIEW]: new ReviewToolHandler(),
545
- [TOOLS.PING]: new PingToolHandler(),
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