@heretyc/subagent-mcp 2.12.3 → 2.12.4

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
@@ -19,6 +19,7 @@ import { loadRoutingTable, buildCandidates, validatePresence, TASK_CATEGORIES, A
19
19
  import { createDeadlockWindow } from "./deadlock.js";
20
20
  import { createRulesetGate, RULESET_HARD_FAIL_MSG, } from "./ruleset.js";
21
21
  import { CONFIG_FILENAME, NONBLOCKING_CULL_DEPS, ZOMBIE_FORCE_GRACE_MS, ZOMBIE_LIVE_IDLE_MS, ZOMBIE_TERMINAL_IDLE_MS, buildProcessTreeKillCommands, drainZombieIntents, drainZombieReports, defaultConfigPath, globalCapMessage, readSlotMetadata, readGlobalCap, releaseSlot, reserveSlot, slotDir, writeSlotMetadata, } from "./concurrency.js";
22
+ import { shouldReapTerminalButAlive } from "./zombie.js";
22
23
  import * as orchestrationMarker from "./orchestration/marker.js";
23
24
  import * as modelMode from "./orchestration/model-mode.js";
24
25
  import { startLivenessHeartbeat } from "./orchestration/liveness.js";
@@ -246,10 +247,7 @@ function runToolMaintenance() {
246
247
  }
247
248
  const slotMeta = agent.slotPath ? readSlotMetadata(agent.slotPath) : null;
248
249
  adoptNewerSlotActivity(agent, slotMeta);
249
- const terminalButAlive = (agent.status === "finished" || agent.status === "errored" || agent.status === "stopped") &&
250
- !agent.driver.closed &&
251
- agent.exitedAt !== null &&
252
- now - agent.exitedAt > zombieTerminalIdleMs();
250
+ const terminalButAlive = shouldReapTerminalButAlive(agent, now, zombieTerminalIdleMs());
253
251
  if (terminalButAlive) {
254
252
  const record = markZombieKilled(agent, "terminal_but_alive", now);
255
253
  records.push(record);
@@ -378,6 +376,8 @@ function handleCompletedStdoutLines(agent, lines, at) {
378
376
  agent.status = "finished";
379
377
  if (agent.exitedAt === null)
380
378
  agent.exitedAt = at;
379
+ releaseSlot(agent.slotPath ?? null);
380
+ agent.slotPath = null;
381
381
  }
382
382
  }
383
383
  }
@@ -442,7 +442,7 @@ const ORCHESTRATION_INSTRUCTIONS = "subagent-mcp - CANONICAL OPERATING MODEL (fu
442
442
  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.\n\nMODEL SELECTION MODE (parallel to orchestration-mode, set via the model-selection-mode tool). DEFAULT is \"smart\" and is used whenever unset: in smart, launch_agent REJECTS any call supplying provider/model/effort selectors and the server auto-picks the best model. \"user-approved-overrides\" opens a 30-MINUTE window where selectors are HONORED, enforced LAZILY (the mode reverts to smart on the next launch_agent call after 30 minutes) and re-enabling does NOT extend an active window. HONOR-BASED: you MUST NOT set \"user-approved-overrides\" without explicit interactive USER authorization via the structured-question tool (AskUserQuestion on Claude / request-user-input on Codex); never enable it on your own initiative.";
443
443
  const server = new McpServer({
444
444
  name: "subagent-mcp",
445
- version: "2.12.3",
445
+ version: "2.12.4",
446
446
  description: "Launches always-interactive local Claude and Codex sub-agent sessions and is the orchestrator's sole launch channel. Claude runs via the Claude Agent SDK over the local Claude Code executable; Codex via `codex app-server` over stdio. The server never calls Anthropic or OpenAI HTTP APIs directly.",
447
447
  }, {
448
448
  instructions: process.env.SUBAGENT_MCP_SUBAGENT === "1"
@@ -1012,7 +1012,7 @@ server.tool("launch_agent", "Spawn a sub-agent session. CONTRACT: every `prompt`
1012
1012
  }) // ponytail: launch_agent still reaps, but callers do not receive zombie_report.
1013
1013
  );
1014
1014
  // Tool 2: poll_agent
