@heretyc/subagent-mcp 2.8.3 → 2.8.5

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/index.js CHANGED
@@ -9,11 +9,12 @@ import { isAbsolute, basename, join, dirname } from "node:path";
9
9
  import { tmpdir } from "node:os";
10
10
  import { pathToFileURL } from "url";
11
11
  import { buildCommand } from "./effort.js";
12
+ import { createProviderDriver } from "./drivers.js";
12
13
  import { resolveExeFor } from "./platform.js";
13
14
  import { formatLocalIso, selectUnreported } from "./wait-helpers.js";
14
15
  import { computeStatusTransition, buildLivenessFields, } from "./status-helpers.js";
15
16
  import { extractFinalTurn } from "./output-helpers.js";
16
- import { consumeStreamChunk, flushStream, retainLastN, } from "./stream-helpers.js";
17
+ import { consumeStreamChunk, flushStream, isTurnCompletedLine, retainLastN, } from "./stream-helpers.js";
17
18
  import { loadRoutingTable, buildCandidates, validatePresence, TASK_CATEGORIES, AUTO_HINT, SPLIT_HINT, } from "./routing.js";
18
19
  import { createDeadlockWindow } from "./deadlock.js";
19
20
  import { createRulesetGate, RULESET_HARD_FAIL_MSG, } from "./ruleset.js";
@@ -27,14 +28,13 @@ const deadlockWindow = createDeadlockWindow();
27
28
  // scoping. The env-check runs lazily at the FIRST launch_agent call; success
28
29
  // latches enabled/disabled for the process lifetime, failure never latches.
29
30
  const rulesetGate = createRulesetGate();
