@northflare/runner 0.0.9 → 0.0.10

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.
Files changed (56) hide show
  1. package/dist/components/claude-sdk-manager.d.ts.map +1 -1
  2. package/dist/components/claude-sdk-manager.js +5 -4
  3. package/dist/components/claude-sdk-manager.js.map +1 -1
  4. package/dist/components/codex-sdk-manager.d.ts +2 -0
  5. package/dist/components/codex-sdk-manager.d.ts.map +1 -1
  6. package/dist/components/codex-sdk-manager.js +91 -10
  7. package/dist/components/codex-sdk-manager.js.map +1 -1
  8. package/dist/components/message-handler-sse.d.ts.map +1 -1
  9. package/dist/components/message-handler-sse.js +15 -14
  10. package/dist/components/message-handler-sse.js.map +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -2
  13. package/dist/index.js.map +1 -1
  14. package/dist/runner-sse.d.ts.map +1 -1
  15. package/dist/runner-sse.js +25 -112
  16. package/dist/runner-sse.js.map +1 -1
  17. package/dist/runner.js +3 -3
  18. package/dist/types/index.d.ts +1 -0
  19. package/dist/types/index.d.ts.map +1 -1
  20. package/dist/utils/config.d.ts.map +1 -1
  21. package/dist/utils/config.js +1 -0
  22. package/dist/utils/config.js.map +1 -1
  23. package/dist/utils/console.d.ts.map +1 -1
  24. package/dist/utils/console.js +2 -1
  25. package/dist/utils/console.js.map +1 -1
  26. package/dist/utils/debug.d.ts +2 -0
  27. package/dist/utils/debug.d.ts.map +1 -0
  28. package/dist/utils/debug.js +19 -0
  29. package/dist/utils/debug.js.map +1 -0
  30. package/dist/utils/logger.d.ts.map +1 -1
  31. package/dist/utils/logger.js +6 -4
  32. package/dist/utils/logger.js.map +1 -1
  33. package/dist/utils/status-line.d.ts +0 -8
  34. package/dist/utils/status-line.d.ts.map +1 -1
  35. package/dist/utils/status-line.js +4 -3
  36. package/dist/utils/status-line.js.map +1 -1
  37. package/dist/utils/tool-response-sanitizer.d.ts +9 -0
  38. package/dist/utils/tool-response-sanitizer.d.ts.map +1 -0
  39. package/dist/utils/tool-response-sanitizer.js +122 -0
  40. package/dist/utils/tool-response-sanitizer.js.map +1 -0
  41. package/exceptions.log +2 -0
  42. package/package.json +1 -1
  43. package/rejections.log +3 -0
  44. package/src/components/claude-sdk-manager.ts +5 -4
  45. package/src/components/codex-sdk-manager.ts +111 -10
  46. package/src/components/message-handler-sse.ts +15 -14
  47. package/src/index.ts +4 -2
  48. package/src/runner-sse.ts +29 -133
  49. package/src/types/index.ts +1 -0
  50. package/src/utils/config.ts +1 -0
  51. package/src/utils/console.ts +4 -2
  52. package/src/utils/debug.ts +18 -0
  53. package/src/utils/logger.ts +8 -5
  54. package/src/utils/status-line.ts +3 -2
  55. package/src/utils/tool-response-sanitizer.ts +160 -0
  56. package/tests/tool-response-sanitizer.test.ts +63 -0
package/src/runner-sse.ts CHANGED
@@ -18,137 +18,24 @@ import { EnhancedRepositoryManager } from "./components/enhanced-repository-mana
18
18
  import { StateManager } from "./utils/StateManager";
19
19
  import { createLogger } from "./utils/logger";
20
20
  import { statusLineManager } from "./utils/status-line";
21
+ import { isRunnerDebugEnabled } from "./utils/debug";
21
22
  import fs from "fs/promises";
22
23
  import path from "path";
23
24
  import computerName from "computer-name";
25
+ import {
26
+ sanitizeToolResponsePayload,
27
+ TOOL_RESPONSE_BYTE_LIMIT,
28
+ } from "./utils/tool-response-sanitizer";
24
29
 
25
30
  const logger = createLogger("RunnerApp");
26
31
 
