Skip to content

Commit 5c0a827

Browse files
xuiocodex
andcommitted
Compact Codex MCP responses
Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent b9ab9ff commit 5c0a827

13 files changed

Lines changed: 661 additions & 28 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codex-subagents",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "Launch OpenAI Codex agents, Spark agents, and parallel Codex subagents from Claude Code through a daemonless MCP server with read-only defaults and explicit full-access mode.",
55
"author": {
66
"name": "xuio"

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.1.1
4+
5+
- Compacted large MCP tool responses so successful long Codex runs do not surface as Claude Code tool-result overflow errors.
6+
37
## 0.1.0
48

59
- Initial Claude Code plugin for launching Codex agents through a daemonless stdio MCP server.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ The plugin lets Claude Code launch one Codex agent or several Codex agents in pa
1919
- Concurrency: Codex processes run through a global queue. Defaults are `CODEX_SUBAGENTS_MAX_GLOBAL_PROCESSES=4` and `CODEX_SUBAGENTS_MAX_PROJECT_PROCESSES=2`.
2020
- Progress: long-running tools emit MCP `notifications/progress` events when the client supplies a progress token.
2121
- Logging: verbose JSONL logs are written to stderr by default. The logs include raw MCP JSON-RPC frames, tool arguments/results, prompt outputs, progress notifications, queue/job/session lifecycle, and Codex stdin/stdout/stderr traffic.
22+
- MCP responses: long Codex outputs are compacted before returning to Claude so successful runs do not trip Claude Code's tool-result size limits. The full raw traffic remains available in the verbose server logs.
2223
- Security: secret-looking output is redacted before it is returned to Claude, and secret-looking environment variables are not forwarded to Codex unless `forward_sensitive_env` is explicitly true.
2324
- Sessions: `start_session` and `send_session_prompt` use Codex's recorded thread id so a Codex subagent can keep context across multiple prompts without a background daemon.
2425

dist/index.js

Lines changed: 165 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22788,6 +22788,147 @@ var CodexJobManager = class {
2278822788
};
2278922789
var jobManager = new CodexJobManager();
2279022790

22791+
// src/response.ts
22792+
var singleAgentLimits = {
22793+
finalMessageChars: 12e3,
22794+
stdioChars: 1500,
22795+
summaryMessageChars: 1e3,
22796+
structuredStringChars: 4e3
22797+
};
22798+
var partialLimits = {
22799+
finalMessageChars: 2e3,
22800+
stdioChars: 1e3,
22801+
summaryMessageChars: 1e3,
22802+
structuredStringChars: 2e3
22803+
};
22804+
function truncateString(text, maxChars) {
22805+
if (!text) return { text: "", omittedChars: 0 };
22806+
if (text.length <= maxChars) return { text, omittedChars: 0 };
22807+
return {
22808+
text: `${text.slice(0, maxChars)}
22809+
22810+
[truncated ${text.length - maxChars} chars from MCP response; full traffic is in server logs]`,
22811+
omittedChars: text.length - maxChars
22812+
};
22813+
}
22814+
function compactUnknown(value, maxStringChars2, depth = 0) {
22815+
if (typeof value === "string") return truncateString(value, maxStringChars2).text;
22816+
if (typeof value !== "object" || value === null) return value;
22817+
if (depth >= 6) return "[MaxDepth]";
22818+
if (Array.isArray(value)) {
22819+
const items = value.slice(0, 80).map((item) => compactUnknown(item, maxStringChars2, depth + 1));
22820+
if (value.length > items.length) items.push(`[truncated ${value.length - items.length} array items]`);
22821+
return items;
22822+
}
22823+
return Object.fromEntries(
22824+
Object.entries(value).map(([key, child]) => [
22825+
key,
22826+
compactUnknown(child, maxStringChars2, depth + 1)
22827+
])
22828+
);
22829+
}
22830+
function compactSummary(summary, limits) {
22831+
return {
22832+
counts: summary.counts,
22833+
threadId: summary.threadId,
22834+
usage: compactUnknown(summary.usage, limits.structuredStringChars),
22835+
commands: summary.commands.slice(0, 20).map((command) => ({
22836+
command: truncateString(command.command, 500).text,
22837+
status: command.status
22838+
})),
22839+
errors: summary.errors.slice(0, 10).map((error2) => truncateString(error2, limits.stdioChars).text),
22840+
lastAgentMessage: summary.lastAgentMessage ? truncateString(summary.lastAgentMessage, limits.summaryMessageChars).text : void 0,
22841+
events: summary.events ? compactUnknown(summary.events, limits.structuredStringChars) : void 0
22842+
};
22843+
}
22844+
function compactedAgentLimits(agentCount) {
22845+
if (agentCount <= 1) return singleAgentLimits;
22846+
return {
22847+
...singleAgentLimits,
22848+
finalMessageChars: Math.max(1500, Math.min(6e3, Math.floor(18e3 / agentCount))),
22849+
stdioChars: 800,
22850+
summaryMessageChars: 600,
22851+
structuredStringChars: 2e3
22852+
};
22853+
}
22854+
function compactAgentResultForMcp(result, limits = singleAgentLimits) {
22855+
const finalMessage = truncateString(result.finalMessage, limits.finalMessageChars);
22856+
const stdoutTail = truncateString(result.stdoutTail, limits.stdioChars);
22857+
const stderr = truncateString(result.stderr, limits.stdioChars);
22858+
const compacted = finalMessage.omittedChars > 0 || stdoutTail.omittedChars > 0 || stderr.omittedChars > 0;
22859+
return {
22860+
...result,
22861+
finalMessage: finalMessage.text,
22862+
stderr: stderr.text,
22863+
stdoutTail: stdoutTail.text,
22864+
eventSummary: compactSummary(result.eventSummary, limits),
22865+
structuredOutput: compactUnknown(result.structuredOutput, limits.structuredStringChars),
22866+
commandPreview: result.commandPreview.slice(0, 40).map((arg) => truncateString(arg, 1e3).text),
22867+
mcpResponse: {
22868+
compacted,
22869+
finalMessageOmittedChars: finalMessage.omittedChars,
22870+
stdoutTailOmittedChars: stdoutTail.omittedChars,
22871+
stderrOmittedChars: stderr.omittedChars,
22872+
note: compacted ? "MCP response was compacted to keep Claude responsive; full raw traffic is in server stderr logs." : void 0
22873+
}
22874+
};
22875+
}
22876+
function compactAgentResultsForMcp(results) {
22877+
const limits = compactedAgentLimits(results.length);
22878+
return results.map((result) => compactAgentResultForMcp(result, limits));
22879+
}
22880+
function compactPartialForMcp(partial2) {
22881+
const stdoutTail = truncateString(partial2.stdoutTail, partialLimits.stdioChars);
22882+
const stderrTail = truncateString(partial2.stderrTail, partialLimits.stdioChars);
22883+
return {
22884+
...partial2,
22885+
stdoutTail: stdoutTail.text,
22886+
stderrTail: stderrTail.text,
22887+
lastAgentMessage: partial2.lastAgentMessage ? truncateString(partial2.lastAgentMessage, partialLimits.finalMessageChars).text : void 0,
22888+
eventSummary: compactSummary(partial2.eventSummary, partialLimits)
22889+
};
22890+
}
22891+
function compactJobSnapshotForMcp(job) {
22892+
return {
22893+
...job,
22894+
result: compactRunValue(job.result),
22895+
partial: isPartial(job.partial) ? compactPartialForMcp(job.partial) : compactUnknown(job.partial, 2e3)
22896+
};
22897+
}
22898+
function compactSessionSnapshotForMcp(session) {
22899+
return {
22900+
...session,
22901+
lastResult: compactRunValue(session.lastResult),
22902+
partial: isPartial(session.partial) ? compactPartialForMcp(session.partial) : compactUnknown(session.partial, 2e3)
22903+
};
22904+
}
22905+
function compactRunValue(value) {
22906+
if (isAgentResult(value)) return compactAgentResultForMcp(value);
22907+
if (Array.isArray(value) && value.every(isAgentResult)) return compactAgentResultsForMcp(value);
22908+
if (isParallelResult(value)) {
22909+
return {
22910+
...value,
22911+
agents: compactAgentResultsForMcp(value.agents)
22912+
};
22913+
}
22914+
return compactUnknown(value, 4e3);
22915+
}
22916+
function isAgentResult(value) {
22917+
return Boolean(
22918+
value && typeof value === "object" && "ok" in value && "status" in value && "finalMessage" in value && "eventSummary" in value
22919+
);
22920+
}
22921+
function isPartial(value) {
22922+
return Boolean(
22923+
value && typeof value === "object" && "status" in value && "stdoutTail" in value && "stderrTail" in value && "eventSummary" in value
22924+
);
22925+
}
22926+
function isParallelResult(value) {
22927+
return Boolean(
22928+
value && typeof value === "object" && Array.isArray(value.agents) && value.agents.every(isAgentResult)
22929+
);
22930+
}
22931+
2279122932
// src/sessions.ts
2279222933
function snapshot2(session) {
2279322934
return {
@@ -23371,7 +23512,7 @@ server.registerTool(
2337123512
});
2337223513
await reportAgentResult(progress, result);
2337323514
await progress.flush();
23374-
return jsonResult({ agent: result }, !result.ok);
23515+
return jsonResult({ agent: compactAgentResultForMcp(result) }, !result.ok);
2337523516
} catch (error2) {
2337623517
await progress.flush();
2337723518
logger.error("run_agent.failed", { error: errorForLog(error2) });
@@ -23487,7 +23628,7 @@ server.registerTool(
2348723628
return jsonResult(
2348823629
{
2348923630
ok,
23490-
agents: results
23631+
agents: compactAgentResultsForMcp(results)
2349123632
},
2349223633
!ok
2349323634
);
@@ -23541,7 +23682,7 @@ server.registerTool(
2354123682
{
2354223683
ok: aggregation.ok,
2354323684
aggregation,
23544-
agents: results
23685+
agents: compactAgentResultsForMcp(results)
2354523686
},
2354623687
!aggregation.ok
2354723688
);
@@ -23601,7 +23742,7 @@ server.registerTool(
2360123742
await progress.flush();
2360223743
return jsonResult({ error: `Unknown job_id: ${args.job_id}` }, true);
2360323744
}
23604-
return jsonResult({ job });
23745+
return jsonResult({ job: compactJobSnapshotForMcp(job) });
2360523746
});
2360623747
}
2360723748
);
@@ -23634,7 +23775,10 @@ server.registerTool(
2363423775
if (!job) return jsonResult({ error: `Unknown job_id: ${args.job_id}` }, true);
2363523776
if (job.completedAt) await progress.send(`Codex job ${job.status}`);
2363623777
await progress.flush();
23637-
return jsonResult({ job }, job.status === "failed" || job.status === "cancelled");
23778+
return jsonResult(
23779+
{ job: compactJobSnapshotForMcp(job) },
23780+
job.status === "failed" || job.status === "cancelled"
23781+
);
2363823782
});
2363923783
}
2364023784
);
@@ -23654,7 +23798,7 @@ server.registerTool(
2365423798
await progress.flush();
2365523799
const job = jobManager.cancel(args.job_id);
2365623800
if (!job) return jsonResult({ error: `Unknown job_id: ${args.job_id}` }, true);
23657-
return jsonResult({ job });
23801+
return jsonResult({ job: compactJobSnapshotForMcp(job) });
2365823802
});
2365923803
}
2366023804
);
@@ -23685,7 +23829,10 @@ server.registerTool(
2368523829
);
2368623830
await reportAgentResult(progress, result);
2368723831
await progress.flush();
23688-
return jsonResult({ session, agent: result }, !result.ok);
23832+
return jsonResult(
23833+
{ session: compactSessionSnapshotForMcp(session), agent: compactAgentResultForMcp(result) },
23834+
!result.ok
23835+
);
2368923836
} catch (error2) {
2369023837
await progress.flush();
2369123838
logger.error("start_session.failed", { error: errorForLog(error2) });
@@ -23713,11 +23860,17 @@ server.registerTool(
2371323860
const { session, result, error: error2 } = await sessionManager.send(args.session_id, args.prompt, toRunOptions(args));
2371423861
if (error2 || !session || !result) {
2371523862
await progress.flush();
23716-
return jsonResult({ error: error2, session }, true);
23863+
return jsonResult(
23864+
{ error: error2, session: session ? compactSessionSnapshotForMcp(session) : session },
23865+
true
23866+
);
2371723867
}
2371823868
await reportAgentResult(progress, result);
2371923869
await progress.flush();
23720-
return jsonResult({ session, agent: result }, !result.ok);
23870+
return jsonResult(
23871+
{ session: compactSessionSnapshotForMcp(session), agent: compactAgentResultForMcp(result) },
23872+
!result.ok
23873+
);
2372123874
} catch (error2) {
2372223875
await progress.flush();
2372323876
logger.error("send_session_prompt.failed", { error: errorForLog(error2) });
@@ -23738,7 +23891,7 @@ server.registerTool(
2373823891
async (args, extra) => loggedToolCall("get_session", args, extra, async () => {
2373923892
const session = sessionManager.get(args.session_id);
2374023893
if (!session) return jsonResult({ error: `Unknown session_id: ${args.session_id}` }, true);
23741-
return jsonResult({ session });
23894+
return jsonResult({ session: compactSessionSnapshotForMcp(session) });
2374223895
})
2374323896
);
2374423897
server.registerTool(
@@ -23752,7 +23905,7 @@ server.registerTool(
2375223905
"list_sessions",
2375323906
args,
2375423907
extra,
23755-
async () => jsonResult({ sessions: sessionManager.list() })
23908+
async () => jsonResult({ sessions: sessionManager.list().map(compactSessionSnapshotForMcp) })
2375623909
)
2375723910
);
2375823911
server.registerTool(
@@ -23767,7 +23920,7 @@ server.registerTool(
2376723920
async (args, extra) => loggedToolCall("cancel_session", args, extra, async () => {
2376823921
const session = sessionManager.cancel(args.session_id);
2376923922
if (!session) return jsonResult({ error: `Unknown session_id: ${args.session_id}` }, true);
23770-
return jsonResult({ session });
23923+
return jsonResult({ session: compactSessionSnapshotForMcp(session) });
2377123924
})
2377223925
);
2377323926
server.registerTool(

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "claude-code-codex-subagents",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "Claude Code plugin that launches OpenAI Codex agents through a daemonless stdio MCP server.",
55
"type": "module",
66
"private": true,
@@ -29,6 +29,7 @@
2929
"test:codex-runtime": "node test/codex-runtime-probe.mjs",
3030
"test:claude-orchestration": "node test/claude-orchestration.mjs",
3131
"test:claude-autodiscovery": "node test/claude-autodiscovery.mjs",
32+
"test:claude-large-output": "node test/claude-large-output.mjs",
3233
"test:claude-real-codex": "node test/claude-real-codex.mjs",
3334
"test:ci": "npm run build && npm test && npm run smoke:mcp && npm run test:reliability && npm run test:stress && npm run test:progress && npm run test:advanced",
3435
"test:comprehensive": "npm run build && npm test && npm run smoke:mcp && npm run test:reliability && npm run test:stress && npm run test:progress && npm run test:advanced && npm run test:codex-runtime && npm run validate:plugin && npm run test:claude-desktop",

skills/codex-subagents/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Default behavior:
2020
- Supports structured results with `output_contract` or `output_schema`; use these when Claude must merge, compare, or aggregate Codex outputs.
2121
- Redacts secret-looking output by default and does not forward secret-looking environment variables unless `forward_sensitive_env` is explicitly true.
2222
- Writes very verbose JSONL logs to stderr by default, including raw MCP JSON-RPC frames, tool arguments/results, prompt outputs, progress notifications, queue/job/session lifecycle, and Codex stdin/stdout/stderr traffic.
23+
- Compacts large tool responses before returning them to Claude; when `mcpResponse.compacted` is true, use the returned summary first and inspect server logs only if the omitted raw tail is necessary.
2324

2425
For one delegated task, call `run_agent`. Make the prompt self-contained: include the scope, the expected read-only behavior, and the output shape Claude needs. For code review and exploration, ask for concise findings with file paths and line references.
2526

0 commit comments

Comments
 (0)