30
- // Post-spawn grace window (ms). A child that exits within this window after a
31
- // successful spawn never launched (codex installed but not logged in, expired
32
- // auth, instant crash) — the attempt loop silently advances instead of falsely
33
- // reporting success. ANY exit within the window counts, even code 0, EXCEPT a
34
- // codex child already finalized by its turn.completed marker (legitimate fast
35
- // completion). SUBAGENT_SPAWN_GRACE_MS overrides (non-negative int; 0 disables
36
- // detection = legacy spawn-event-only success) — a test seam; production never
37
- // sets it.
31
+ // Post-spawn grace window (ms). A provider driver that exits within this window
32
+ // after a successful driver start never launched (not logged in, expired auth, instant
33
+ // crash) — the attempt loop silently advances instead of falsely reporting
34
+ // success. ANY exit within the window counts, even code 0, EXCEPT a driver
35
+ // already finalized by its turn-completed marker (legitimate fast completion).
36
+ // SUBAGENT_SPAWN_GRACE_MS overrides (non-negative int; 0 disables only this
37
+ // post-start early-exit detection) — a test seam; production never sets it.
38
38
  const SPAWN_GRACE_MS = (() => {
39
39
  const raw = process.env.SUBAGENT_SPAWN_GRACE_MS;
40
40
  if (raw === undefined || raw === "")
@@ -120,8 +120,8 @@ const ORCHESTRATION_INSTRUCTIONS = "BINDING IN BOTH MODES - SOLE CHANNEL: while
120
120
  const SUBAGENT_INSTRUCTIONS = "SUB-AGENT SESSION: you are a child process launched by subagent-mcp. Follow the parent prompt. Do not treat yourself as the orchestrator, do not re-trigger orchestration carryover, and do not launch further sub-agents unless the parent prompt explicitly assigns that.";
121
121
  const server = new McpServer({
122
122
  name: "subagent-mcp",
123
- version: "2.3.9",
124
- description: "Spawns the LOCALLY INSTALLED `claude` and `codex` CLI binaries as child processes. Does NOT call the Anthropic or OpenAI HTTP APIs directly (no API keys, no SDK) and there are no plans to all model access is via the local CLIs.",
123
+ version: "2.8.5",
124
+ description: "Launches always-interactive local Claude and Codex sub-agent sessions. Claude uses the Claude Agent SDK over the local Claude Code executable; Codex uses `codex app-server` over stdio. The server does not call Anthropic or OpenAI HTTP APIs directly.",
125
125
  }, {
126
126
  instructions: process.env.SUBAGENT_MCP_SUBAGENT === "1"
127
127
  ? SUBAGENT_INSTRUCTIONS
@@ -140,7 +140,7 @@ function cleanupUcSettingsPath(ucSettingsPath) {
140
140
  catch { }
141
141
  }
142
142
  // Attempt to spawn + register a single candidate. Resolves to the agent_id on a
143
- // successful spawn, or a launch-time failure reason string (never throws/rejects).
143
+ // successful driver start, or a launch-time failure reason string (never throws/rejects).
144
144
  //
145
145
  // spawn() failures (ENOENT/EACCES) are ASYNC: a missing/broken CLI emits the
146
146
  // child 'error' event AFTER spawn() returns, so a try/catch around spawn cannot
@@ -165,7 +165,7 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
165
165
  let buildResult;
166
166
  let cmd;
167
167
  try {
168
- buildResult = buildCommand(candidate.provider, candidate.model, effortForBuild, prompt, agentCwd);
168
+ buildResult = buildCommand(candidate.provider, candidate.model, effortForBuild, agentCwd);
169
169
  cmd = resolveExe(candidate.provider);
170
170
  }
171
171
  catch (e) {
@@ -177,14 +177,17 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
177
177
  cleanupUcSettingsPath(buildResult.ucSettingsPath);
178
178
  return { reason: `CLI executable not found: ${cmd}` };
179
179
  }
180
- const stdinMode = candidate.provider === "claude" ? "pipe" : "ignore";
181
- let childProcess;
180
+ let driver;
182
181
  try {
183
- childProcess = spawn(cmd, buildResult.args, {
182
+ driver = await createProviderDriver({
183
+ provider: candidate.provider,
184
+ command: cmd,
185
+ args: buildResult.args,
184
186
  cwd: agentCwd,
185
187
  env: { ...process.env, SUBAGENT_MCP_SUBAGENT: "1" },
186
- stdio: [stdinMode, "pipe", "pipe"],
187
- windowsHide: true,
188
+ model: candidate.model,
189
+ effort: candidate.effort,
190
+ ucSettingsPath: buildResult.ucSettingsPath,
188
191
  });
189
192
  }
190
193
  catch (error) {
@@ -192,6 +195,7 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
192
195
  cleanupUcSettingsPath(buildResult.ucSettingsPath);
193
196
  return { reason: error instanceof Error ? error.message : String(error) };
194
197
  }
198
+ const childProcess = driver.process;
195
199
  // Await the one-shot spawn/error race. The 'error' handler is attached BEFORE
196
200
  // we await so an async ENOENT cannot escape as an unhandled event.
197
201
  try {
@@ -208,7 +212,7 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
208
212
  // Launch-time failure (ENOENT/EACCES/etc.) — kill if somehow alive, clean up
209
213
  // the settings file, and report so the attempt loop advances.
210
214
  try {
211
- childProcess.kill();
215
+ driver.kill();
212
216
  }
213
217
  catch { }
214
218
  cleanupUcSettingsPath(buildResult.ucSettingsPath);
@@ -229,6 +233,7 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
229
233
  : {}),
230
234
  status: "processing",
231
235
  process: childProcess,
236
+ driver,
232
237
  stdout: "",
233
238
  stderr: "",
234
239
  exitCode: null,
@@ -250,15 +255,6 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
250
255
  // item, so it does NOT refresh the heartbeat.
251
256
  agentState.stderr += `\n[process error] ${err instanceof Error ? err.message : String(err)}`;
252
257
  });
253
- if (candidate.provider === "claude" && childProcess.stdin) {
254
- // EPIPE if the child dies before draining the prompt (the grace-window
255
- // early-exit class) — fold into stderr, never crash the server.
256
- childProcess.stdin.on("error", (err) => {
257
- agentState.stderr += `\n[stdin error] ${err instanceof Error ? err.message : String(err)}`;
258
- });
259
- childProcess.stdin.write(prompt);
260
- childProcess.stdin.end();
261
- }
262
258
  if (childProcess.stdout) {
263
259
  childProcess.stdout.on("data", (data) => {
264
260
  const chunk = data.toString();
@@ -278,17 +274,15 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
278
274
  agentState.lastActivity = at;
279
275
  agentState.visibleStream = retainLastN(agentState.visibleStream, items.map((it) => ({ ...it, at })), 3);
280
276
  }
281
- // Codex emits JSONL; turn.completed signals task done kill process. Scan
282
- // COMPLETE lines only so a marker split across chunks is matched once
283
- // fully assembled (never on a partial fragment).
284
- if (agentState.provider === "codex" &&
285
- lines.some((l) => l.includes('"type":"turn.completed"'))) {
277
+ // Provider completion events mark the current turn finished while the
278
+ // logical interactive session can remain available for later messages.
279
+ // Scan COMPLETE lines only so a marker split across chunks is matched
280
+ // once fully assembled (never on a partial fragment).
281
+ if (lines.some((l) => isTurnCompletedLine(agentState.provider, l))) {
286
282
  agentState.turnCompleted = true;
287
283
  agentState.status = "finished";
288
- agentState.exitCode = 0;
289
284
  if (agentState.exitedAt === null)
290
285
  agentState.exitedAt = at;
291
- childProcess.kill();
292
286
  }
293
287
  });
294
288
  }
@@ -309,11 +303,13 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
309
303
  for (const line of lines) {
310
304
  agentState.stdout += line + "\n";
311
305
  }
312
- // A turn.completed marker may arrive only in this final flush (no
313
- // trailing newline) — the grace window's success exception needs it.
314
- if (agentState.provider === "codex" &&
315
- lines.some((l) => l.includes('"type":"turn.completed"'))) {
306
+ // A completion marker may arrive only in this final flush (no trailing
307
+ // newline) — the grace window's success exception needs it.
308
+ if (lines.some((l) => isTurnCompletedLine(agentState.provider, l))) {
316
309
  agentState.turnCompleted = true;
310
+ agentState.status = "finished";
311
+ if (agentState.exitedAt === null)
312
+ agentState.exitedAt = at;
317
313
  }
318
314
  if (items.length > 0) {
319
315
  agentState.lastActivity = at;
@@ -332,7 +328,12 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
332
328
  return;
333
329
  }
334
330
  if (agentState.status === "finished") {
335
- // Already finalized by turn.completed; exitedAt already stamped
331
+ // Already finalized by turn.completed; record that the interactive
332
+ // driver is no longer live so poll/list stop advertising alive=true.
333
+ if (agentState.exitCode === null)
334
+ agentState.exitCode = code !== null ? code : -1;
335
+ if (code !== 0)
336
+ agentState.status = "errored";
336
337
  return;
337
338
  }
338
339
  // Normal exit: set exit code and derive status
@@ -346,28 +347,51 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
346
347
  const closedAfterFlush = new Promise((resolve) => {
347
348
  childProcess.once("close", () => resolve());
348
349
  });
349
- // Post-spawn grace window: a 'spawn' win alone is NOT success a binary that
350
- // spawns then dies immediately (codex installed but not logged in) must
351
- // advance the attempt loop, not falsely conclude it. AgentState is fully
352
- // wired and the claude prompt already written above, so a surviving child
353
- // loses no stream output during the wait. Exception: a codex child already
354
- // finalized by its turn.completed marker (dedicated turnCompleted flag — the
355
- // SOLE in-window success exception; visibility-and-failover.md) completed
356
- // the task legitimately fast — that is a success, never a launch failure.
357
- // The close handler above cleans up a condemned child (uc settings, stream
358
- // flush); the agent is simply never registered.
350
+ // POSIX seam: a child can die and deliver 'exit' DURING the startup write
351
+ // below (the write then rejects with EPIPE) before the grace block attaches
352
+ // its own listener. Capture that exit here so the grace window still sees it
353
+ // and reports the exit code, instead of registering a corpse or surfacing the
354
+ // raw EPIPE as the failure reason.
355
+ let earlyExitInfo = null;
356
+ childProcess.once("exit", (code, signal) => {
357
+ earlyExitInfo = { code, signal };
358
+ });
359
+ // The startup write is best-effort. On POSIX, writing to an already-dead
360
+ // child's stdin rejects with EPIPE; that is NOT itself a launch failure.
361
+ // Record it and fall through to the grace window, which reports the real exit
362
+ // code (grace>0) or the legacy startup-write seam registers it (grace=0).
363
+ let startError = null;
364
+ try {
365
+ await driver.start(prompt);
366
+ }
367
+ catch (err) {
368
+ startError = err instanceof Error ? err : new Error(String(err));
369
+ }
370
+ // Post-spawn grace window: a 'spawn' win alone is NOT success — a provider
371
+ // driver that starts then dies immediately must advance the attempt loop, not
372
+ // falsely conclude it. AgentState is fully wired and the initial turn already
373
+ // submitted above, so a surviving driver loses no stream output during the
374
+ // wait. Exception: a driver already finalized by its turn-completion marker
375
+ // completed the task legitimately fast — that is a success, never a launch
376
+ // failure. The close handler above cleans up a condemned driver (uc settings,
377
+ // stream flush); the agent is simply never registered.
359
378
  if (SPAWN_GRACE_MS > 0) {
360
- const earlyExit = await new Promise((resolve) => {
361
- const timer = setTimeout(() => {
362
- childProcess.removeListener("exit", onExit);
363
- resolve(null);
364
- }, SPAWN_GRACE_MS);
365
- const onExit = (code, signal) => {
366
- clearTimeout(timer);
367
- resolve({ code, signal });
368
- };
369
- childProcess.once("exit", onExit);
370
- });
379
+ const earlyExit = earlyExitInfo ??
380
+ (await new Promise((resolve) => {
381
+ if (earlyExitInfo) {
382
+ resolve(earlyExitInfo);
383
+ return;
384
+ }
385
+ const timer = setTimeout(() => {
386
+ childProcess.removeListener("exit", onExit);
387
+ resolve(null);
388
+ }, SPAWN_GRACE_MS);
389
+ const onExit = (code, signal) => {
390
+ clearTimeout(timer);
391
+ resolve({ code, signal });
392
+ };
393
+ childProcess.once("exit", onExit);
394
+ }));
371
395
  if (earlyExit) {
372
396
  // 'exit' can be delivered before the final stdout chunk, so wait for
373
397
  // 'close' (streams drained, flush scanned) before deciding — a
@@ -381,6 +405,17 @@ async function tryLaunchCandidate(candidate, prompt, agentCwd, routingTier, rule
381
405
  };
382
406
  }
383
407
  }
408
+ else if (startError && !agentState.turnCompleted) {
409
+ // Lived past the grace window but the startup write failed: the child is
410
+ // not accepting input, so advance the loop rather than register an agent
411
+ // that never received its prompt.
412
+ try {
413
+ driver.kill();
414
+ }
415
+ catch { }
416
+ cleanupUcSettings(agentState);
417
+ return { reason: startError.message };
418
+ }
384
419
  }
385
420
  agents.set(agentId, agentState);
386
421
  return { agentId };
@@ -394,7 +429,7 @@ function sameTriples(a, b) {
394
429
  return a.every((c, i) => c.provider === b[i].provider && c.model === b[i].model && c.effort === b[i].effort);
395
430
  }
396
431
  // Tool 1: launch_agent
397
- server.tool("launch_agent", "Spawn a sub-agent. AUTO MODE (mandatory first attempt unless an override is licensed below): pass only `prompt` + `task_category` and NO overrides; the server picks the best provider/model/effort for that category from its routing table, launches the top candidate, and silently falls back to the next-best on launch failure. `provider`/`model`/`effort` are overrides — licensed on 1st/2nd attempts ONLY when the task verifiably requires a specific capability; STATE that capability when overriding; if you pass `model` you must also pass `provider`, and if you pass `effort` you must pass both `provider` and `model`. SOLE CHANNEL: while this server is connected this tool is the ONLY sanctioned way to spawn sub-agents, in BOTH orchestration states — harness-native Task/Agent tools are FORBIDDEN for sub-agent launches. PROMPT RULE: the FIRST line of every `prompt` MUST be \"<this is a request from a parent process>\" (sub-agent self-identification). Unsure which task_category fits? Don't submit one amorphous task — SPLIT into atomic steps that each map to a single category, one agent per step. ultracode effort is Opus-4.8+ only (induced via a temp `--settings {\"ultracode\":true}` file; the CLI rejects `--effort ultracode`). Each sub-agent is a separate claude/codex CLI child that does NOT inherit this session's MCP servers; children run with env SUBAGENT_MCP_SUBAGENT=1 so the orchestration hooks skip them (they are not orchestrators and don't re-trigger carryover). Launch returns status `processing` (alive); a later `stalled` is alive-but-quiet (thinking or awaiting a temp-file handoff), NOT dead — wait or re-poll, don't kill (see poll_agent). DEADLOCK RULE: you MUST ALWAYS set `deadlock=true` when 2 launch attempts for the SAME atomic task have already failed or been unsatisfactory (the 3rd attempt onward; re-wording or re-splitting the prompt does NOT make it a different task), and NEVER otherwise — from the 3rd attempt deadlock outranks any capability override: drop provider/model/effort.", {
432
+ server.tool("launch_agent", "Spawn a sub-agent session. AUTO MODE (mandatory first attempt unless an override is licensed below): pass only `prompt` + `task_category` and NO overrides; the server picks the best provider/model/effort for that category from its routing table, launches the top candidate, and silently falls back to the next-best on launch failure. `provider`/`model`/`effort` are overrides — licensed on 1st/2nd attempts ONLY when the task verifiably requires a specific capability; STATE that capability when overriding; if you pass `model` you must also pass `provider`, and if you pass `effort` you must pass both `provider` and `model`. SOLE CHANNEL: while this server is connected this tool is the ONLY sanctioned way to spawn sub-agents, in BOTH orchestration states — harness-native Task/Agent tools are FORBIDDEN for sub-agent launches. PROMPT RULE: the FIRST line of every `prompt` MUST be \"<this is a request from a parent process>\" (sub-agent self-identification). Unsure which task_category fits? Don't submit one amorphous task — SPLIT into atomic steps that each map to a single category, one agent per step. ultracode effort is Opus 4.8+ only. Claude uses a Claude Agent SDK logical session over the local Claude executable; Codex uses a `codex app-server` child. Children run with env SUBAGENT_MCP_SUBAGENT=1 so the orchestration hooks skip them (they are not orchestrators and don't re-trigger carryover). Launch returns status `processing` (alive); a later `stalled` is alive-but-quiet (thinking or awaiting a temp-file handoff), NOT dead — wait or re-poll, don't kill (see poll_agent). DEADLOCK RULE: you MUST ALWAYS set `deadlock=true` when 2 launch attempts for the SAME atomic task have already failed or been unsatisfactory (the 3rd attempt onward; re-wording or re-splitting the prompt does NOT make it a different task), and NEVER otherwise — from the 3rd attempt deadlock outranks any capability override: drop provider/model/effort.", {
398
433
  task_category: z.enum(TASK_CATEGORIES).describe(TASK_CATEGORY_GLOSS),
399
434
  prompt: z.string().min(1),
400
435
  provider: z.enum(["claude", "codex"]).optional(),
@@ -491,7 +526,7 @@ server.tool("launch_agent", "Spawn a sub-agent. AUTO MODE (mandatory first attem
491
526
  }
492
527
  candidates = applied.candidates;
493
528
  }
494
- // 6. Attempt loop: best→worst. Register on first successful spawn; silently
529
+ // 6. Attempt loop: best→worst. Register on first successful driver start; silently
495
530
  // advance on launch-time failure. Sub-agent task outcome is NEVER a trigger.
496
531
  const skipped = [];
497
532
  for (const candidate of candidates) {
@@ -608,7 +643,7 @@ server.tool("poll_agent", "Get an agent's current status and output. Status `pro
608
643
  };
609
644
  });
610
645
  // Tool 3: kill_agent
611
- server.tool("kill_agent", "Terminate a live agent (status `processing` or `stalled`) by immediately force-killing its managed process tree. No-op for already-terminal agents.", {
646
+ server.tool("kill_agent", "Terminate a live agent/session (status `processing`, `stalled`, or turn-finished but still interactive) by immediately force-killing its managed driver. No-op for already-terminal closed agents.", {
612
647
  agent_id: z.string(),
613
648
  }, async (params) => {
614
649
  const agent = agents.get(params.agent_id);
@@ -623,9 +658,11 @@ server.tool("kill_agent", "Terminate a live agent (status `processing` or `stall
623
658
  isError: true,
624
659
  };
625
660
  }
626
- // Kill applies to ALL live states (processing OR stalled). A terminal agent
627
- // (finished/errored/stopped) is a no-op.
628
- const isLive = agent.status === "processing" || agent.status === "stalled";
661
+ // Kill applies to ALL live driver states (processing, stalled, or finished
662
+ // current turn with an open interactive session). Closed terminal agents are
663
+ // a no-op.
664
+ const isLive = (agent.status === "processing" || agent.status === "stalled" || agent.status === "finished") &&
665
+ !agent.driver.closed;
629
666
  if (!isLive) {
630
667
  return {
631
668
  content: [
@@ -645,6 +682,7 @@ server.tool("kill_agent", "Terminate a live agent (status `processing` or `stall
645
682
  // grace period. On Windows, taskkill /t /f tears down the whole tree; on
646
683
  // POSIX, SIGKILL the process (close handler records the real exit code).
647
684
  agent.status = "stopped";
685
+ agent.driver.kill();
648
686
  if (isWindows && agent.process.pid) {
649
687
  spawn("taskkill", ["/pid", String(agent.process.pid), "/t", "/f"], {
650
688
  windowsHide: true,
@@ -683,7 +721,7 @@ server.tool("kill_agent", "Terminate a live agent (status `processing` or `stall
683
721
  }
684
722
  });
685
723
  // Tool 4: send_message
686
- server.tool("send_message", "Send a message to a running agent's stdin", {
724
+ server.tool("send_message", "Enqueue a user message for an open interactive agent session. Observe output with poll_agent or wait.", {
687
725
  agent_id: z.string(),
688
726
  message: z.string().min(1),
689
727
  }, async (params) => {
@@ -699,7 +737,8 @@ server.tool("send_message", "Send a message to a running agent's stdin", {
699
737
  isError: true,
700
738
  };
701
739
  }
702
- const isLive = agent.status === "processing" || agent.status === "stalled";
740
+ const isLive = (agent.status === "processing" || agent.status === "stalled" || agent.status === "finished") &&
741
+ !agent.driver.closed;
703
742
  if (!isLive) {
704
743
  return {
705
744
  content: [
@@ -711,20 +750,15 @@ server.tool("send_message", "Send a message to a running agent's stdin", {
711
750
  isError: true,
712
751
  };
713
752
  }
714
- if (!agent.process.stdin) {
715
- return {
716
- content: [
717
- {
718
- type: "text",
719
- text: `Error: Agent stdin is not available`,
720
- },
721
- ],
722
- isError: true,
723
- };
724
- }
725
753
  try {
726
- agent.process.stdin.write(params.message + "\n");
727
- agent.lastActivity = Date.now();
754
+ await agent.driver.send(params.message);
755
+ const now = Date.now();
756
+ agent.status = "processing";
757
+ agent.exitCode = null;
758
+ agent.exitedAt = null;
759
+ agent.waitReported = false;
760
+ agent.turnCompleted = false;
761
+ agent.lastActivity = now;
728
762
  return {
729
763
  content: [
730
764
  {
@@ -732,7 +766,7 @@ server.tool("send_message", "Send a message to a running agent's stdin", {
732
766
  text: JSON.stringify({
733
767
  agent_id: agent.id,
734
768
  status: "sent",
735
- message: "Message written to agent stdin",
769
+ message: "Message accepted by provider driver",
736
770
  }),
737
771
  },
738
772
  ],
@@ -781,7 +815,7 @@ server.tool("list_agents", "List all agents with token-efficient core metrics (s
781
815
  };
782
816
  });
783
817
  // Tool 6: wait
784
- server.tool("wait", "Blocks until one or more sub-agents reach a terminal state (finished/errored/stopped), returning each one's exit code + local-time exit timestamp; or returns the live-job list after a 15-minute timeout. A `stalled` agent is still ALIVE and does NOT end the wait — only a terminal exit does. Pass `verbose: true` to add each finished agent's `final_output` (its final assistant turn, extracted from captured stdout).", {
818
+ server.tool("wait", "Blocks until one or more sub-agents reach a reportable state (turn-finished, errored, or stopped), returning exit code when known + local-time timestamp; or returns the live-job list after a 15-minute timeout. A `finished` agent can still be alive and accept `send_message` when exit_code is null. A `stalled` agent is still ALIVE and does NOT end the wait. Pass `verbose: true` to add each finished agent's `final_output`.", {
785
819
  verbose: z.boolean().optional().default(false),
786
820
  }, async (params) => {
787
821
  const { verbose } = params;
@@ -4,13 +4,36 @@
4
4
  function rawFallback(stdout) {
5
5
  return (stdout || "").trim();
6
6
  }
7
- // Pull a final assistant-message string out of one parsed codex `--json` event.
8
- // Codex emits newline-delimited JSON; the final assistant message has appeared
9
- // under a few shapes across CLI versions, so match tolerantly.
7
+ // Pull a final assistant-message string out of one parsed Codex event. Codex
8
+ // app-server emits JSON-RPC notifications, while older CLI JSONL used top-level
9
+ // event objects, so match tolerantly.
10
10
  function codexEventText(evt) {
11
11
  if (!evt || typeof evt !== "object")
12
12
  return null;
13
13
  const e = evt;
14
+ if (typeof e.method === "string" && e.params && typeof e.params === "object") {
15
+ const params = e.params;
16
+ if (e.method === "item/agentMessage/delta" && typeof params.delta === "string") {
17
+ return params.delta;
18
+ }
19
+ if (e.method === "item/completed" && params.item && typeof params.item === "object") {
20
+ const item = params.item;
21
+ if (item.type === "agentMessage" && typeof item.text === "string")
22
+ return item.text;
23
+ }
24
+ if (e.method === "turn/completed" && params.turn && typeof params.turn === "object") {
25
+ const turn = params.turn;
26
+ const items = Array.isArray(turn.items) ? turn.items : [];
27
+ for (let i = items.length - 1; i >= 0; i--) {
28
+ const item = items[i];
29
+ if (item && typeof item === "object") {
30
+ const obj = item;
31
+ if (obj.type === "agentMessage" && typeof obj.text === "string")
32
+ return obj.text;
33
+ }
34
+ }
35
+ }
36
+ }
14
37
  // Shape A: { type: "agent_message", message: "..." }
15
38
  if (e.type === "agent_message" && typeof e.message === "string") {
16
39
  return e.message;