27
- const TOOL_RESPONSE_BYTE_LIMIT = 500 * 1024; // 500 KiB per tool response field
28
- const TOOL_RESPONSE_SUFFIX = "... [truncated]";
29
-
30
- function sanitizeToolResponsePayload(
31
- method: string | undefined,
32
- params: any
33
- ): any {
34
- if (method !== "message.agent" || !params) {
35
- return params;
36
- }
37
-
38
- if (!isToolResultPayload(params)) {
39
- return params;
40
- }
41
-
42
- const [sanitized, changed] = truncateStringsRecursively(params);
43
- if (!changed) {
44
- return params;
45
- }
46
-
47
- const toolUseId = extractToolUseId(params);
32
+ function logToolResponseTruncation(toolUseId?: string) {
48
33
  const message = `Tool response exceeded ${TOOL_RESPONSE_BYTE_LIMIT} bytes and was truncated`;
49
34
  if (toolUseId) {
50
35
  logger.warn(message, { toolUseId });
51
36
  } else {
52
37
  logger.warn(message);
53
38
  }
54
- return sanitized;
55
- }
56
-
57
- function isToolResultPayload(payload: any): boolean {
58
- if (!payload) {
59
- return false;
60
- }
61
- if (payload.type === "tool_result" || payload.subtype === "tool_result") {
62
- return true;
63
- }
64
- if (Array.isArray(payload.content)) {
65
- return payload.content.some((entry: any) => isToolResultBlock(entry));
66
- }
67
- return isToolResultBlock(payload.content);
68
- }
69
-
70
- function isToolResultBlock(block: any): boolean {
71
- if (!block) {
72
- return false;
73
- }
74
- return (
75
- block.type === "tool_result" ||
76
- block.subtype === "tool_result" ||
77
- typeof block.tool_use_id === "string"
78
- );
79
- }
80
-
81
- function extractToolUseId(payload: any): string | undefined {
82
- if (!payload) {
83
- return undefined;
84
- }
85
- if (typeof payload.tool_use_id === "string") {
86
- return payload.tool_use_id;
87
- }
88
- if (Array.isArray(payload.content)) {
89
- const match = payload.content.find(
90
- (entry: any) => entry && typeof entry.tool_use_id === "string"
91
- );
92
- if (match) {
93
- return match.tool_use_id;
94
- }
95
- }
96
- if (payload.content && typeof payload.content === "object") {
97
- return extractToolUseId(payload.content);
98
- }
99
- return undefined;
100
- }
101
-
102
- function truncateStringsRecursively(value: any): [any, boolean] {
103
- if (typeof value === "string") {
104
- return truncateStringValue(value);
105
- }
106
-
107
- if (Array.isArray(value)) {
108
- let mutated = false;
109
- let result: any[] = value;
110
- for (let i = 0; i < value.length; i++) {
111
- const [newItem, changed] = truncateStringsRecursively(value[i]);
112
- if (changed) {
113
- if (!mutated) {
114
- result = value.slice();
115
- mutated = true;
116
- }
117
- result[i] = newItem;
118
- }
119
- }
120
- return [result, mutated];
121
- }
122
-
123
- if (value && typeof value === "object") {
124
- let mutated = false;
125
- let result: any = value;
126
- for (const key of Object.keys(value)) {
127
- const [newVal, changed] = truncateStringsRecursively(value[key]);
128
- if (changed) {
129
- if (!mutated) {
130
- result = { ...value };
131
- mutated = true;
132
- }
133
- result[key] = newVal;
134
- }
135
- }
136
- return [result, mutated];
137
- }
138
-
139
- return [value, false];
140
- }
141
-
142
- function truncateStringValue(value: string): [string, boolean] {
143
- const byteLength = Buffer.byteLength(value, "utf8");
144
- if (byteLength <= TOOL_RESPONSE_BYTE_LIMIT) {
145
- return [value, false];
146
- }
147
-
148
- const suffixBytes = Buffer.byteLength(TOOL_RESPONSE_SUFFIX, "utf8");
149
- const maxBytes = Math.max(0, TOOL_RESPONSE_BYTE_LIMIT - suffixBytes);
150
- const truncatedBuffer = Buffer.from(value, "utf8").subarray(0, maxBytes);
151
- return [truncatedBuffer.toString("utf8") + TOOL_RESPONSE_SUFFIX, true];
152
39
  }
153
40
 
154
41
  export class RunnerApp implements IRunnerApp {
@@ -271,7 +158,7 @@ export class RunnerApp implements IRunnerApp {
271
158
  await this.registerWithRetry();
272
159
 
273
160
  // Log debug info after registration
274
- if (process.env["DEBUG"] === "true") {
161
+ if (isRunnerDebugEnabled()) {
275
162
  logger.debug("Runner initialized with ownership details", {
276
163
  runnerId: this.config.runnerId,
277
164
  runnerUid: this.runnerUid,
@@ -325,7 +212,13 @@ export class RunnerApp implements IRunnerApp {
325
212
 
326
213
  async notify(method: string, params: any): Promise<void> {
327
214
  try {
328
- const safeParams = sanitizeToolResponsePayload(method, params);
215
+ const sanitization = sanitizeToolResponsePayload(method, params);
216
+ if (sanitization.truncated) {
217
+ logToolResponseTruncation(sanitization.toolUseId);
218
+ }
219
+ const safeParams = sanitization.truncated
220
+ ? sanitization.params
221
+ : params;
329
222
  // Log RPC notification in debug mode
330
223
  logger.debug(`[RPC] Sending notification: ${method}`, {
331
224
  method,
@@ -357,17 +250,20 @@ export class RunnerApp implements IRunnerApp {
357
250
  }
358
251
 
359
252
  async sendToOrchestrator(message: JsonRpcMessage): Promise<any> {
360
- const sanitizedParams = sanitizeToolResponsePayload(
253
+ const sanitization = sanitizeToolResponsePayload(
361
254
  message.method,
362
255
  message.params
363
256
  );
364
- const messageToSend =
365
- sanitizedParams === message.params
366
- ? message
367
- : {
368
- ...message,
369
- params: sanitizedParams,
370
- };
257
+ if (sanitization.truncated) {
258
+ logToolResponseTruncation(sanitization.toolUseId);
259
+ }
260
+
261
+ const messageToSend = sanitization.truncated
262
+ ? {
263
+ ...message,
264
+ params: sanitization.params,
265
+ }
266
+ : message;
371
267
 
372
268
  try {
373
269
  // Log RPC request in debug mode
@@ -507,7 +403,7 @@ export class RunnerApp implements IRunnerApp {
507
403
  logger.info(`Registration response:`, JSON.stringify(response, null, 2));
508
404
 
509
405
  // Log the runnerRepos being sent and received
510
- if (process.env["DEBUG"] === "true") {
406
+ if (isRunnerDebugEnabled()) {
511
407
  logger.debug(
512
408
  "Registration runnerRepos sent:",
513
409
  JSON.stringify(filteredRunnerRepos)
@@ -569,7 +465,7 @@ export class RunnerApp implements IRunnerApp {
569
465
  }
570
466
 
571
467
  // Debug logging for registration details
572
- if (process.env["DEBUG"] === "true") {
468
+ if (isRunnerDebugEnabled()) {
573
469
  logger.debug("Registration complete with details", {
574
470
  runnerId: this.config.runnerId,
575
471
  runnerUid: this.runnerUid,
@@ -976,7 +872,7 @@ export class RunnerApp implements IRunnerApp {
976
872
  logger.error("Failed to persist active status:", error);
977
873
  });
978
874
 
979
- if (process.env["DEBUG"] === "true") {
875
+ if (isRunnerDebugEnabled()) {
980
876
  logger.debug("Active runner status changed", {
981
877
  previous: previousState,
982
878
  new: active,
@@ -990,7 +886,7 @@ export class RunnerApp implements IRunnerApp {
990
886
  const previousTimestamp = this.lastProcessedAt;
991
887
  this.lastProcessedAt = timestamp;
992
888
 
993
- if (process.env["DEBUG"] === "true") {
889
+ if (isRunnerDebugEnabled()) {
994
890
  logger.debug("LastProcessedAt updated", {
995
891
  previous: previousTimestamp?.toISOString() || "null",
996
892
  new: timestamp?.toISOString() || "null",
@@ -59,5 +59,6 @@ export interface EnvironmentConfig {
59
59
  NORTHFLARE_RUNNER_TOKEN: string;
60
60
  NORTHFLARE_WORKSPACE_DIR: string;
61
61
  NORTHFLARE_ORCHESTRATOR_URL: string;
62
+ NORTHFLARE_RUNNER_DEBUG?: string;
62
63
  DEBUG?: string;
63
64
  }
@@ -294,6 +294,7 @@ export class ConfigManager {
294
294
  NORTHFLARE_RUNNER_TOKEN: process.env["NORTHFLARE_RUNNER_TOKEN"]!,
295
295
  NORTHFLARE_WORKSPACE_DIR: process.env["NORTHFLARE_WORKSPACE_DIR"]!,
296
296
  NORTHFLARE_ORCHESTRATOR_URL: process.env["NORTHFLARE_ORCHESTRATOR_URL"]!,
297
+ NORTHFLARE_RUNNER_DEBUG: process.env["NORTHFLARE_RUNNER_DEBUG"],
297
298
  DEBUG: process.env["DEBUG"],
298
299
  };
299
300
  }
@@ -2,7 +2,9 @@
2
2
  * Console wrapper that suppresses output when not in debug mode
3
3
  */
4
4
 
5
- const isDebug = process.env["DEBUG"] === "true";
5
+ import { isRunnerDebugEnabled } from "./debug";
6
+
7
+ const isDebug = isRunnerDebugEnabled();
6
8
 
7
9
  export const console = {
8
10
  log: isDebug ? global.console.log.bind(global.console) : () => {},
@@ -10,4 +12,4 @@ export const console = {
10
12
  error: global.console.error.bind(global.console), // Always show errors
11
13
  info: isDebug ? global.console.info.bind(global.console) : () => {},
12
14
  debug: isDebug ? global.console.debug.bind(global.console) : () => {},
13
- };
15
+ };
@@ -0,0 +1,18 @@
1
+ export function isRunnerDebugEnabled(): boolean {
2
+ const explicitFlag = process.env["NORTHFLARE_RUNNER_DEBUG"];
3
+ if (typeof explicitFlag === "string") {
4
+ return isTruthy(explicitFlag);
5
+ }
6
+
7
+ const legacyFlag = process.env["DEBUG"];
8
+ if (typeof legacyFlag === "string") {
9
+ return isTruthy(legacyFlag);
10
+ }
11
+
12
+ return false;
13
+ }
14
+
15
+ function isTruthy(value: string): boolean {
16
+ const normalized = value.trim().toLowerCase();
17
+ return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
18
+ }
@@ -4,6 +4,7 @@
4
4
 
5
5
  import winston from "winston";
6
6
  import path from "path";
7
+ import { isRunnerDebugEnabled } from "./debug";
7
8
 
8
9
  // Custom log levels
9
10
  const levels = {
@@ -29,6 +30,8 @@ const colors = {
29
30
 
30
31
  winston.addColors(colors);
31
32
 
33
+ const debugEnabled = isRunnerDebugEnabled();
34
+
32
35
  // Format for console output
33
36
  const consoleFormat = winston.format.combine(
34
37
  winston.format.colorize({ all: true }),
@@ -36,7 +39,7 @@ const consoleFormat = winston.format.combine(
36
39
  winston.format.printf(({ timestamp, level, message, stack, ...metadata }) => {
37
40
  let msg = `${timestamp} [${level}]: ${message}`;
38
41
  // Only show stack traces and metadata in debug mode
39
- if (process.env["DEBUG"] === "true") {
42
+ if (debugEnabled) {
40
43
  if (stack) {
41
44
  msg += `\n${stack}`;
42
45
  }
@@ -59,13 +62,13 @@ const fileFormat = winston.format.combine(
59
62
 
60
63
  // Create logger instance
61
64
  export const logger = winston.createLogger({
62
- level: process.env["DEBUG"] === "true" ? "debug" : "info",
65
+ level: debugEnabled ? "debug" : "info",
63
66
  levels,
64
67
  transports: [
65
68
  // Always include console transport for errors
66
69
  new winston.transports.Console({
67
70
  format: consoleFormat,
68
- level: process.env["DEBUG"] === "true" ? "debug" : "error",
71
+ level: debugEnabled ? "debug" : "error",
69
72
  }),
70
73
  ],
71
74
  });
@@ -94,7 +97,7 @@ export function configureFileLogging(logDir: string): void {
94
97
  );
95
98
 
96
99
  // Debug log file (only in debug mode)
97
- if (process.env["DEBUG"] === "true") {
100
+ if (debugEnabled) {
98
101
  logger.add(
99
102
  new winston.transports.File({
100
103
  filename: path.join(logDir, "debug.log"),
@@ -128,4 +131,4 @@ logger.rejections.handle(
128
131
  );
129
132
 
130
133
  // Export default logger
131
- export default logger;
134
+ export default logger;
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * StatusLineManager - Manages persistent status line output for active agent processes
3
- *
3
+ *
4
4
  * This component maintains a single-line status output that updates every minute to show
5
5
  * the count of active agent processes. It prevents the machine from entering idle
6
6
  * state while processes are running and provides clear visual feedback without cluttering
7
7
  * the terminal with multiple log lines.
8
8
  */
9
+ import { isRunnerDebugEnabled } from "./debug";
9
10
 
10
11
  export class StatusLineManager {
11
12
  private intervalId?: NodeJS.Timeout;
@@ -15,7 +16,7 @@ export class StatusLineManager {
15
16
 
16
17
  constructor() {
17
18
  // Only enable status line when not in debug mode
18
- this.isEnabled = process.env["DEBUG"] !== "true";
19
+ this.isEnabled = !isRunnerDebugEnabled();
19
20
  }
20
21
 
21
22
  /**
@@ -0,0 +1,160 @@
1
+ export const TOOL_RESPONSE_BYTE_LIMIT = 500 * 1024; // 500 KiB cap per tool response payload
2
+ export const TOOL_RESPONSE_SUFFIX = "... [truncated]";
3
+
4
+ export interface ToolResponseSanitizationResult {
5
+ params: any;
6
+ truncated: boolean;
7
+ toolUseId?: string;
8
+ }
9
+
10
+ interface TruncationContext {
11
+ bytesRemaining: number;
12
+ truncated: boolean;
13
+ }
14
+
15
+ export function sanitizeToolResponsePayload(
16
+ method: string | undefined,
17
+ params: any
18
+ ): ToolResponseSanitizationResult {
19
+ if (method !== "message.agent" || !params) {
20
+ return { params, truncated: false };
21
+ }
22
+
23
+ if (!isToolResultPayload(params)) {
24
+ return { params, truncated: false };
25
+ }
26
+
27
+ const context: TruncationContext = {
28
+ bytesRemaining: TOOL_RESPONSE_BYTE_LIMIT,
29
+ truncated: false,
30
+ };
31
+
32
+ const [sanitized, changed] = truncateStringsRecursively(params, context);
33
+
34
+ if (!changed && !context.truncated) {
35
+ return { params, truncated: false };
36
+ }
37
+
38
+ return {
39
+ params: sanitized,
40
+ truncated: true,
41
+ toolUseId: extractToolUseId(params),
42
+ };
43
+ }
44
+
45
+ function isToolResultPayload(payload: any): boolean {
46
+ if (!payload) {
47
+ return false;
48
+ }
49
+ if (payload.type === "tool_result" || payload.subtype === "tool_result") {
50
+ return true;
51
+ }
52
+ if (Array.isArray(payload.content)) {
53
+ return payload.content.some((entry: any) => isToolResultBlock(entry));
54
+ }
55
+ return isToolResultBlock(payload.content);
56
+ }
57
+
58
+ function isToolResultBlock(block: any): boolean {
59
+ if (!block) {
60
+ return false;
61
+ }
62
+ return (
63
+ block.type === "tool_result" ||
64
+ block.subtype === "tool_result" ||
65
+ typeof block.tool_use_id === "string"
66
+ );
67
+ }
68
+
69
+ function extractToolUseId(payload: any): string | undefined {
70
+ if (!payload) {
71
+ return undefined;
72
+ }
73
+ if (typeof payload.tool_use_id === "string") {
74
+ return payload.tool_use_id;
75
+ }
76
+ if (Array.isArray(payload.content)) {
77
+ const match = payload.content.find(
78
+ (entry: any) => entry && typeof entry.tool_use_id === "string"
79
+ );
80
+ if (match) {
81
+ return match.tool_use_id;
82
+ }
83
+ }
84
+ if (payload.content && typeof payload.content === "object") {
85
+ return extractToolUseId(payload.content);
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ function truncateStringsRecursively(
91
+ value: any,
92
+ context: TruncationContext
93
+ ): [any, boolean] {
94
+ if (typeof value === "string") {
95
+ return truncateStringValue(value, context);
96
+ }
97
+
98
+ if (Array.isArray(value)) {
99
+ let mutated = false;
100
+ let result: any[] = value;
101
+ for (let i = 0; i < value.length; i++) {
102
+ const [newItem, changed] = truncateStringsRecursively(value[i], context);
103
+ if (changed) {
104
+ if (!mutated) {
105
+ result = value.slice();
106
+ mutated = true;
107
+ }
108
+ result[i] = newItem;
109
+ }
110
+ }
111
+ return [result, mutated];
112
+ }
113
+
114
+ if (value && typeof value === "object") {
115
+ let mutated = false;
116
+ let result: Record<string, any> = value;
117
+ for (const key of Object.keys(value)) {
118
+ const [newVal, changed] = truncateStringsRecursively(
119
+ value[key],
120
+ context
121
+ );
122
+ if (changed) {
123
+ if (!mutated) {
124
+ result = { ...value };
125
+ mutated = true;
126
+ }
127
+ result[key] = newVal;
128
+ }
129
+ }
130
+ return [result, mutated];
131
+ }
132
+
133
+ return [value, false];
134
+ }
135
+
136
+ function truncateStringValue(
137
+ value: string,
138
+ context: TruncationContext
139
+ ): [string, boolean] {
140
+ if (context.bytesRemaining <= 0) {
141
+ if (value.length === 0) {
142
+ return [value, false];
143
+ }
144
+ context.truncated = true;
145
+ return ["", true];
146
+ }
147
+
148
+ const byteLength = Buffer.byteLength(value, "utf8");
149
+ if (byteLength <= context.bytesRemaining) {
150
+ context.bytesRemaining -= byteLength;
151
+ return [value, false];
152
+ }
153
+
154
+ const suffixBytes = Buffer.byteLength(TOOL_RESPONSE_SUFFIX, "utf8");
155
+ const maxBytes = Math.max(0, context.bytesRemaining - suffixBytes);
156
+ const truncatedBuffer = Buffer.from(value, "utf8").subarray(0, maxBytes);
157
+ context.bytesRemaining = 0;
158
+ context.truncated = true;
159
+ return [truncatedBuffer.toString("utf8") + TOOL_RESPONSE_SUFFIX, true];
160
+ }
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ sanitizeToolResponsePayload,
4
+ TOOL_RESPONSE_BYTE_LIMIT,
5
+ TOOL_RESPONSE_SUFFIX,
6
+ } from "../src/utils/tool-response-sanitizer";
7
+
8
+ describe("sanitizeToolResponsePayload", () => {
9
+ it("returns original params for non message.agent notifications", () => {
10
+ const payload = { type: "tool_result", content: [] };
11
+ const result = sanitizeToolResponsePayload("runner.heartbeat", payload);
12
+ expect(result.truncated).toBe(false);
13
+ expect(result.params).toBe(payload);
14
+ });
15
+
16
+ it("truncates oversized tool_result strings", () => {
17
+ const longText = "x".repeat(TOOL_RESPONSE_BYTE_LIMIT + 128);
18
+ const payload = {
19
+ type: "tool_result",
20
+ tool_use_id: "tool-123",
21
+ content: [{ type: "text", text: longText }],
22
+ };
23
+
24
+ const result = sanitizeToolResponsePayload("message.agent", payload);
25
+ expect(result.truncated).toBe(true);
26
+ expect(result.toolUseId).toBe("tool-123");
27
+
28
+ const sanitizedText = result.params.content[0].text as string;
29
+ expect(sanitizedText.endsWith(TOOL_RESPONSE_SUFFIX)).toBe(true);
30
+ expect(Buffer.byteLength(sanitizedText, "utf8")).toBeLessThanOrEqual(
31
+ TOOL_RESPONSE_BYTE_LIMIT + Buffer.byteLength(TOOL_RESPONSE_SUFFIX, "utf8")
32
+ );
33
+
34
+ // Ensure original payload was not mutated
35
+ expect(payload.content[0].text).toBe(longText);
36
+ });
37
+
38
+ it("enforces the byte budget across multiple string blocks", () => {
39
+ const firstBlockBytes = 400 * 1024;
40
+ const secondBlockBytes = 200 * 1024;
41
+ const payload = {
42
+ type: "tool_result",
43
+ content: [
44
+ { type: "text", text: "a".repeat(firstBlockBytes) },
45
+ { type: "text", text: "b".repeat(secondBlockBytes) },
46
+ ],
47
+ };
48
+
49
+ const result = sanitizeToolResponsePayload("message.agent", payload);
50
+ expect(result.truncated).toBe(true);
51
+
52
+ const first = result.params.content[0].text as string;
53
+ const second = result.params.content[1].text as string;
54
+
55
+ expect(Buffer.byteLength(first, "utf8")).toBe(firstBlockBytes);
56
+ expect(second.endsWith(TOOL_RESPONSE_SUFFIX)).toBe(true);
57
+ const combinedBytes =
58
+ Buffer.byteLength(first, "utf8") + Buffer.byteLength(second, "utf8");
59
+ expect(combinedBytes).toBeLessThanOrEqual(
60
+ TOOL_RESPONSE_BYTE_LIMIT + Buffer.byteLength(TOOL_RESPONSE_SUFFIX, "utf8")
61
+ );
62
+ });
63
+ });