Skip to content

Commit 987966f

Browse files
committed
fix(session): omit oversized tool result detail from hook payloads
Hook payloads capped content but passed detail through verbatim, letting a verbose server blow up payloads. Omit serialized detail over the payload budget while preserving small detail.
1 parent f3c995a commit 987966f

2 files changed

Lines changed: 47 additions & 2 deletions

File tree

‎src/session/hooks.test.ts‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ function event(type: string, data: unknown): ReactorEmittedEvent {
1717
function observeOneTurnWithToolResult(
1818
collector: ReturnType<typeof createTurnContextCollector>,
1919
toolResultContent: string,
20+
toolResultDetail?: unknown,
2021
): void {
2122
collector.observe(
2223
event("inference.done", {
@@ -34,7 +35,11 @@ function observeOneTurnWithToolResult(
3435
);
3536
collector.observe(
3637
event("tool.done", {
37-
result: { callId: "call-1", content: toolResultContent },
38+
result: {
39+
callId: "call-1",
40+
content: toolResultContent,
41+
...(toolResultDetail !== undefined ? { detail: toolResultDetail } : {}),
42+
},
3843
}),
3944
);
4045
}
@@ -64,6 +69,33 @@ describe("createTurnContextCollector tool result truncation", () => {
6469
const [turn] = collector.getTurns();
6570
expect(turn?.toolResults[0]?.content).toBe(smallOutput);
6671
});
72+
73+
test("preserves small structured detail in hook payloads", () => {
74+
const collector = createTurnContextCollector(() => undefined);
75+
const detail = { answer: 42 };
76+
77+
observeOneTurnWithToolResult(collector, "exit code 0", detail);
78+
79+
const [turn] = collector.getTurns();
80+
expect(turn?.toolResults[0]?.detail).toEqual(detail);
81+
});
82+
83+
test("omits oversized structured detail while keeping the content cap", () => {
84+
const collector = createTurnContextCollector(() => undefined);
85+
const hugeOutput = "x".repeat(HOOK_PAYLOAD_TOOL_RESULT_CHARS * 4);
86+
const hugeDetail = { blob: "y".repeat(HOOK_PAYLOAD_TOOL_RESULT_CHARS * 4) };
87+
88+
observeOneTurnWithToolResult(collector, hugeOutput, hugeDetail);
89+
90+
const [turn] = collector.getTurns();
91+
const result = turn?.toolResults[0];
92+
expect(result?.detail).toBeUndefined();
93+
const content = result?.content;
94+
expect(typeof content).toBe("string");
95+
expect((content as string).length).toBeLessThanOrEqual(
96+
HOOK_PAYLOAD_TOOL_RESULT_CHARS + 64,
97+
);
98+
});
6799
});
68100

69101
describe("lifecycle hook payload delivery", () => {

‎src/session/hooks.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,13 +501,26 @@ function truncateToolResultForHookPayload(result: ToolResult): ToolResult {
501501
callId: result.callId,
502502
content,
503503
...(result.isError !== undefined ? { isError: result.isError } : {}),
504-
...(result.detail !== undefined ? { detail: result.detail } : {}),
504+
...(hookPayloadDetailWithinBudget(result.detail)
505+
? { detail: result.detail }
506+
: {}),
505507
...(result.pendingMarker !== undefined
506508
? { pendingMarker: result.pendingMarker }
507509
: {}),
508510
};
509511
}
510512

513+
function hookPayloadDetailWithinBudget(detail: unknown): boolean {
514+
if (detail === undefined) return false;
515+
let serialized: string;
516+
try {
517+
serialized = JSON.stringify(detail) ?? String(detail);
518+
} catch {
519+
return false;
520+
}
521+
return serialized.length <= HOOK_PAYLOAD_TOOL_RESULT_CHARS;
522+
}
523+
511524
function addUsage(a: TokenUsage, b: TokenUsage): TokenUsage {
512525
return {
513526
input: a.input + b.input,

0 commit comments

Comments
 (0)