1015
- server.tool("poll_agent", "Get an agent's current status and output. `processing` = ALIVE with visible provider activity in the last 10 min; `stalled` = ALIVE but no visible provider stream item for 10 min (thinking, or awaiting a temp-file handoff) — NOT dead, so prefer `wait`/re-poll over killing. Always returns `alive` + `idle_seconds`, plus `recent_stream` (last 3 timestamped visible stream items) and a `hint` while stalled. `verbose: true` also returns `final_output`, the agent's final assistant turn from its captured stdout.", {
1015
+ server.tool("poll_agent", "Get an agent's current status and output. `processing` = ALIVE with visible provider activity in the last 10 min; `stalled` = ALIVE but no visible provider stream item for 10 min (thinking, or awaiting a temp-file handoff) — NOT dead, so prefer `wait`/re-poll over killing. Polling refreshes the agent's idle clock. Always returns `alive` + `idle_seconds`, plus `recent_stream` (last 3 timestamped visible stream items) and a `hint` while stalled. `verbose: true` also returns `final_output`, the agent's final assistant turn from its captured stdout.", {
1016
1016
  agent_id: z.string(),
1017
1017
  verbose: z.boolean().optional().default(false),
1018
1018
  }, withMaintenance(async (params) => {
@@ -1032,6 +1032,7 @@ server.tool("poll_agent", "Get an agent's current status and output. `processing
1032
1032
  // completed/failed immediately (no up-to-10s health-monitor lag).
1033
1033
  const now = Date.now();
1034
1034
  reconcileAgent(agent, now);
1035
+ agent.lastActivity = now;
1035
1036
  const escapedStdout = escapeUntrustedTags(agent.stdout);
1036
1037
  const escapedStderr = escapeUntrustedTags(agent.stderr);
1037
1038
  const stdoutTail = envelopeUntrustedOutput(escapedStdout.length > 2000
@@ -1085,7 +1086,7 @@ server.tool("poll_agent", "Get an agent's current status and output. `processing
1085
1086
  };
1086
1087
  }, { includeZombieReport: true }));
1087
1088
  // Tool 3: kill_agent
1088
- 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.", {
1089
+ 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. A finished agent is force-killed automatically after 6 minutes of no `send_message`/`poll_agent` activity; call `kill_agent` sooner to free resources immediately. No-op for already-terminal closed agents.", {
1089
1090
  agent_id: z.string(),
1090
1091
  }, withMaintenance(async (params) => {
1091
1092
  const agent = agents.get(params.agent_id);
@@ -1262,7 +1263,7 @@ server.tool("list_agents", "List all agents with token-efficient core metrics (s
1262
1263
  };
1263
1264
  }, { includeZombieReport: true }));
1264
1265
  // Tool 6: wait
1265
- server.tool("wait", "Blocks until one or more sub-agents reach a reportable state (turn-finished, errored, stopped, or zombie_killed), returning exit code when known + local-time timestamp; or returns the live-job list after a 15-minute timeout. This is how you learn an agent finished — do NOT poll-loop. A `finished` agent with null exit_code is still alive and accepts `send_message`; a `stalled` agent is still ALIVE and does NOT end the wait. `verbose: true` adds each finished agent's `final_output`.", {
1266
+ server.tool("wait", "Blocks until one or more sub-agents reach a reportable state (turn-finished, errored, stopped, or zombie_killed), returning exit code when known + local-time timestamp; or returns the live-job list after a 15-minute timeout. This is how you learn an agent finished — do NOT poll-loop. A `finished` agent with null exit_code is still alive and accepts `send_message`; `send_message`/`poll_agent` activity keeps it alive, and 6 minutes idle auto-kills it. A `stalled` agent is still ALIVE and does NOT end the wait. `verbose: true` adds each finished agent's `final_output`.", {
1266
1267
  verbose: z.boolean().optional().default(false),
1267
1268
  }, withMaintenance(async (params, zombieRecords) => {
1268
1269
  const { verbose } = params;
package/dist/zombie.js CHANGED
@@ -3,7 +3,7 @@ import { platform } from "node:os";
3
3
  import { basename, dirname, join } from "node:path";
4
4
  import { execFileSync } from "node:child_process";
5
5
  export const ZOMBIE_LIVE_IDLE_MS = 6 * 60 * 1000;
6
- export const ZOMBIE_TERMINAL_IDLE_MS = 30 * 1000;
6
+ export const ZOMBIE_TERMINAL_IDLE_MS = 6 * 60 * 1000;
7
7
  export const ZOMBIE_FORCE_GRACE_MS = 20 * 1000;
8
8
  export const ZOMBIE_INTENTS_FILENAME = "zombie-intents.jsonl";
9
9
  export const ZOMBIE_REPORTS_FILENAME = "zombie-reports.jsonl";
@@ -72,7 +72,7 @@ export function writeSlotMetadata(slotPath, metadata) {
72
72
  };
73
73
  writeFileSync(slotPath, JSON.stringify(full), { mode: 0o600 });
74
74
  }
75
- function drainJsonl(path) {
75
+ export function drainJsonl(path) {
76
76
  if (!existsSync(path))
77
77
  return [];
78
78
  const claim = join(dirname(path), `${basename(path)}.${process.pid}.drain`);
@@ -84,7 +84,14 @@ function drainJsonl(path) {
84
84
  }
85
85
  try {
86
86
  const records = [];
87
- for (const line of readFileSync(claim, "utf8").split(/\r?\n/).filter(Boolean)) {
87
+ let raw;
88
+ try {
89
+ raw = readFileSync(claim, "utf8");
90
+ }
91
+ catch {
92
+ return [];
93
+ }
94
+ for (const line of raw.split(/\r?\n/).filter(Boolean)) {
88
95
  try {
89
96
  records.push(JSON.parse(line));
90
97
  }
@@ -101,6 +108,12 @@ function drainJsonl(path) {
101
108
  catch { }
102
109
  }
103
110
  }
111
+ export function shouldReapTerminalButAlive(agent, now, terminalIdleMs) {
112
+ return ((agent.status === "finished" || agent.status === "errored" || agent.status === "stopped") &&
113
+ !agent.driver.closed &&
114
+ agent.exitedAt !== null &&
115
+ now - Math.max(agent.exitedAt, agent.lastActivity) > terminalIdleMs);
116
+ }
104
117
  export function buildProcessTreeKillCommands(pid, p = platform()) {
105
118
  if (p === "win32") {
106
119
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heretyc/subagent-mcp",
3
- "version": "2.12.3",
3
+ "version": "2.12.4",
4
4
  "description": "MCP server that launches and manages always-interactive Claude Code and Codex sub-agent sessions (no direct Anthropic/OpenAI API).",
5
5
  "keywords": [
6
6
  "mcp",
@@ -38,7 +38,7 @@
38
38
  "postinstall": "node scripts/postinstall.mjs",
39
39
  "prepare": "npm run build",
40
40
  "prepublishOnly": "npm test",
41
- "test": "npm run check:versions && node test/effort.test.mjs && node test/drivers.test.mjs && node test/platform.test.mjs && node test/wait.test.mjs && node test/status.test.mjs && node test/output.test.mjs && node test/stream.test.mjs && node test/routing.test.mjs && node test/deadlock.test.mjs && node test/handler-validation.test.mjs && node test/index-handler.test.mjs && node test/ruleset.test.mjs && node test/ruleset-exec.test.mjs && node test/ruleset-handler.test.mjs && node test/failover.test.mjs && node test/global-concurrency-cap.test.mjs && node test/update-check.test.mjs && node test/zombie.test.mjs && node test/orchestration-marker.test.mjs && node test/orchestration-hook-core.test.mjs && node test/orchestration-adapters.test.mjs && node test/orchestration-pretool.test.mjs && node test/orchestration-directives.test.mjs && node test/no-five-call.test.mjs && node test/no-per-provider-cap.test.mjs && node test/rag-pointers.test.mjs && node test/check-worktree-subagent.test.mjs && node test/launch-agent-upsert.test.mjs && node test/claude-session-limit.test.mjs && node test/init-migration.test.mjs && node test/init-global.test.mjs && node test/mirror-fragments.test.mjs && node test/performance-tier-effort.test.mjs && node test/setup-repair.test.mjs && node test/setup-quoting.test.mjs && node test/setup-wire.test.mjs && node test/setup-cli-integration.test.mjs && node test/init.test.mjs && node test/model-selection-mode.test.mjs && node test/cli-args.test.mjs && node test/index-guard.test.mjs && node test/drivers-guard.test.mjs && node test/audit-next13-lb3.test.mjs && node test/zombie-guard.test.mjs && node test/concurrency-guard.test.mjs && node test/hook-core-guard.test.mjs && node scripts/validate_provider.mjs && node scripts/validate_seed_sites.mjs && node scripts/validate_routing_audit.mjs && node test/seed-sites.test.mjs && node test/mcp-compliance.test.mjs"
41
+ "test": "npm run check:versions && node test/effort.test.mjs && node test/drivers.test.mjs && node test/platform.test.mjs && node test/wait.test.mjs && node test/status.test.mjs && node test/output.test.mjs && node test/stream.test.mjs && node test/routing.test.mjs && node test/deadlock.test.mjs && node test/handler-validation.test.mjs && node test/index-handler.test.mjs && node test/ruleset.test.mjs && node test/ruleset-exec.test.mjs && node test/ruleset-handler.test.mjs && node test/failover.test.mjs && node test/global-concurrency-cap.test.mjs && node test/update-check.test.mjs && node test/zombie.test.mjs && node test/zombie-reap-idle.test.mjs && node test/orchestration-marker.test.mjs && node test/orchestration-hook-core.test.mjs && node test/orchestration-adapters.test.mjs && node test/orchestration-pretool.test.mjs && node test/orchestration-directives.test.mjs && node test/no-five-call.test.mjs && node test/no-per-provider-cap.test.mjs && node test/rag-pointers.test.mjs && node test/check-worktree-subagent.test.mjs && node test/launch-agent-upsert.test.mjs && node test/claude-session-limit.test.mjs && node test/init-migration.test.mjs && node test/init-global.test.mjs && node test/mirror-fragments.test.mjs && node test/performance-tier-effort.test.mjs && node test/setup-repair.test.mjs && node test/setup-quoting.test.mjs && node test/setup-wire.test.mjs && node test/setup-cli-integration.test.mjs && node test/init.test.mjs && node test/model-selection-mode.test.mjs && node test/cli-args.test.mjs && node test/index-guard.test.mjs && node test/drivers-guard.test.mjs && node test/audit-next13-lb3.test.mjs && node test/zombie-guard.test.mjs && node test/concurrency-guard.test.mjs && node test/hook-core-guard.test.mjs && node scripts/validate_provider.mjs && node scripts/validate_seed_sites.mjs && node scripts/validate_routing_audit.mjs && node test/seed-sites.test.mjs && node test/mcp-compliance.test.mjs"
42
42
  },
43
43
  "author": "Lexi Blackburn",
44
44
  "license": "Apache-2.0",