@heretyc/subagent-mcp 2.9.2 → 2.10.1
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/LICENSE +201 -201
- package/NOTICE +5 -5
- package/README.md +184 -190
- package/dist/concurrency.js +108 -0
- package/dist/config-scaffold.js +2 -0
- package/dist/global-concurrency.jsonc +28 -0
- package/dist/hooks/orchestration-codex.js +9 -6
- package/dist/index.js +337 -77
- package/dist/orchestration/hook-core.js +35 -4
- package/dist/orchestration/pretool.js +9 -4
- package/dist/setup.js +1 -0
- package/dist/status-helpers.js +2 -2
- package/dist/wait-helpers.js +1 -1
- package/dist/zombie.js +189 -0
- package/package.json +7 -6
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { serverAlive } from "./liveness.js";
|
|
2
|
+
import { cullHookZombies, hookZombieReportText } from "./hook-core.js";
|
|
2
3
|
const NATIVE_SUBAGENT_TOOLS = new Set(["Task", "Agent", "Explore"]);
|
|
3
4
|
function decision(permissionDecision, permissionDecisionReason, additionalContext) {
|
|
4
5
|
return {
|
|
@@ -21,17 +22,21 @@ function decision(permissionDecision, permissionDecisionReason, additionalContex
|
|
|
21
22
|
*/
|
|
22
23
|
export function runClaudePreTool(payload, env, now = Date.now()) {
|
|
23
24
|
try {
|
|
25
|
+
const zombieReport = hookZombieReportText(cullHookZombies());
|
|
24
26
|
if (env.SUBAGENT_MCP_SUBAGENT === "1")
|
|
25
27
|
return null;
|
|
28
|
+
const zombieAllowedDecision = zombieReport
|
|
29
|
+
? decision("allow", "zombies culled; allowing requested tool.", zombieReport)
|
|
30
|
+
: null;
|
|
26
31
|
if (!serverAlive(now))
|
|
27
|
-
return
|
|
32
|
+
return zombieAllowedDecision;
|
|
28
33
|
const tool = typeof payload.tool_name === "string" ? payload.tool_name : "";
|
|
29
34
|
if (!tool)
|
|
30
|
-
return
|
|
35
|
+
return zombieAllowedDecision;
|
|
31
36
|
if (NATIVE_SUBAGENT_TOOLS.has(tool)) {
|
|
32
|
-
return decision("deny", "subagent-mcp is alive; harness-native Task/Agent/Explore is not the sanctioned sub-agent channel. Use the subagent-mcp launch_agent MCP tool with the parent-process sentinel as prompt line 1.");
|
|
37
|
+
return decision("deny", "subagent-mcp is alive; harness-native Task/Agent/Explore is not the sanctioned sub-agent channel. Use the subagent-mcp launch_agent MCP tool with the parent-process sentinel as prompt line 1.", zombieReport || undefined);
|
|
33
38
|
}
|
|
34
|
-
return
|
|
39
|
+
return zombieAllowedDecision;
|
|
35
40
|
}
|
|
36
41
|
catch {
|
|
37
42
|
return null;
|
package/dist/setup.js
CHANGED
|
@@ -227,6 +227,7 @@ export function verifyInstall(root = INSTALL_ROOT) {
|
|
|
227
227
|
const required = [
|
|
228
228
|
"dist/index.js",
|
|
229
229
|
"dist/advanced-ruleset.py",
|
|
230
|
+
"dist/global-concurrency.jsonc",
|
|
230
231
|
"dist/hooks/orchestration-claude.js",
|
|
231
232
|
"dist/hooks/orchestration-claude-pretool.js",
|
|
232
233
|
"dist/hooks/orchestration-codex.js",
|
package/dist/status-helpers.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// finished - current turn completed, or driver exited 0.
|
|
10
10
|
// errored - process exited non-zero.
|
|
11
11
|
// stopped - process was killed.
|
|
12
|
+
// zombie_killed - stale process tree was culled outside the owning server.
|
|
12
13
|
//
|
|
13
14
|
// Liveness is driven by a heartbeat: launch time is the initial heartbeat and
|
|
14
15
|
// every subsequent PARSED visible provider stream item refreshes it (raw
|
|
@@ -44,8 +45,7 @@ export function computeStatusTransition(input) {
|
|
|
44
45
|
// opts in (poll_agent does; list_agents omits it to stay token-efficient).
|
|
45
46
|
export function buildLivenessFields(status, exitCode, lastActivity, now, includeHint = true) {
|
|
46
47
|
const idle_seconds = Math.floor((now - lastActivity) / 1000);
|
|
47
|
-
const alive = exitCode === null &&
|
|
48
|
-
(status === "processing" || status === "stalled" || status === "finished");
|
|
48
|
+
const alive = exitCode === null && (status === "processing" || status === "stalled");
|
|
49
49
|
const fields = { alive, idle_seconds };
|
|
50
50
|
if (status === "stalled" && includeHint) {
|
|
51
51
|
fields.hint =
|
package/dist/wait-helpers.js
CHANGED
|
@@ -15,7 +15,7 @@ export function formatLocalIso(ms) {
|
|
|
15
15
|
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
16
16
|
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${sign}${offH}:${offM} (${zone})`;
|
|
17
17
|
}
|
|
18
|
-
const TERMINAL_STATUSES = new Set(["finished", "errored", "stopped"]);
|
|
18
|
+
const TERMINAL_STATUSES = new Set(["finished", "errored", "stopped", "zombie_killed"]);
|
|
19
19
|
export function selectUnreported(list) {
|
|
20
20
|
return list.filter((a) => TERMINAL_STATUSES.has(a.status) && a.exitedAt !== null && !a.waitReported);
|
|
21
21
|
}
|
package/dist/zombie.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
export const ZOMBIE_LIVE_IDLE_MS = 6 * 60 * 1000;
|
|
6
|
+
export const ZOMBIE_TERMINAL_IDLE_MS = 30 * 1000;
|
|
7
|
+
export const ZOMBIE_FORCE_GRACE_MS = 20 * 1000;
|
|
8
|
+
export const ZOMBIE_INTENTS_FILENAME = "zombie-intents.jsonl";
|
|
9
|
+
export const ZOMBIE_REPORTS_FILENAME = "zombie-reports.jsonl";
|
|
10
|
+
function numberOrNull(v) {
|
|
11
|
+
return Number.isFinite(v) ? v : null;
|
|
12
|
+
}
|
|
13
|
+
function parseTimeMs(v) {
|
|
14
|
+
if (Number.isFinite(v))
|
|
15
|
+
return v;
|
|
16
|
+
if (typeof v !== "string")
|
|
17
|
+
return null;
|
|
18
|
+
const t = Date.parse(v);
|
|
19
|
+
return Number.isFinite(t) ? t : null;
|
|
20
|
+
}
|
|
21
|
+
export function slotPathForAgent(dir, agentId) {
|
|
22
|
+
return join(dir, `slot-${agentId}.json`);
|
|
23
|
+
}
|
|
24
|
+
export function parseSlotMetadata(text, slotPath = "") {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = JSON.parse(text);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const baseAgentId = basename(slotPath).replace(/^slot-/, "").replace(/\.json$/, "");
|
|
33
|
+
const agent_id = typeof raw.agent_id === "string" ? raw.agent_id : baseAgentId;
|
|
34
|
+
const started_at = typeof raw.started_at === "string"
|
|
35
|
+
? raw.started_at
|
|
36
|
+
: typeof raw.startedAt === "string"
|
|
37
|
+
? raw.startedAt
|
|
38
|
+
: null;
|
|
39
|
+
const started_at_ms = parseTimeMs(raw.started_at_ms ?? raw.started_at ?? raw.startedAt);
|
|
40
|
+
return {
|
|
41
|
+
schema_version: 1,
|
|
42
|
+
agent_id,
|
|
43
|
+
server_pid: numberOrNull(raw.server_pid ?? raw.pid),
|
|
44
|
+
child_pid: numberOrNull(raw.child_pid),
|
|
45
|
+
cwd: typeof raw.cwd === "string" ? raw.cwd : null,
|
|
46
|
+
started_at,
|
|
47
|
+
started_at_ms,
|
|
48
|
+
last_activity_ms: parseTimeMs(raw.last_activity_ms ?? raw.lastActivity ?? raw.started_at_ms ?? raw.startedAt),
|
|
49
|
+
status: typeof raw.status === "string" ? raw.status : null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function readSlotMetadata(slotPath) {
|
|
53
|
+
try {
|
|
54
|
+
return parseSlotMetadata(readFileSync(slotPath, "utf8"), slotPath);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function writeSlotMetadata(slotPath, metadata) {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
const full = {
|
|
63
|
+
schema_version: 1,
|
|
64
|
+
agent_id: metadata.agent_id,
|
|
65
|
+
server_pid: metadata.server_pid ?? process.pid,
|
|
66
|
+
child_pid: metadata.child_pid ?? null,
|
|
67
|
+
cwd: metadata.cwd ?? process.cwd(),
|
|
68
|
+
started_at: metadata.started_at ?? new Date(now).toISOString(),
|
|
69
|
+
started_at_ms: metadata.started_at_ms ?? now,
|
|
70
|
+
last_activity_ms: metadata.last_activity_ms ?? now,
|
|
71
|
+
status: metadata.status ?? null,
|
|
72
|
+
};
|
|
73
|
+
writeFileSync(slotPath, JSON.stringify(full), { mode: 0o600 });
|
|
74
|
+
}
|
|
75
|
+
function drainJsonl(path) {
|
|
76
|
+
if (!existsSync(path))
|
|
77
|
+
return [];
|
|
78
|
+
const claim = join(dirname(path), `${basename(path)}.${process.pid}.drain`);
|
|
79
|
+
try {
|
|
80
|
+
renameSync(path, claim);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return readFileSync(claim, "utf8")
|
|
87
|
+
.split(/\r?\n/)
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.map((line) => JSON.parse(line));
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
try {
|
|
93
|
+
unlinkSync(claim);
|
|
94
|
+
}
|
|
95
|
+
catch { }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function buildProcessTreeKillCommands(pid, p = platform()) {
|
|
99
|
+
if (p === "win32") {
|
|
100
|
+
return {
|
|
101
|
+
graceful: { command: "taskkill", args: ["/PID", String(pid), "/T"] },
|
|
102
|
+
force: { command: "taskkill", args: ["/PID", String(pid), "/T", "/F"] },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
graceful: { command: "kill", args: ["-TERM", `-${pid}`] },
|
|
107
|
+
force: { command: "kill", args: ["-KILL", `-${pid}`] },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export function appendZombieRecord(dir, record) {
|
|
111
|
+
mkdirSync(dir, { recursive: true, mode: 0o1777 });
|
|
112
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
113
|
+
writeFileSync(join(dir, ZOMBIE_INTENTS_FILENAME), line, { flag: "a", mode: 0o600 });
|
|
114
|
+
writeFileSync(join(dir, ZOMBIE_REPORTS_FILENAME), line, { flag: "a", mode: 0o600 });
|
|
115
|
+
}
|
|
116
|
+
export function drainZombieReports(dir) {
|
|
117
|
+
return drainJsonl(join(dir, ZOMBIE_REPORTS_FILENAME));
|
|
118
|
+
}
|
|
119
|
+
export function drainZombieIntents(dir) {
|
|
120
|
+
return drainJsonl(join(dir, ZOMBIE_INTENTS_FILENAME));
|
|
121
|
+
}
|
|
122
|
+
function defaultRunCommand(command, args) {
|
|
123
|
+
execFileSync(command, args, { stdio: "ignore" });
|
|
124
|
+
}
|
|
125
|
+
function defaultSleepMs(ms) {
|
|
126
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
127
|
+
}
|
|
128
|
+
export function cullStaleSlots(dir, deps = {}) {
|
|
129
|
+
const now = deps.now?.() ?? Date.now();
|
|
130
|
+
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
131
|
+
const sleepMs = deps.sleepMs ?? defaultSleepMs;
|
|
132
|
+
const forceGraceMs = deps.forceGraceMs?.() ?? ZOMBIE_FORCE_GRACE_MS;
|
|
133
|
+
const p = deps.platform ?? platform();
|
|
134
|
+
const records = [];
|
|
135
|
+
let files;
|
|
136
|
+
try {
|
|
137
|
+
files = readdirSync(dir).filter((f) => f.startsWith("slot-") && f.endsWith(".json"));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return records;
|
|
141
|
+
}
|
|
142
|
+
for (const file of files) {
|
|
143
|
+
const slotPath = join(dir, file);
|
|
144
|
+
const meta = readSlotMetadata(slotPath);
|
|
145
|
+
if (!meta?.last_activity_ms)
|
|
146
|
+
continue;
|
|
147
|
+
if (now - meta.last_activity_ms <= ZOMBIE_LIVE_IDLE_MS)
|
|
148
|
+
continue;
|
|
149
|
+
const pid = meta.child_pid;
|
|
150
|
+
if (pid && pid !== process.pid) {
|
|
151
|
+
const commands = buildProcessTreeKillCommands(pid, p);
|
|
152
|
+
try {
|
|
153
|
+
runCommand(commands.graceful.command, commands.graceful.args);
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
156
|
+
const forceKill = () => {
|
|
157
|
+
try {
|
|
158
|
+
runCommand(commands.force.command, commands.force.args);
|
|
159
|
+
}
|
|
160
|
+
catch { }
|
|
161
|
+
};
|
|
162
|
+
if (deps.scheduleForceKill) {
|
|
163
|
+
deps.scheduleForceKill(forceGraceMs, forceKill);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
sleepMs(forceGraceMs);
|
|
167
|
+
forceKill();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const record = {
|
|
171
|
+
kind: "zombie_killed",
|
|
172
|
+
agent_id: meta.agent_id,
|
|
173
|
+
child_pid: meta.child_pid,
|
|
174
|
+
server_pid: meta.server_pid,
|
|
175
|
+
slot_path: slotPath,
|
|
176
|
+
reason: "stale_live",
|
|
177
|
+
detected_at_ms: now,
|
|
178
|
+
last_activity_ms: meta.last_activity_ms,
|
|
179
|
+
message: `zombies: culled stale subagent ${meta.agent_id}`,
|
|
180
|
+
};
|
|
181
|
+
appendZombieRecord(dir, record);
|
|
182
|
+
try {
|
|
183
|
+
unlinkSync(slotPath);
|
|
184
|
+
}
|
|
185
|
+
catch { }
|
|
186
|
+
records.push(record);
|
|
187
|
+
}
|
|
188
|
+
return records;
|
|
189
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heretyc/subagent-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.1",
|
|
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",
|
|
@@ -30,14 +30,15 @@
|
|
|
30
30
|
"NOTICE",
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
|
-
"scripts": {
|
|
34
|
-
"check:versions": "node scripts/check_version_sync.mjs",
|
|
35
|
-
"build": "npm run check:versions && node scripts/gen-ruleset-scaffold.mjs && tsc && node scripts/copy-provider.mjs",
|
|
36
|
-
"
|
|
33
|
+
"scripts": {
|
|
34
|
+
"check:versions": "node scripts/check_version_sync.mjs",
|
|
35
|
+
"build": "npm run check:versions && node scripts/gen-ruleset-scaffold.mjs && tsc && node scripts/copy-provider.mjs",
|
|
36
|
+
"verify:npmjs-release": "node scripts/verify_npmjs_release.mjs",
|
|
37
|
+
"start": "node dist/index.js",
|
|
37
38
|
"postinstall": "node scripts/postinstall.mjs",
|
|
38
39
|
"prepare": "npm run build",
|
|
39
40
|
"prepublishOnly": "npm test",
|
|
40
|
-
"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/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/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 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/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/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 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
42
|
},
|
|
42
43
|
"author": "Lexi Blackburn",
|
|
43
44
|
"license": "Apache-2.0",
|