@heretyc/subagent-mcp 2.12.13 → 2.12.14

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/drivers.js CHANGED
@@ -18,6 +18,7 @@ export const CLAUDE_SESSION_LIMIT = /^\s*you['’]ve hit your session limit\s*·
18
18
  export function isClaudeSessionLimit(text) {
19
19
  return CLAUDE_SESSION_LIMIT.test(text);
20
20
  }
21
+ const TRANSIENT_FAILURE_RE = /\b429\b|\b(?:http(?:\/\d(?:\.\d)?)?|status|statuscode|status_code|code|error)\b[\s:=#-]*5\d{2}\b|quota|rate.?limit|timeout|ECONNRESET|ETIMEDOUT|ECONNREFUSED|too many requests|service unavailable|server error|overloaded/i;
21
22
  export function claudeMessageText(message) {
22
23
  if (message?.type === "assistant") {
23
24
  const content = message.message?.content;
@@ -167,7 +168,9 @@ function isElicitationMethod(method) {
167
168
  // Shape the `result` payload for an elicitation reply. When the RPC offered a
168
169
  // discrete option set, select only an exact case-insensitive label match and
169
170
  // echo its identifier; otherwise pass the answer through as text.
170
- function buildElicitationResult(options, answer) {
171
+ function buildElicitationResult(options, answer, action = "answer") {
172
+ if (action === "decline")
173
+ return { action: "decline" };
171
174
  if (options && options.length > 0) {
172
175
  const norm = answer.trim().toLowerCase();
173
176
  const chosen = options.find((opt) => {
@@ -751,7 +754,8 @@ export class CodexAppServerDriver {
751
754
  else if (isJsonRpcId(message.id) &&
752
755
  typeof message.method === "string" &&
753
756
  isElicitationMethod(message.method)) {
754
- void this.replyJsonRpc(message.id, buildElicitationResult(undefined, ""));
757
+ console.error(`[codex app-server driver] declining unhandled elicitation method: ${message.method}`);
758
+ void this.replyJsonRpc(message.id, buildElicitationResult(undefined, "", "decline"));
755
759
  }
756
760
  if (message.method === "turn/started" && message.params && typeof message.params === "object") {
757
761
  const turn = (message.params.turn ?? {});
@@ -819,7 +823,7 @@ export class CodexAppServerDriver {
819
823
  }
820
824
  fail(error) {
821
825
  const msg = error.message;
822
- const transient = /\b429\b|\b5\d{2}\b|quota|rate.?limit|timeout|ECONNRESET|ETIMEDOUT|ECONNREFUSED|too many requests|service unavailable|server error|overloaded/i.test(msg);
826
+ const transient = TRANSIENT_FAILURE_RE.test(msg);
823
827
  this._definitelyStartedReject(transient ? new ProviderTransientError(msg) : error);
824
828
  this.process.fail(error);
825
829
  this.rejectPending(error);
@@ -1028,7 +1032,7 @@ export class ClaudeSdkDriver {
1028
1032
  catch (error) {
1029
1033
  if (!this.process.killed) {
1030
1034
  const msg = error instanceof Error ? error.message : String(error);
1031
- const transient = /\b429\b|\b5\d{2}\b|quota|rate.?limit|timeout|ECONNRESET|ETIMEDOUT|ECONNREFUSED|too many requests|service unavailable|server error|overloaded/i.test(msg);
1035
+ const transient = TRANSIENT_FAILURE_RE.test(msg);
1032
1036
  this._definitelyStartedReject(transient ? new ProviderTransientError(msg) : new Error(msg));
1033
1037
  this.closedFlag = true;
1034
1038
  this.process.stderr.write(msg);
@@ -1038,8 +1042,11 @@ export class ClaudeSdkDriver {
1038
1042
  }
1039
1043
  }
1040
1044
  export async function createProviderDriver(options) {
1041
- if ((options.provider === "claude" && process.env.SUBAGENT_MOCK_CLAUDE_DRIVER === "jsonl") ||
1042
- (options.provider === "codex" && process.env.SUBAGENT_MOCK_CODEX_DRIVER === "jsonl")) {
1045
+ const testSeamsEnabled = process.env.NODE_ENV === "test" || process.env.SUBAGENT_MCP_ENABLE_TEST_SEAMS === "1";
1046
+ if (testSeamsEnabled &&
1047
+ ((options.provider === "claude" && process.env.SUBAGENT_MOCK_CLAUDE_DRIVER === "jsonl") ||
1048
+ (options.provider === "codex" && process.env.SUBAGENT_MOCK_CODEX_DRIVER === "jsonl"))) {
1049
+ // Test seam: never honor mock-driver script env vars in production by default.
1043
1050
  const mockScript = process.env.SUBAGENT_MOCK_DRIVER_SCRIPT;
1044
1051
  const child = mockScript
1045
1052
  ? spawn(process.execPath, [mockScript, options.provider], {
package/dist/effort.js CHANGED
@@ -6,9 +6,13 @@ export function mapModel(provider, model) {
6
6
  if (provider === "claude") {
7
7
  if (model === "opus" || model === "opus-4-8")
8
8
  return "claude-opus-4-8";
9
+ if (model === "sonnet")
10
+ return "claude-sonnet-4-6";
11
+ if (model === "haiku")
12
+ return "claude-haiku-4-5";
9
13
  if (model === "fable")
10
14
  return "claude-fable-5";
11
- return model; // haiku, sonnet as-is
15
+ return model;
12
16
  }
13
17
  else {
14
18
  return model; // gpt-5.5
@@ -60,10 +64,8 @@ export function buildCommand(provider, model, effort, cwd, agentId) {
60
64
  const ucSettingsPath = join(ucSettingsDir, "settings.json");
61
65
  writeFileSync(ucSettingsPath, '{"ultracode":true}', { mode: 0o600 });
62
66
  args.push("--settings", ucSettingsPath);
63
- args.push("--permission-mode", "bypassPermissions", "--tools", "default", "--max-turns", "50");
64
67
  return { args, ucSettingsPath, ucSettingsDir };
65
68
  }
66
- args.push("--permission-mode", "bypassPermissions", "--tools", "default", "--max-turns", "50");
67
69
  return { args };
68
70
  }
69
71
  else {
@@ -1,43 +1,43 @@
1
- // subagent-mcp - Global Subagent MCP Config
2
- // ------------------------------------------------------------------
3
- // SOLE source of truth for machine-wide subagent-mcp defaults that must be
4
- // available beside the compiled server.
5
- //
6
- // globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
7
- // across EVERY session, process, and user on this machine. There is NO
8
- // environment-variable override for the cap.
9
- //
10
- // RE-READ on every launch_agent call - edits take effect immediately, no
11
- // server restart required.
12
- //
13
- // Cap value rules:
14
- // - missing / unset / non-integer / 0 / negative -> reset to default 20
15
- // - 1 through 9 -> forced UP to minimum 10
16
- // - 10 or greater -> used as-is
17
- //
18
- // checkForUpdates controls the silent npmjs update check started when the MCP
19
- // server connects. Default true. Set to false to skip the registry fetch and
20
- // suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
21
- //
22
- // permissionsCeiling controls launch-time permissions posture:
23
- // - auto (default): shared engine gates unsafe/residue actions
24
- // - manual: human approval for residue while still auto-denying DANGER
25
- // - yolo: preserve the historical bypass/danger-full-access path, except
26
- // for non-bypassable config-file self-protection
27
- //
28
- // escalation applies only in auto mode. irreversible-only routes irreversible
29
- // NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
30
- //
31
- // strictReadParity controls logging only. Unparseable Codex approval payloads
32
- // always fail closed to ask; warn logs the construct, off silences that log.
33
- //
34
- // sandboxNetwork controls Codex workspace-write network access. Default false.
35
- // Set true only when sub-agents need network after permission approval.
36
- {
37
- "globalConcurrentSubagents": 20,
38
- "checkForUpdates": true,
39
- "permissionsCeiling": "auto",
40
- "escalation": "irreversible-only",
41
- "strictReadParity": "warn",
42
- "sandboxNetwork": false
43
- }
1
+ // subagent-mcp - Global Subagent MCP Config
2
+ // ------------------------------------------------------------------
3
+ // SOLE source of truth for machine-wide subagent-mcp defaults that must be
4
+ // available beside the compiled server.
5
+ //
6
+ // globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
7
+ // across EVERY session, process, and user on this machine. There is NO
8
+ // environment-variable override for the cap.
9
+ //
10
+ // RE-READ on every launch_agent call - edits take effect immediately, no
11
+ // server restart required.
12
+ //
13
+ // Cap value rules:
14
+ // - missing / unset / non-integer / 0 / negative -> reset to default 20
15
+ // - 1 through 9 -> forced UP to minimum 10
16
+ // - 10 or greater -> used as-is
17
+ //
18
+ // checkForUpdates controls the silent npmjs update check started when the MCP
19
+ // server connects. Default true. Set to false to skip the registry fetch and
20
+ // suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
21
+ //
22
+ // permissionsCeiling controls launch-time permissions posture:
23
+ // - auto (default): shared engine gates unsafe/residue actions
24
+ // - manual: human approval for residue while still auto-denying DANGER
25
+ // - yolo: preserve the historical bypass/danger-full-access path, except
26
+ // for non-bypassable config-file self-protection
27
+ //
28
+ // escalation applies only in auto mode. irreversible-only routes irreversible
29
+ // NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
30
+ //
31
+ // strictReadParity controls logging only. Unparseable Codex approval payloads
32
+ // always fail closed to ask; warn logs the construct, off silences that log.
33
+ //
34
+ // sandboxNetwork controls Codex workspace-write network access. Default false.
35
+ // Set true only when sub-agents need network after permission approval.
36
+ {
37
+ "globalConcurrentSubagents": 20,
38
+ "checkForUpdates": true,
39
+ "permissionsCeiling": "auto",
40
+ "escalation": "irreversible-only",
41
+ "strictReadParity": "warn",
42
+ "sandboxNetwork": false
43
+ }
@@ -1,43 +1,43 @@
1
- // subagent-mcp - Global Subagent MCP Config
2
- // ------------------------------------------------------------------
3
- // SOLE source of truth for machine-wide subagent-mcp defaults that must be
4
- // available beside the compiled server.
5
- //
6
- // globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
7
- // across EVERY session, process, and user on this machine. There is NO
8
- // environment-variable override for the cap.
9
- //
10
- // RE-READ on every launch_agent call - edits take effect immediately, no
11
- // server restart required.
12
- //
13
- // Cap value rules:
14
- // - missing / unset / non-integer / 0 / negative -> reset to default 20
15
- // - 1 through 9 -> forced UP to minimum 10
16
- // - 10 or greater -> used as-is
17
- //
18
- // checkForUpdates controls the silent npmjs update check started when the MCP
19
- // server connects. Default true. Set to false to skip the registry fetch and
20
- // suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
21
- //
22
- // permissionsCeiling controls launch-time permissions posture:
23
- // - auto (default): shared engine gates unsafe/residue actions
24
- // - manual: human approval for residue while still auto-denying DANGER
25
- // - yolo: preserve the historical bypass/danger-full-access path, except
26
- // for non-bypassable config-file self-protection
27
- //
28
- // escalation applies only in auto mode. irreversible-only routes irreversible
29
- // NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
30
- //
31
- // strictReadParity controls logging only. Unparseable Codex approval payloads
32
- // always fail closed to ask; warn logs the construct, off silences that log.
33
- //
34
- // sandboxNetwork controls Codex workspace-write network access. Default false.
35
- // Set true only when sub-agents need network after permission approval.
36
- {
37
- "globalConcurrentSubagents": 20,
38
- "checkForUpdates": true,
39
- "permissionsCeiling": "auto",
40
- "escalation": "irreversible-only",
41
- "strictReadParity": "warn",
42
- "sandboxNetwork": false
43
- }
1
+ // subagent-mcp - Global Subagent MCP Config
2
+ // ------------------------------------------------------------------
3
+ // SOLE source of truth for machine-wide subagent-mcp defaults that must be
4
+ // available beside the compiled server.
5
+ //
6
+ // globalConcurrentSubagents controls how many subagents may be ALIVE AT ONCE
7
+ // across EVERY session, process, and user on this machine. There is NO
8
+ // environment-variable override for the cap.
9
+ //
10
+ // RE-READ on every launch_agent call - edits take effect immediately, no
11
+ // server restart required.
12
+ //
13
+ // Cap value rules:
14
+ // - missing / unset / non-integer / 0 / negative -> reset to default 20
15
+ // - 1 through 9 -> forced UP to minimum 10
16
+ // - 10 or greater -> used as-is
17
+ //
18
+ // checkForUpdates controls the silent npmjs update check started when the MCP
19
+ // server connects. Default true. Set to false to skip the registry fetch and
20
+ // suppress hook notices. SUBAGENT_UPDATE_CHECK=0 or false also disables it.
21
+ //
22
+ // permissionsCeiling controls launch-time permissions posture:
23
+ // - auto (default): shared engine gates unsafe/residue actions
24
+ // - manual: human approval for residue while still auto-denying DANGER
25
+ // - yolo: preserve the historical bypass/danger-full-access path, except
26
+ // for non-bypassable config-file self-protection
27
+ //
28
+ // escalation applies only in auto mode. irreversible-only routes irreversible
29
+ // NEUTRAL residue to a human; off leaves residue to orchestrator judgment.
30
+ //
31
+ // strictReadParity controls logging only. Unparseable Codex approval payloads
32
+ // always fail closed to ask; warn logs the construct, off silences that log.
33
+ //
34
+ // sandboxNetwork controls Codex workspace-write network access. Default false.
35
+ // Set true only when sub-agents need network after permission approval.
36
+ {
37
+ "globalConcurrentSubagents": 20,
38
+ "checkForUpdates": true,
39
+ "permissionsCeiling": "auto",
40
+ "escalation": "irreversible-only",
41
+ "strictReadParity": "warn",
42
+ "sandboxNetwork": false
43
+ }
package/dist/index.js CHANGED
@@ -393,10 +393,11 @@ function withMaintenance(handler, options = {}) {
393
393
  }
394
394
  export function classifyFailureReason(reason, stderr) {
395
395
  const text = `${reason}\n${stderr}`;
396
- return /\b429\b|\b5\d{2}\b|quota|usage.?cap|rate.?limit|timeout|connection.?reset|ECONNRESET|ETIMEDOUT|ECONNREFUSED|too many requests|service unavailable|server error|overloaded/i.test(text)
396
+ return TRANSIENT_FAILURE_RE.test(text)
397
397
  ? "transient_provider"
398
398
  : "permanent";
399
399
  }
400
+ const TRANSIENT_FAILURE_RE = /\b429\b|\b(?:http(?:\/\d(?:\.\d)?)?|status|statuscode|status_code|code|error)\b[\s:=#-]*5\d{2}\b|quota|usage.?cap|rate.?limit|timeout|connection.?reset|ECONNRESET|ETIMEDOUT|ECONNREFUSED|too many requests|service unavailable|server error|overloaded/i;
400
401
  function buildFailoverNote(skipped, winner) {
401
402
  const top = skipped[0];
402
403
  const topLabel = `${top.model}@${top.effort} (${top.provider})`;
@@ -438,10 +439,9 @@ export function isClaudeBackgroundWakeLine(line) {
438
439
  return true;
439
440
  if (marker.includes("taskoutput") && marker.includes("complete"))
440
441
  return true;
441
- // No concrete Claude bg-task-complete JSONL fixture exists in-repo. As a
442
- // backstop, any non-result JSONL activity after a finished turn means the
443
- // still-live SDK stream observed something new and needs a nudge.
444
- return true;
442
+ // Only concrete Claude bg-task-complete markers should wake the parked SDK
443
+ // loop; unrelated post-turn JSONL activity is not a resume signal.
444
+ return false;
445
445
  }
446
446
  catch {
447
447
  return false;
@@ -572,7 +572,7 @@ const ORCHESTRATION_INSTRUCTIONS = "subagent-mcp - CANONICAL OPERATING MODEL (fu
572
572
  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. launch_agent is code-capped at 2 spawn levels below the main orchestrator: depth 1 may launch depth 2 workers; depth 2 workers cannot spawn further.\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.";
573
573
  const server = new McpServer({
574
574
  name: "subagent-mcp",
575
- version: "2.12.13",
575
+ version: "2.12.14",
576
576
  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.",
577
577
  }, {
578
578
  instructions: process.env.SUBAGENT_MCP_SUBAGENT === "1"
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  const PARK_TIMEOUT_MS = 5 * 60 * 1000;
3
3
  const PER_AGENT_FIFO_CAP = 16;
4
+ const PENDING_PERMISSION_HISTORY_CAP = 200;
4
5
  function publicRecord(record) {
5
6
  // record may be a StoredPendingPermission at runtime, which carries a live
6
7
  // NodeJS.Timeout and a resolve closure. Both are non-serializable (the timer
@@ -57,7 +58,7 @@ export class PendingPermissionManager {
57
58
  answer_reason: "pending-queue cap reached",
58
59
  };
59
60
  this.telemetry.cap_overflow_auto_denies += 1;
60
- this.history.push(publicRecord(record));
61
+ this.remember(record);
61
62
  console.error(`[permissions] auto-deny ${record.request_id} for agent ${record.agent_id}: pending-queue cap reached`);
62
63
  void Promise.resolve(input.resolve({
63
64
  request_id: record.request_id,
@@ -125,6 +126,7 @@ export class PendingPermissionManager {
125
126
  if (record)
126
127
  closed.push(await this.finish(record, "deny", reason));
127
128
  }
129
+ this.askedCountByAgent.delete(agentId);
128
130
  return closed;
129
131
  }
130
132
  async autoDeny(requestId, reason) {
@@ -141,8 +143,10 @@ export class PendingPermissionManager {
141
143
  const queue = (this.pendingByAgent.get(record.agent_id) ?? []).filter((id) => id !== record.request_id);
142
144
  if (queue.length > 0)
143
145
  this.pendingByAgent.set(record.agent_id, queue);
144
- else
146
+ else {
145
147
  this.pendingByAgent.delete(record.agent_id);
148
+ this.askedCountByAgent.delete(record.agent_id);
149
+ }
146
150
  record.state = autoRule ? "auto_answered" : "answered";
147
151
  record.auto_answer_rule = autoRule;
148
152
  record.answered_at = Date.now();
@@ -160,10 +164,16 @@ export class PendingPermissionManager {
160
164
  record.state = "errored";
161
165
  record.answer_reason = e instanceof Error ? e.message : String(e);
162
166
  }
163
- this.history.push(publicRecord(record));
167
+ this.remember(record);
164
168
  this.emitQueue(record.agent_id);
165
169
  return publicRecord(record);
166
170
  }
171
+ remember(record) {
172
+ this.history.push(publicRecord(record));
173
+ if (this.history.length > PENDING_PERMISSION_HISTORY_CAP) {
174
+ this.history.splice(0, this.history.length - PENDING_PERMISSION_HISTORY_CAP);
175
+ }
176
+ }
167
177
  emitQueue(agentId) {
168
178
  const count = this.pendingCount(agentId);
169
179
  for (const listener of this.queueListeners)