@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 +9 -8
- package/dist/zombie.js +16 -3
- package/package.json +2 -2
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
|
|
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.
|
|
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`;
|
|
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 =
|
|
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
|
-
|
|
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
|
+
"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",
|