@heretyc/subagent-mcp 2.10.0 → 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 +6 -26
- package/dist/concurrency.js +15 -3
- package/dist/config-scaffold.js +1 -1
- package/dist/global-concurrency.jsonc +6 -7
- package/dist/hooks/orchestration-codex.js +9 -6
- package/dist/index.js +223 -22
- package/dist/orchestration/hook-core.js +35 -4
- package/dist/orchestration/pretool.js +9 -4
- 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
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.10.
|
|
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/global-concurrency-cap.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",
|