@aexol/spectral 0.9.209 → 0.9.211

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.
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAwBH,OAAO,EAAE,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AAYlE,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAmB,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAYrE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAazD;AAED,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA6B9D;AAED,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,uCAAuC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;CAC/E;AAED,MAAM,WAAW,cAAc;IAC7B,qCAAqC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,WAAW,CAAC;IACnB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAwiBlF;AA+DD;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAc5E"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAwBH,OAAO,EAAE,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AAYlE,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAmB,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAarE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAazD;AAED,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA6B9D;AAED,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,uCAAuC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;CAC/E;AAED,MAAM,WAAW,cAAc;IAC7B,qCAAqC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,WAAW,CAAC;IACnB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAojBlF;AA+DD;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAc5E"}
@@ -48,6 +48,7 @@ import { gracefulShutdown } from "../server/shutdown.js";
48
48
  import { getLiveMcpState } from "../mcp/state-getter.js";
49
49
  import { preflightSqlite, SessionStore } from "../server/storage.js";
50
50
  import { DevProcessRegistry } from "../server/dev-process-registry.js";
51
+ import { sweepOffloads } from "../server/offload-gc.js";
51
52
  /** Default backend base URL when neither env nor override is set. */
52
53
  const DEFAULT_BACKEND_URL = "https://api.aexol.ai";
53
54
  import { getVersion } from "../version.js";
@@ -133,9 +134,21 @@ export async function runServe(opts = {}) {
133
134
  process.stderr.write(`✗ ${preflight.error}\n`);
134
135
  process.exit(1);
135
136
  }
137
+ // Pre-flight: sweep stale large-result offload files (fire-and-forget).
136
138
  const store = new SessionStore(dbPath);
137
139
  const devProcessRegistry = new DevProcessRegistry(getAgentDir());
138
140
  devProcessRegistry.reconcile();
141
+ // GC for stale large-result offload files. Deferred via setImmediate so it
142
+ // never blocks server startup; sweepOffloads() never throws, but the guard
143
+ // stays so a failed sweep can never take down serving.
144
+ setImmediate(() => {
145
+ try {
146
+ sweepOffloads();
147
+ }
148
+ catch {
149
+ // Best-effort.
150
+ }
151
+ });
139
152
  // Mutable holder for the meta-event publisher. Bound below once we
140
153
  // have both `relay` (the send target) and `registration.record.machineId`
141
154
  // (the canonical machineId the backend uses to fan out). The manager
@@ -1 +1 @@
1
- {"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/console.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CAyDhF"}
1
+ {"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/console.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAIvD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CA2EhF"}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Console messages tool: browser_console_messages.
3
3
  */
4
+ import { offloadLargeResult } from "../../../sdk/coding-agent/core/tools/offload.js";
4
5
  export function registerConsoleTools(ext, bs) {
5
6
  const consoleTool = {
6
7
  name: "browser_console_messages",
@@ -30,12 +31,31 @@ export function registerConsoleTools(ext, bs) {
30
31
  }
31
32
  const lines = messages.map((m) => `[${m.type.padEnd(7)}] ${m.text.slice(0, 300)}`);
32
33
  const text = lines.join("\n");
34
+ // Small console dumps pass through untouched; large ones become
35
+ // preview + offload footer (fallback keeps the legacy cap).
36
+ let displayText = text;
37
+ let fullOutputPath;
38
+ let offloadTruncation;
39
+ try {
40
+ const offloaded = offloadLargeResult(text, "browser_console_messages", "shared");
41
+ displayText = offloaded.preview;
42
+ if (offloaded.fullOutputPath && offloaded.truncation) {
43
+ fullOutputPath = offloaded.fullOutputPath;
44
+ offloadTruncation = offloaded.truncation;
45
+ }
46
+ }
47
+ catch {
48
+ displayText = text.length > 20_000 ? text.slice(0, 20_000) + "\n\n[truncated]" : text;
49
+ }
33
50
  return {
34
51
  content: [{
35
52
  type: "text",
36
- text: text.length > 20_000 ? text.slice(0, 20_000) + "\n\n[truncated]" : text,
53
+ text: displayText,
37
54
  }],
38
- details: {},
55
+ details: {
56
+ ...(fullOutputPath ? { fullOutputPath } : {}),
57
+ ...(offloadTruncation ? { offloadTruncation } : {}),
58
+ },
39
59
  };
40
60
  }
41
61
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"evaluate.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/evaluate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CA0GjF"}
1
+ {"version":3,"file":"evaluate.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/evaluate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAuBvD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CAkHjF"}
@@ -1,6 +1,22 @@
1
1
  /**
2
2
  * Evaluate tools: browser_evaluate, browser_run_code_unsafe.
3
3
  */
4
+ import { offloadLargeResult, } from "../../../sdk/coding-agent/core/tools/offload.js";
5
+ function offloadBrowserText(text, toolName) {
6
+ try {
7
+ const offloaded = offloadLargeResult(text, toolName, "shared");
8
+ if (!offloaded.fullOutputPath || !offloaded.truncation)
9
+ return { text };
10
+ return {
11
+ text: offloaded.preview,
12
+ fullOutputPath: offloaded.fullOutputPath,
13
+ offloadTruncation: offloaded.truncation,
14
+ };
15
+ }
16
+ catch {
17
+ return { text: text.length > 50000 ? text.slice(0, 50000) + "\n\n[truncated]" : text };
18
+ }
19
+ }
4
20
  export function registerEvaluateTools(ext, bs) {
5
21
  const evaluateTool = {
6
22
  name: "browser_evaluate",
@@ -34,12 +50,16 @@ export function registerEvaluateTools(ext, bs) {
34
50
  const text = typeof result === "string"
35
51
  ? result
36
52
  : JSON.stringify(result, null, 2);
53
+ const offloaded = offloadBrowserText(text, "browser_evaluate");
37
54
  return {
38
55
  content: [{
39
56
  type: "text",
40
- text: text.length > 50000 ? text.slice(0, 50000) + "\n\n[truncated]" : text,
57
+ text: offloaded.text,
41
58
  }],
42
- details: {},
59
+ details: {
60
+ ...(offloaded.fullOutputPath ? { fullOutputPath: offloaded.fullOutputPath } : {}),
61
+ ...(offloaded.offloadTruncation ? { offloadTruncation: offloaded.offloadTruncation } : {}),
62
+ },
43
63
  };
44
64
  }
45
65
  catch (err) {
@@ -79,12 +99,16 @@ export function registerEvaluateTools(ext, bs) {
79
99
  const text = typeof result === "string"
80
100
  ? result
81
101
  : JSON.stringify(result, null, 2);
102
+ const offloaded = offloadBrowserText(text, "browser_run_code_unsafe");
82
103
  return {
83
104
  content: [{
84
105
  type: "text",
85
- text: text.length > 50000 ? text.slice(0, 50000) + "\n\n[truncated]" : text,
106
+ text: offloaded.text,
86
107
  }],
87
- details: {},
108
+ details: {
109
+ ...(offloaded.fullOutputPath ? { fullOutputPath: offloaded.fullOutputPath } : {}),
110
+ ...(offloaded.offloadTruncation ? { offloadTruncation: offloaded.offloadTruncation } : {}),
111
+ },
88
112
  };
89
113
  }
90
114
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/network.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CAwHhF"}
1
+ {"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/network.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAwBvD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CA4HhF"}
@@ -1,6 +1,22 @@
1
1
  /**
2
2
  * Network tools: browser_network_requests, browser_network_request.
3
3
  */
4
+ import { offloadLargeResult, } from "../../../sdk/coding-agent/core/tools/offload.js";
5
+ function offloadBrowserText(text, toolName, legacyLimit) {
6
+ try {
7
+ const offloaded = offloadLargeResult(text, toolName, "shared");
8
+ if (!offloaded.fullOutputPath || !offloaded.truncation)
9
+ return { text };
10
+ return {
11
+ text: offloaded.preview,
12
+ fullOutputPath: offloaded.fullOutputPath,
13
+ offloadTruncation: offloaded.truncation,
14
+ };
15
+ }
16
+ catch {
17
+ return { text: text.length > legacyLimit ? text.slice(0, legacyLimit) + "\n\n[truncated]" : text };
18
+ }
19
+ }
4
20
  export function registerNetworkTools(ext, bs) {
5
21
  const networkRequestsTool = {
6
22
  name: "browser_network_requests",
@@ -90,12 +106,16 @@ export function registerNetworkTools(ext, bs) {
90
106
  }
91
107
  }
92
108
  const text = lines.join("\n");
109
+ const offloaded = offloadBrowserText(text, "browser_network_request", 50_000);
93
110
  return {
94
111
  content: [{
95
112
  type: "text",
96
- text: text.length > 50_000 ? text.slice(0, 50_000) + "\n\n[truncated]" : text,
113
+ text: offloaded.text,
97
114
  }],
98
- details: {},
115
+ details: {
116
+ ...(offloaded.fullOutputPath ? { fullOutputPath: offloaded.fullOutputPath } : {}),
117
+ ...(offloaded.offloadTruncation ? { offloadTruncation: offloaded.offloadTruncation } : {}),
118
+ },
99
119
  };
100
120
  }
101
121
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/fetch.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAK9C,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CA0I7E"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/fetch.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAyC9C,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CAqK7E"}
@@ -1,6 +1,33 @@
1
1
  import { fetchPage } from "../utils/fetcher.js";
2
2
  import { renderPage, isRendererAvailable } from "../utils/renderer.js";
3
- import { errorResult, textResult, truncate } from "./helpers.js";
3
+ import { countLines, offloadLargeResult } from "../../../sdk/coding-agent/core/tools/offload.js";
4
+ import { errorResult, textResult } from "./helpers.js";
5
+ /**
6
+ * Offload one large text field via the shared offload module (shared
7
+ * thresholds, head+tail preview + footer, owner-only file permissions).
8
+ * `shown` is always counted from the actually displayed preview text.
9
+ */
10
+ function tryOffloadSeoContent(content, toolName, sessionId) {
11
+ if (!content)
12
+ return { text: "" };
13
+ let offloaded;
14
+ try {
15
+ offloaded = offloadLargeResult(content, toolName, sessionId);
16
+ }
17
+ catch {
18
+ return { text: content };
19
+ }
20
+ if (!offloaded.fullOutputPath || !offloaded.truncation) {
21
+ return { text: content };
22
+ }
23
+ return {
24
+ text: offloaded.preview,
25
+ fullOutputPath: offloaded.fullOutputPath,
26
+ offloadTruncation: offloaded.truncation,
27
+ shownLines: countLines(offloaded.preview),
28
+ totalLines: countLines(content),
29
+ };
30
+ }
4
31
  export function registerFetchTools(ext, config) {
5
32
  const fetchTool = {
6
33
  name: "seo_fetch",
@@ -24,7 +51,7 @@ export function registerFetchTools(ext, config) {
24
51
  },
25
52
  required: ["url"],
26
53
  },
27
- async execute(_toolCallId, params) {
54
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
28
55
  const { url, userAgent, timeout, followRedirects } = params;
29
56
  try {
30
57
  const result = await fetchPage(url, config, {
@@ -35,18 +62,26 @@ export function registerFetchTools(ext, config) {
35
62
  if (result.error) {
36
63
  return errorResult(`Fetch failed: ${result.error}`, { result });
37
64
  }
65
+ const sessionId = ctx
66
+ ?.sessionManager?.getSessionId?.() ?? "shared";
67
+ const offloaded = tryOffloadSeoContent(result.content, "seo_fetch", sessionId);
38
68
  const summary = `✓ Fetched: ${result.finalUrl}\n` +
39
69
  `Status: ${result.statusCode} ${result.statusText}\n` +
40
70
  `Redirects: ${result.redirectChain.length}\n` +
41
71
  `Content-Length: ${result.contentLengthBytes} bytes\n` +
42
- `Headers: ${Object.keys(result.headers).join(", ")}`;
72
+ `Headers: ${Object.keys(result.headers).join(", ")}` +
73
+ (offloaded.fullOutputPath ? `\nFull output: ${offloaded.fullOutputPath}` : "");
43
74
  return textResult(summary, {
44
75
  url: result.url,
45
76
  finalUrl: result.finalUrl,
46
77
  statusCode: result.statusCode,
47
- content: truncate(result.content, 50_000),
78
+ content: offloaded.text,
48
79
  headers: result.headers,
49
80
  redirectChain: result.redirectChain,
81
+ ...(offloaded.fullOutputPath ? { fullOutputPath: offloaded.fullOutputPath } : {}),
82
+ ...(offloaded.offloadTruncation ? { offloadTruncation: offloaded.offloadTruncation } : {}),
83
+ ...(offloaded.shownLines !== undefined ? { shownLines: offloaded.shownLines } : {}),
84
+ ...(offloaded.totalLines !== undefined ? { totalLines: offloaded.totalLines } : {}),
50
85
  });
51
86
  }
52
87
  catch (err) {
@@ -83,7 +118,7 @@ export function registerFetchTools(ext, config) {
83
118
  },
84
119
  required: ["url"],
85
120
  },
86
- async execute(_toolCallId, params) {
121
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
87
122
  const { url, viewport, captureA11y, captureConsole, timeout } = params;
88
123
  const available = await isRendererAvailable();
89
124
  if (!available) {
@@ -96,21 +131,37 @@ export function registerFetchTools(ext, config) {
96
131
  captureConsole,
97
132
  timeout,
98
133
  });
134
+ const sessionId = ctx
135
+ ?.sessionManager?.getSessionId?.() ?? "shared";
136
+ const contentOffload = tryOffloadSeoContent(result.content, "seo_render", sessionId);
137
+ const rawOffload = tryOffloadSeoContent(result.rawContent, "seo_render", sessionId);
138
+ const offloadNote = contentOffload.fullOutputPath || rawOffload.fullOutputPath
139
+ ? `\nFull output: ${[contentOffload.fullOutputPath, rawOffload.fullOutputPath].filter(Boolean).join(", ")}`
140
+ : "";
99
141
  const summary = `✓ Rendered: ${result.finalUrl}\n` +
100
142
  `Status: ${result.statusCode}\n` +
101
143
  `SPA: ${result.isSpa}\n` +
102
144
  `Render time: ${result.renderMs}ms\n` +
103
- `Console errors: ${result.consoleErrors.length}`;
145
+ `Console errors: ${result.consoleErrors.length}` + offloadNote;
146
+ // Keep BOTH offload paths — content and rawContent are independent
147
+ // fields and each may need re-reading.
148
+ const fullOutputPaths = [contentOffload.fullOutputPath, rawOffload.fullOutputPath].filter((p) => Boolean(p));
104
149
  return textResult(summary, {
105
150
  url: result.url,
106
151
  finalUrl: result.finalUrl,
107
152
  statusCode: result.statusCode,
108
- content: truncate(result.content, 50_000),
109
- rawContent: truncate(result.rawContent, 20_000),
153
+ content: contentOffload.text,
154
+ rawContent: rawOffload.text,
110
155
  isSpa: result.isSpa,
111
156
  consoleErrors: result.consoleErrors,
112
157
  renderMs: result.renderMs,
113
158
  accessibilityTree: result.accessibilityTree,
159
+ ...(fullOutputPaths.length > 0
160
+ ? { fullOutputPath: fullOutputPaths[0], fullOutputPaths }
161
+ : {}),
162
+ ...(contentOffload.offloadTruncation
163
+ ? { offloadTruncation: contentOffload.offloadTruncation }
164
+ : {}),
114
165
  });
115
166
  }
116
167
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/extensions/web/tools/fetch.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAmB,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AA+BxG,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CA2D5D"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/extensions/web/tools/fetch.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAmB,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AAgCxG,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAwE5D"}
@@ -2,11 +2,9 @@
2
2
  * web_fetch tool definition.
3
3
  */
4
4
  import { Type } from "typebox";
5
- import { truncateTail } from "../../../sdk/coding-agent/core/tools/truncate.js";
5
+ import { isAlreadyOffloaded, offloadLargeResult, } from "../../../sdk/coding-agent/core/tools/offload.js";
6
6
  import { fetchWebPage, WebFetchError } from "../utils/fetcher.js";
7
7
  import { convertContent } from "../utils/parser.js";
8
- const MAX_OUTPUT_LINES = 4000;
9
- const MAX_OUTPUT_BYTES = 200 * 1024; // 200 KB
10
8
  const webFetchSchema = Type.Object({
11
9
  url: Type.String({
12
10
  description: "The URL to fetch. Must begin with http:// or https://.",
@@ -32,7 +30,7 @@ export function registerWebFetchTool(ext) {
32
30
  "Supports HTTP/HTTPS only and rejects binary content.",
33
31
  promptSnippet: "`web_fetch { url, format?, timeout? }` — fetch a web page as markdown/text/HTML/JSON",
34
32
  parameters: webFetchSchema,
35
- async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
33
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
36
34
  const { url, format = "markdown", timeout = 30 } = params;
37
35
  try {
38
36
  const fetchResult = await fetchWebPage(url, {
@@ -40,19 +38,33 @@ export function registerWebFetchTool(ext) {
40
38
  signal,
41
39
  });
42
40
  const converted = await convertContent(fetchResult.content, format);
43
- const truncation = truncateTail(converted, {
44
- maxLines: MAX_OUTPUT_LINES,
45
- maxBytes: MAX_OUTPUT_BYTES,
46
- });
47
- const output = truncation.content;
48
- const truncationNotice = truncation.truncated
49
- ? `\n\n[Output truncated: exceeded ${truncation.truncatedBy === "bytes" ? `${MAX_OUTPUT_BYTES} bytes` : `${MAX_OUTPUT_LINES} lines`}. Original size: ${truncation.totalBytes} bytes, ${truncation.totalLines} lines.]`
50
- : "";
41
+ // Single truncation system: offloadLargeResult decides by its own
42
+ // byte/line thresholds and builds the head+tail preview + footer.
43
+ // The full content is passed directly — no pre-truncation here.
44
+ let fullOutputPath;
45
+ let output = converted;
46
+ let offloadTruncation;
47
+ if (!isAlreadyOffloaded(converted)) {
48
+ try {
49
+ const sessionId = ctx?.sessionManager?.getSessionId?.() ?? "shared";
50
+ const offloaded = offloadLargeResult(converted, "web_fetch", sessionId);
51
+ output = offloaded.preview;
52
+ if (offloaded.fullOutputPath) {
53
+ fullOutputPath = offloaded.fullOutputPath;
54
+ offloadTruncation = offloaded.truncation ?? undefined;
55
+ }
56
+ }
57
+ catch {
58
+ // Best-effort — serve the full content when offload fails.
59
+ output = converted;
60
+ }
61
+ }
62
+ const truncated = fullOutputPath !== undefined;
51
63
  return {
52
64
  content: [
53
65
  {
54
66
  type: "text",
55
- text: `✓ Fetched ${fetchResult.finalUrl}\nContent-Type: ${fetchResult.contentType || "unknown"}\nFormat: ${format}\nSize: ${fetchResult.contentLengthBytes} bytes${truncationNotice}\n\n${output}`,
67
+ text: `✓ Fetched ${fetchResult.finalUrl}\nContent-Type: ${fetchResult.contentType || "unknown"}\nFormat: ${format}\nSize: ${fetchResult.contentLengthBytes} bytes\n\n${output}`,
56
68
  },
57
69
  ],
58
70
  details: {
@@ -61,8 +73,9 @@ export function registerWebFetchTool(ext) {
61
73
  contentType: fetchResult.contentType,
62
74
  format,
63
75
  output,
64
- truncated: truncation.truncated,
65
- truncatedBy: truncation.truncatedBy,
76
+ truncated,
77
+ ...(fullOutputPath ? { fullOutputPath } : {}),
78
+ ...(offloadTruncation ? { offloadTruncation } : {}),
66
79
  },
67
80
  };
68
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tool-output-compressor.d.ts","sourceRoot":"","sources":["../../src/memory/tool-output-compressor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAEX,gBAAgB,EAIhB,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAGN,KAAK,0BAA0B,EAC/B,MAAM,+CAA+C,CAAC;AACvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAO5C,MAAM,MAAM,gBAAgB,GAAG,0BAA0B,CAAC;AAE1D,eAAO,MAAM,yBAAyB,EAAE,gBAEvC,CAAC;AAgsBF,MAAM,WAAW,gBAAgB;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,8BAA8B;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3F;AA8BD,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,OAAO,GAAG,8BAA8B,CAElG;AAED,wBAAgB,mCAAmC,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE1E;AAkDD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,MAAM,EAAE;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,EAC7F,GAAG,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,GAC7C,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,CAYvG;AAED,wBAAgB,4BAA4B,CAC3C,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,OAAO,GACd,IAAI,CAkCN"}
1
+ {"version":3,"file":"tool-output-compressor.d.ts","sourceRoot":"","sources":["../../src/memory/tool-output-compressor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAEX,gBAAgB,EAIhB,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAGN,KAAK,0BAA0B,EAC/B,MAAM,+CAA+C,CAAC;AACvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAQ5C,MAAM,MAAM,gBAAgB,GAAG,0BAA0B,CAAC;AAE1D,eAAO,MAAM,yBAAyB,EAAE,gBAEvC,CAAC;AAgsBF,MAAM,WAAW,gBAAgB;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,8BAA8B;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3F;AA8BD,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,OAAO,GAAG,8BAA8B,CAElG;AAED,wBAAgB,mCAAmC,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE1E;AAgGD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,MAAM,EAAE;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,EAC7F,GAAG,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,GAC7C,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,CAYvG;AAED,wBAAgB,4BAA4B,CAC3C,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,OAAO,GACd,IAAI,CAmCN"}
@@ -29,6 +29,7 @@
29
29
  */
30
30
  import { DEFAULT_COMPACTION_POLICY, } from "../sdk/coding-agent/core/compaction/policy.js";
31
31
  import { debugLog } from "./debug-log.js";
32
+ import { isAlreadyOffloaded, offloadLargeResult } from "../sdk/coding-agent/core/tools/offload.js";
32
33
  export const DEFAULT_COMPRESSOR_CONFIG = {
33
34
  ...DEFAULT_COMPACTION_POLICY.toolOutputCompressor,
34
35
  };
@@ -672,6 +673,55 @@ function resolveCompressorConfig(ctx) {
672
673
  const manager = ctx.settingsManager;
673
674
  return manager?.getCompactionPolicy?.().toolOutputCompressor ?? DEFAULT_COMPRESSOR_CONFIG;
674
675
  }
676
+ // ============================================================================
677
+ // Large-result offload fallback
678
+ // ============================================================================
679
+ /**
680
+ * Fallback offload for tool types without native preview+file handling.
681
+ *
682
+ * - Skips read/bash (they already compress natively) and anything already
683
+ * carrying a `Full output:` footer (notably the bash offload path).
684
+ * - Skips error results and non-text/binary content.
685
+ * - Persists the full text via `offloadLargeResult()` and swaps the event
686
+ * text for preview + footer. Small results pass through untouched.
687
+ *
688
+ * The `tool_result` hook carries no session id, so offloads land under the
689
+ * shared `offloads/shared/` namespace.
690
+ */
691
+ function maybeOffloadLargeResult(event) {
692
+ if (event.toolName === "read" || event.toolName === "bash")
693
+ return undefined;
694
+ if (event.isError)
695
+ return undefined;
696
+ const texts = event.content.filter((c) => c.type === "text" && typeof c.text === "string");
697
+ if (texts.length === 0)
698
+ return undefined;
699
+ const original = texts.map((c) => c.text).join("\n");
700
+ if (!original || original.includes("\0"))
701
+ return undefined;
702
+ if (isAlreadyOffloaded(original))
703
+ return undefined;
704
+ let offloaded;
705
+ try {
706
+ offloaded = offloadLargeResult(original, event.toolName, "shared");
707
+ }
708
+ catch {
709
+ return undefined;
710
+ }
711
+ if (!offloaded.fullOutputPath || !offloaded.truncation)
712
+ return undefined;
713
+ return {
714
+ content: [
715
+ ...event.content.filter((c) => c.type !== "text"),
716
+ { type: "text", text: offloaded.preview },
717
+ ],
718
+ details: {
719
+ ...(typeof event.details === "object" && event.details !== null ? event.details : {}),
720
+ fullOutputPath: offloaded.fullOutputPath,
721
+ offloadTruncation: offloaded.truncation,
722
+ },
723
+ };
724
+ }
675
725
  function compressedContentResult(runtime, event, toolName, original, compressed) {
676
726
  if (!compressed || compressed === original)
677
727
  return undefined;
@@ -734,7 +784,8 @@ export function registerToolOutputCompressor(ext, runtime) {
734
784
  const compressed = compressBashOutput(event, config);
735
785
  return compressedContentResult(runtime, event, "bash", original, compressed);
736
786
  }
737
- // Unknown tool types: do nothing
738
- return;
787
+ // All other tool types (web_fetch, browser_*, seo_*, pdf_*, …): fall back
788
+ // to large-result offloading so no truncated output is ever lost.
789
+ return maybeOffloadLargeResult(_event);
739
790
  });
740
791
  }
@@ -1 +1 @@
1
- {"version":3,"file":"history-projection.d.ts","sourceRoot":"","sources":["../../src/relay/history-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,QAAa,CAAC;AAEpD,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,iDAAiD,CAAC;AAoMhF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAwC9D;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,WAAW,EAAE,CAMtF"}
1
+ {"version":3,"file":"history-projection.d.ts","sourceRoot":"","sources":["../../src/relay/history-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,QAAa,CAAC;AAEpD,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,iDAAiD,CAAC;AAwMhF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAwC9D;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,WAAW,EAAE,CAMtF"}
@@ -72,6 +72,13 @@ function projectToolResult(event) {
72
72
  compactDetails.status = details.status;
73
73
  if ("mode" in details)
74
74
  compactDetails.mode = details.mode;
75
+ // Preserve offload pointers so the agent can re-read full outputs.
76
+ if ("fullOutputPath" in details)
77
+ compactDetails.fullOutputPath = details.fullOutputPath;
78
+ if ("offloadTruncation" in details)
79
+ compactDetails.offloadTruncation = details.offloadTruncation;
80
+ if ("truncation" in details)
81
+ compactDetails.truncation = truncateStrings(details.truncation);
75
82
  projectedResult = { ...result, details: compactDetails };
76
83
  }
77
84
  if (typeof projectedResult.content === "string") {
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Large-result offloading for tool outputs.
3
+ *
4
+ * Goal: no truncated tool result is ever lost. The full output goes to a
5
+ * file under `<configDir>/offloads/<sessionId>/`, and only a head+tail
6
+ * preview plus a footer (path + re-read instructions) enters the LLM context.
7
+ * The agent can then page through the file with `read` (offset/limit) or
8
+ * `bash` (sed/head/tail).
9
+ *
10
+ * This is the generic version of the pattern pioneered by the bash tool
11
+ * (preview + `Full output: <path>` footer). Small results pass through
12
+ * untouched — no file, no overhead. Binary-looking content (NUL bytes) is
13
+ * never offloaded.
14
+ */
15
+ /** Default byte threshold above which a result is offloaded to disk. */
16
+ export declare const DEFAULT_OFFLOAD_BYTES: number;
17
+ /** Default line threshold above which a result is offloaded to disk. */
18
+ export declare const DEFAULT_OFFLOAD_LINES = 2000;
19
+ /** Preview head size (lines kept from the start). */
20
+ export declare const PREVIEW_HEAD_LINES = 100;
21
+ /** Preview tail size (lines kept from the end). */
22
+ export declare const PREVIEW_TAIL_LINES = 100;
23
+ /** Emergency byte cap for previews (head+tail can still be huge with long lines). */
24
+ export declare const PREVIEW_MAX_BYTES: number;
25
+ /** Marker identifying an already-offloaded result (bash tool compat). */
26
+ export declare const OFFLOAD_MARKER = "Full output:";
27
+ /** Anchored marker check: only a real `Full output: <path>.log` footer counts. */
28
+ export declare const OFFLOAD_PATH_RE: RegExp;
29
+ /**
30
+ * Parse a positive-int env override. Returns `fallback` when the variable is
31
+ * unset, unparseable, zero, or negative — a non-positive budget would
32
+ * otherwise disable offloading/GC entirely, so it is treated as "not set".
33
+ */
34
+ export declare function envPositiveInt(name: string, fallback: number): number;
35
+ /** Effective byte limit (override via `SPECTRAL_OFFLOAD_BYTES`). */
36
+ export declare function getOffloadByteLimit(): number;
37
+ /** Effective line limit (override via `SPECTRAL_OFFLOAD_LINES`). */
38
+ export declare function getOffloadLineLimit(): number;
39
+ export interface OffloadTruncation {
40
+ originalBytes: number;
41
+ shownBytes: number;
42
+ headLines: number;
43
+ tailLines: number;
44
+ omittedLines: number;
45
+ totalLines: number;
46
+ fullOutputPath: string;
47
+ }
48
+ export interface OffloadResult {
49
+ /** Model-facing text: full original for small results, preview+footer for large ones. */
50
+ preview: string;
51
+ /** Absolute path of the persisted full output, or null when nothing was written. */
52
+ fullOutputPath: string | null;
53
+ /** Truncation metadata, or null when nothing was offloaded. */
54
+ truncation: OffloadTruncation | null;
55
+ }
56
+ export declare function countLines(text: string): number;
57
+ /** Marker for results that were already offloaded (or produced by the bash tool). */
58
+ export declare function isAlreadyOffloaded(text: string): boolean;
59
+ /**
60
+ * Whether `text` is large enough to deserve offloading.
61
+ * Binary-looking content (NUL bytes) is never offloaded.
62
+ */
63
+ export declare function shouldOffload(text: string): boolean;
64
+ export interface PreviewResult {
65
+ preview: string;
66
+ headLines: number;
67
+ tailLines: number;
68
+ omittedLines: number;
69
+ totalLines: number;
70
+ }
71
+ /**
72
+ * Build a head+tail preview. Never cuts in the middle of a line (except via
73
+ * the emergency byte cap below); the gap is replaced with a
74
+ * `[... N lines omitted ...]` marker. When head+tail itself exceeds
75
+ * `PREVIEW_MAX_BYTES` (e.g. a single giant line), the preview is cut
76
+ * mid-line with a `[... N bytes omitted ...]` marker so the LLM context
77
+ * never receives an unbounded preview.
78
+ */
79
+ export declare function buildPreview(text: string, headLines?: number, tailLines?: number): PreviewResult;
80
+ export interface OffloadFooterOptions {
81
+ headLines?: number;
82
+ tailLines?: number;
83
+ totalLines?: number;
84
+ }
85
+ /**
86
+ * Single-line footer appended to a preview. Carries the full-output path plus
87
+ * a copy-pasteable re-read hint for the agent.
88
+ */
89
+ export declare function formatOffloadFooter(path: string, shown: number, total: number, opts?: OffloadFooterOptions): string;
90
+ /**
91
+ * Resolve (and create) the offload file path for a tool result:
92
+ * `<configDir>/offloads/<sessionId>/<tool>-<timestamp>-<rand>.log`.
93
+ * Honors `SPECTRAL_CONFIG_DIR` via `getConfigDir()`.
94
+ */
95
+ export declare function resolveOffloadPath(sessionId: string, toolName: string): string;
96
+ /**
97
+ * Offload `text` to disk when it exceeds the size thresholds.
98
+ * Small (or binary, or already-offloaded) results are returned unchanged
99
+ * with `fullOutputPath: null` — no file, no overhead.
100
+ */
101
+ export declare function offloadLargeResult(text: string, toolName: string, sessionId: string): OffloadResult;
102
+ //# sourceMappingURL=offload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offload.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/tools/offload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAQH,wEAAwE;AACxE,eAAO,MAAM,qBAAqB,QAAY,CAAC;AAC/C,wEAAwE;AACxE,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,qDAAqD;AACrD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,mDAAmD;AACnD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AACtC,qFAAqF;AACrF,eAAO,MAAM,iBAAiB,QAAoB,CAAC;AACnD,yEAAyE;AACzE,eAAO,MAAM,cAAc,iBAAiB,CAAC;AAC7C,kFAAkF;AAClF,eAAO,MAAM,eAAe,QAA0B,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKrE;AAED,oEAAoE;AACpE,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,oEAAoE;AACpE,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,MAAM,WAAW,iBAAiB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC7B,yFAAyF;IACzF,OAAO,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,+DAA+D;IAC/D,UAAU,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACrC;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK/C;AAED,qFAAqF;AACrF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAKnD;AAED,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC3B,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,MAA2B,EACtC,SAAS,GAAE,MAA2B,GACpC,aAAa,CAmBf;AAmCD,MAAM,WAAW,oBAAoB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAClC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,oBAAyB,GAC7B,MAAM,CAiBR;AAOD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAK9E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,aAAa,CA6BnG"}
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Large-result offloading for tool outputs.
3
+ *
4
+ * Goal: no truncated tool result is ever lost. The full output goes to a
5
+ * file under `<configDir>/offloads/<sessionId>/`, and only a head+tail
6
+ * preview plus a footer (path + re-read instructions) enters the LLM context.
7
+ * The agent can then page through the file with `read` (offset/limit) or
8
+ * `bash` (sed/head/tail).
9
+ *
10
+ * This is the generic version of the pattern pioneered by the bash tool
11
+ * (preview + `Full output: <path>` footer). Small results pass through
12
+ * untouched — no file, no overhead. Binary-looking content (NUL bytes) is
13
+ * never offloaded.
14
+ */
15
+ import { randomBytes } from "node:crypto";
16
+ import { mkdirSync, writeFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { getConfigDir } from "../../../../config.js";
19
+ import { DEFAULT_MAX_BYTES } from "./truncate.js";
20
+ /** Default byte threshold above which a result is offloaded to disk. */
21
+ export const DEFAULT_OFFLOAD_BYTES = 50 * 1024; // 50KB
22
+ /** Default line threshold above which a result is offloaded to disk. */
23
+ export const DEFAULT_OFFLOAD_LINES = 2000;
24
+ /** Preview head size (lines kept from the start). */
25
+ export const PREVIEW_HEAD_LINES = 100;
26
+ /** Preview tail size (lines kept from the end). */
27
+ export const PREVIEW_TAIL_LINES = 100;
28
+ /** Emergency byte cap for previews (head+tail can still be huge with long lines). */
29
+ export const PREVIEW_MAX_BYTES = DEFAULT_MAX_BYTES;
30
+ /** Marker identifying an already-offloaded result (bash tool compat). */
31
+ export const OFFLOAD_MARKER = "Full output:";
32
+ /** Anchored marker check: only a real `Full output: <path>.log` footer counts. */
33
+ export const OFFLOAD_PATH_RE = /Full output: \S+\.log/;
34
+ /**
35
+ * Parse a positive-int env override. Returns `fallback` when the variable is
36
+ * unset, unparseable, zero, or negative — a non-positive budget would
37
+ * otherwise disable offloading/GC entirely, so it is treated as "not set".
38
+ */
39
+ export function envPositiveInt(name, fallback) {
40
+ const raw = process.env[name];
41
+ if (!raw)
42
+ return fallback;
43
+ const parsed = Number.parseInt(raw, 10);
44
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
45
+ }
46
+ /** Effective byte limit (override via `SPECTRAL_OFFLOAD_BYTES`). */
47
+ export function getOffloadByteLimit() {
48
+ return envPositiveInt("SPECTRAL_OFFLOAD_BYTES", DEFAULT_OFFLOAD_BYTES);
49
+ }
50
+ /** Effective line limit (override via `SPECTRAL_OFFLOAD_LINES`). */
51
+ export function getOffloadLineLimit() {
52
+ return envPositiveInt("SPECTRAL_OFFLOAD_LINES", DEFAULT_OFFLOAD_LINES);
53
+ }
54
+ export function countLines(text) {
55
+ if (text.length === 0)
56
+ return 0;
57
+ const lines = text.split("\n");
58
+ // A trailing newline does not start a new line.
59
+ return text.endsWith("\n") ? lines.length - 1 : lines.length;
60
+ }
61
+ /** Marker for results that were already offloaded (or produced by the bash tool). */
62
+ export function isAlreadyOffloaded(text) {
63
+ return OFFLOAD_PATH_RE.test(text);
64
+ }
65
+ /**
66
+ * Whether `text` is large enough to deserve offloading.
67
+ * Binary-looking content (NUL bytes) is never offloaded.
68
+ */
69
+ export function shouldOffload(text) {
70
+ if (!text || text.includes("\0"))
71
+ return false;
72
+ if (isAlreadyOffloaded(text))
73
+ return false;
74
+ if (Buffer.byteLength(text, "utf-8") > getOffloadByteLimit())
75
+ return true;
76
+ return countLines(text) > getOffloadLineLimit();
77
+ }
78
+ /**
79
+ * Build a head+tail preview. Never cuts in the middle of a line (except via
80
+ * the emergency byte cap below); the gap is replaced with a
81
+ * `[... N lines omitted ...]` marker. When head+tail itself exceeds
82
+ * `PREVIEW_MAX_BYTES` (e.g. a single giant line), the preview is cut
83
+ * mid-line with a `[... N bytes omitted ...]` marker so the LLM context
84
+ * never receives an unbounded preview.
85
+ */
86
+ export function buildPreview(text, headLines = PREVIEW_HEAD_LINES, tailLines = PREVIEW_TAIL_LINES) {
87
+ const rawLines = text.split("\n");
88
+ // Drop the phantom empty element from a trailing newline.
89
+ const lines = text.endsWith("\n") && rawLines.length > 0 ? rawLines.slice(0, -1) : rawLines;
90
+ const totalLines = lines.length;
91
+ if (totalLines <= headLines + tailLines) {
92
+ return capPreviewBytes(text, {
93
+ preview: text,
94
+ headLines: totalLines,
95
+ tailLines: 0,
96
+ omittedLines: 0,
97
+ totalLines,
98
+ });
99
+ }
100
+ const head = lines.slice(0, headLines);
101
+ const tail = lines.slice(totalLines - tailLines);
102
+ const omittedLines = totalLines - headLines - tailLines;
103
+ const preview = [...head, `[... ${omittedLines} lines omitted ...]`, ...tail].join("\n");
104
+ return capPreviewBytes(preview, { preview, headLines, tailLines, omittedLines, totalLines });
105
+ }
106
+ /**
107
+ * Emergency byte cap for previews. Long-line blobs (e.g. minified JSON on a
108
+ * single line) can make head+tail previews arbitrarily large; cut them
109
+ * mid-line with a byte-omitted marker. UTF-8 safe via Buffer slicing.
110
+ */
111
+ function capPreviewBytes(textOrResult, fallback) {
112
+ const text = typeof textOrResult === "string" ? textOrResult : textOrResult.preview;
113
+ const base = typeof textOrResult === "string"
114
+ ? fallback
115
+ : {
116
+ preview: textOrResult.preview,
117
+ headLines: textOrResult.headLines,
118
+ tailLines: textOrResult.tailLines,
119
+ omittedLines: textOrResult.omittedLines,
120
+ totalLines: textOrResult.totalLines,
121
+ };
122
+ if (Buffer.byteLength(text, "utf-8") <= PREVIEW_MAX_BYTES) {
123
+ return { ...base, preview: text };
124
+ }
125
+ const buf = Buffer.from(text, "utf-8");
126
+ // Leave room for the marker; walk back to a UTF-8 char boundary.
127
+ const markerReserve = 64;
128
+ let end = Math.max(0, PREVIEW_MAX_BYTES - markerReserve);
129
+ while (end > 0 && end < buf.length && (buf[end] & 0xc0) === 0x80)
130
+ end--;
131
+ const omitted = buf.length - end;
132
+ const preview = buf.subarray(0, end).toString("utf-8") + `\n[... ${omitted} bytes omitted ...]`;
133
+ return { ...base, preview };
134
+ }
135
+ /**
136
+ * Single-line footer appended to a preview. Carries the full-output path plus
137
+ * a copy-pasteable re-read hint for the agent.
138
+ */
139
+ export function formatOffloadFooter(path, shown, total, opts = {}) {
140
+ const headLines = opts.headLines ?? PREVIEW_HEAD_LINES;
141
+ const tailLines = opts.tailLines ?? PREVIEW_TAIL_LINES;
142
+ // Guard against inconsistent callers (e.g. byte-counted `shown` on a
143
+ // line-based total): never claim to show more than exists.
144
+ const safeShown = Math.min(shown, total);
145
+ const safeTotal = Math.max(total, 0);
146
+ let readHint;
147
+ if (opts.totalLines !== undefined && opts.totalLines > headLines + tailLines) {
148
+ const tailStart = opts.totalLines - tailLines + 1;
149
+ readHint =
150
+ `Read with: sed -n '1,${headLines}p' ${path} | head -c ${DEFAULT_MAX_BYTES} ` +
151
+ `or sed -n '${tailStart},${opts.totalLines}p' ${path} | head -c ${DEFAULT_MAX_BYTES}`;
152
+ }
153
+ else {
154
+ readHint = `Read with: sed -n '1,${headLines}p' ${path} | head -c ${DEFAULT_MAX_BYTES}`;
155
+ }
156
+ return `[Truncated: showing ${safeShown} of ${safeTotal} bytes. Full output: ${path}. ${readHint}]`;
157
+ }
158
+ function sanitizeSegment(segment) {
159
+ const cleaned = segment.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
160
+ return cleaned || "shared";
161
+ }
162
+ /**
163
+ * Resolve (and create) the offload file path for a tool result:
164
+ * `<configDir>/offloads/<sessionId>/<tool>-<timestamp>-<rand>.log`.
165
+ * Honors `SPECTRAL_CONFIG_DIR` via `getConfigDir()`.
166
+ */
167
+ export function resolveOffloadPath(sessionId, toolName) {
168
+ const dir = join(getConfigDir(), "offloads", sanitizeSegment(sessionId));
169
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
170
+ const stamp = `${Date.now()}-${randomBytes(4).toString("hex")}`;
171
+ return join(dir, `${sanitizeSegment(toolName)}-${stamp}.log`);
172
+ }
173
+ /**
174
+ * Offload `text` to disk when it exceeds the size thresholds.
175
+ * Small (or binary, or already-offloaded) results are returned unchanged
176
+ * with `fullOutputPath: null` — no file, no overhead.
177
+ */
178
+ export function offloadLargeResult(text, toolName, sessionId) {
179
+ if (!shouldOffload(text)) {
180
+ return { preview: text, fullOutputPath: null, truncation: null };
181
+ }
182
+ const originalBytes = Buffer.byteLength(text, "utf-8");
183
+ const { preview, headLines, tailLines, omittedLines, totalLines } = buildPreview(text);
184
+ const fullOutputPath = resolveOffloadPath(sessionId, toolName);
185
+ // Owner-only: offloads may contain secrets (headers, tokens, page bodies).
186
+ writeFileSync(fullOutputPath, text, { encoding: "utf-8", mode: 0o600 });
187
+ // `shown` is measured on the actually displayed preview text.
188
+ const footer = formatOffloadFooter(fullOutputPath, Buffer.byteLength(preview, "utf-8"), originalBytes, {
189
+ headLines,
190
+ tailLines,
191
+ totalLines,
192
+ });
193
+ const previewWithFooter = `${preview}\n${footer}`;
194
+ return {
195
+ preview: previewWithFooter,
196
+ fullOutputPath,
197
+ truncation: {
198
+ originalBytes,
199
+ shownBytes: Buffer.byteLength(previewWithFooter, "utf-8"),
200
+ headLines,
201
+ tailLines,
202
+ omittedLines,
203
+ totalLines,
204
+ fullOutputPath,
205
+ },
206
+ };
207
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"agent-bridge.d.ts","sourceRoot":"","sources":["../../src/server/agent-bridge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent-bridge.d.ts","sourceRoot":"","sources":["../../src/server/agent-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAaH,OAAO,EAQL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EAEtB,MAAM,8BAA8B,CAAC;AAatC,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AAatC,OAAO,EACL,kBAAkB,IAAI,yBAAyB,EAEhD,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAG3E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,UAAU,4BAA4B;IACpC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CACpC;AAiBD,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG,OAAO,yBAAyB,CAAC;AAmCpE,MAAM,WAAW,kBAAkB;IACjC,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,IAAI,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACnC;;;;;OAKG;IACH,0BAA0B,EAAE,CAAC,GAAG,EAAE;QAChC,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,6EAA6E;QAC7E,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,KAAK,IAAI,CAAC;IACX;;;OAGG;IACH,yBAAyB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;IAC/B;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAC9C;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,oBAAoB,CAAC;IAC1C;;;;;OAKG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC;;;;;;;;;;;OAWG;IACH,yBAAyB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtE;AAqkBD,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAC,CAAe;IAC/B,OAAO,CAAC,iCAAiC,CAAqB;IAC9D,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,WAAW,CAAC,CAAa;IACjC,OAAO,CAAC,OAAO,CAAC,CAA0B;IAC1C,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAqB;IACjC;;;;;OAKG;IACH,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC;;;;OAIG;IACH,OAAO,CAAC,aAAa,CAAC,CAAiB;IACvC;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB,CAAC,CAAS;IAEpC,OAAO,CAAC,mBAAmB,CAAC,CAAgC;IAC5D,OAAO,CAAC,YAAY,CAAC,CAAuB;IAC5C,8EAA8E;IAC9E,OAAO,CAAC,kBAAkB,CAAC,CAAgC;IAC3D;;;;;;;;OAQG;IACH,OAAO,CAAC,eAAe,CAAK;IAC5B,sFAAsF;IACtF,OAAO,CAAC,iBAAiB,CAMT;IAChB,OAAO,CAAC,WAAW,CACuD;IAC1E,OAAO,CAAC,mBAAmB,CAA+B;IAC1D;;;;OAIG;IACH,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,cAAc,CAAC,CAA2B;IAClD,OAAO,CAAC,mBAAmB,CAAC,CAAgC;IAC5D;;;;;OAKG;IACH,OAAO,CAAC,wBAAwB,CAO5B;gBAEQ,IAAI,EAAE,kBAAkB;IAIpC;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA0X5B,OAAO,CAAC,4BAA4B;IAQpC;;;;;OAKG;YACW,qBAAqB;IAcnC;;;OAGG;IACG,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC;IAG3C;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAmC3B,OAAO,CAAC,sBAAsB;YAahB,oBAAoB;IAoBlC;;;;;;;;;;;;OAYG;IACH;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,0BAA0B;IA6ElC;;;;;;;;;;;;;;;OAeG;IACH;;;;;;;;OAQG;IACH,wBAAwB,IAAI,MAAM,GAAG,SAAS;IAI9C;;;;OAIG;IACH,eAAe,IACX;QAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GACxE,SAAS;IAIb,mDAAmD;IACnD,WAAW,IAAI,OAAO;IAItB,mEAAmE;IACnE,UAAU,IAAI,OAAO;IAIrB,2EAA2E;IAC3E,MAAM,IAAI,OAAO;IAIjB,gBAAgB,IAAI,KAAK,CAAC;QACxB,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC;IAgBF,iBAAiB,IAAI;QACnB,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;QACtE,QAAQ,EAAE;YACR,QAAQ,EAAE,OAAO,CAAC;YAClB,UAAU,EAAE,OAAO,CAAC;YACpB,UAAU,EAAE,OAAO,CAAC;YACpB,MAAM,EAAE,OAAO,CAAC;SACjB,CAAC;KACH;IAYD,OAAO,CAAC,iBAAiB;IAmCnB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IA2DpE;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,iCAAiC;IAkBzC;;;;;;OAMG;IACH,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAoBpD;;;;;;;;;OASG;IACH,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI;IAI7D;;;;;;;;;;;;OAYG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAgDrE;;;;;;;;OAQG;YACW,iBAAiB;IAwB/B;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,0BAA0B;IAiBlC;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAM1B,2EAA2E;IAC3E,OAAO,CAAC,6BAA6B;IAUrC;;;;OAIG;IACH,OAAO,CAAC,8BAA8B;IAQtC,yEAAyE;IACzE,OAAO,CAAC,iCAAiC;IAOzC,OAAO,CAAC,iBAAiB;IAazB;;;;;OAKG;IACH,OAAO,CAAC,uBAAuB;IAc/B,OAAO,CAAC,uBAAuB;IAMzB,OAAO,CACX,OAAO,CAAC,EAAE,MAAM,GAAG,4BAA4B,GAC9C,OAAO,CAAC,gBAAgB,CAAC;IAY5B,OAAO,IAAI,IAAI;IA6Bf;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAqBrB;;;;;;;;OAQG;IACH,OAAO,CAAC,6BAA6B;IAiCrC;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAiB7B,uEAAuE;IACvE,OAAO,CAAC,wBAAwB;IAMhC;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,sBAAsB;IAwD9B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI;CAskBzC"}
@@ -446,6 +446,13 @@ function parseWireToolEvents(eventsJsonl) {
446
446
  * images and other rich results.
447
447
  */
448
448
  const MAX_REHYDRATED_TEXT = 100_000;
449
+ /**
450
+ * Ceiling for blocks that already carry an offload footer (`Full output:`).
451
+ * These blocks are a bounded preview + a re-readable path, so cutting them
452
+ * with the generic limit would destroy the footer. Keep them whole up to
453
+ * this size; larger ones lose their middle but keep head + footer.
454
+ */
455
+ const MAX_REHYDRATED_OFFLOAD_TEXT = 200_000;
449
456
  function sanitizeRehydratedBlock(block) {
450
457
  // Tool-result image blocks are not accepted by every provider; the raw
451
458
  // result remains in events_jsonl for the UI, while the model gets a compact reference.
@@ -460,6 +467,20 @@ function sanitizeRehydratedBlock(block) {
460
467
  // Truncate oversized text blocks to prevent context overflow.
461
468
  if (block.type !== "text")
462
469
  return block;
470
+ if (block.text.includes("Full output:")) {
471
+ if (block.text.length <= MAX_REHYDRATED_OFFLOAD_TEXT)
472
+ return block;
473
+ // Keep the head (preview) and the tail (offload footer), drop the middle.
474
+ const keepHead = Math.floor(MAX_REHYDRATED_OFFLOAD_TEXT * 0.7);
475
+ const keepTail = MAX_REHYDRATED_OFFLOAD_TEXT - keepHead;
476
+ const omitted = block.text.length - keepHead - keepTail;
477
+ return {
478
+ ...block,
479
+ text: block.text.slice(0, keepHead) +
480
+ `\n\n[... ${omitted.toLocaleString()} bytes omitted from rehydrated offload preview ...]\n\n` +
481
+ block.text.slice(block.text.length - keepTail),
482
+ };
483
+ }
463
484
  if (block.text.length <= MAX_REHYDRATED_TEXT)
464
485
  return block;
465
486
  const omitted = block.text.length - MAX_REHYDRATED_TEXT;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Garbage collection for large-result offload files.
3
+ *
4
+ * Offloads live under `<configDir>/offloads/<sessionId>/*.log`. This sweep
5
+ * removes per-session directories older than `SPECTRAL_OFFLOAD_TTL_DAYS`
6
+ * (default 7) and, when the total still exceeds `SPECTRAL_OFFLOAD_MAX_MB`
7
+ * (default 500), evicts the oldest sessions first.
8
+ *
9
+ * Best-effort and synchronous: callers fire-and-forget it during preflight.
10
+ */
11
+ export declare const DEFAULT_OFFLOAD_TTL_DAYS = 7;
12
+ export declare const DEFAULT_OFFLOAD_MAX_MB = 500;
13
+ export declare function getOffloadTtlMs(): number;
14
+ export declare function getOffloadMaxBytes(): number;
15
+ export interface OffloadSweepResult {
16
+ removedSessions: number;
17
+ removedBytes: number;
18
+ }
19
+ /** Remove stale/over-budget offload directories. Never throws. */
20
+ export declare function sweepOffloads(root?: string): OffloadSweepResult;
21
+ //# sourceMappingURL=offload-gc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offload-gc.d.ts","sourceRoot":"","sources":["../../src/server/offload-gc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,wBAAgB,eAAe,IAAI,MAAM,CAGxC;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAG3C;AAsBD,MAAM,WAAW,kBAAkB;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAoD/D"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Garbage collection for large-result offload files.
3
+ *
4
+ * Offloads live under `<configDir>/offloads/<sessionId>/*.log`. This sweep
5
+ * removes per-session directories older than `SPECTRAL_OFFLOAD_TTL_DAYS`
6
+ * (default 7) and, when the total still exceeds `SPECTRAL_OFFLOAD_MAX_MB`
7
+ * (default 500), evicts the oldest sessions first.
8
+ *
9
+ * Best-effort and synchronous: callers fire-and-forget it during preflight.
10
+ */
11
+ import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { getConfigDir } from "../config.js";
14
+ import { envPositiveInt } from "../sdk/coding-agent/core/tools/offload.js";
15
+ export const DEFAULT_OFFLOAD_TTL_DAYS = 7;
16
+ export const DEFAULT_OFFLOAD_MAX_MB = 500;
17
+ export function getOffloadTtlMs() {
18
+ const days = envPositiveInt("SPECTRAL_OFFLOAD_TTL_DAYS", DEFAULT_OFFLOAD_TTL_DAYS);
19
+ return days * 24 * 60 * 60 * 1000;
20
+ }
21
+ export function getOffloadMaxBytes() {
22
+ const mb = envPositiveInt("SPECTRAL_OFFLOAD_MAX_MB", DEFAULT_OFFLOAD_MAX_MB);
23
+ return mb * 1024 * 1024;
24
+ }
25
+ function dirSizeBytes(dir) {
26
+ let total = 0;
27
+ let entries;
28
+ try {
29
+ entries = readdirSync(dir, { withFileTypes: true });
30
+ }
31
+ catch {
32
+ return 0;
33
+ }
34
+ for (const entry of entries) {
35
+ const full = join(dir, entry.name);
36
+ try {
37
+ if (entry.isDirectory())
38
+ total += dirSizeBytes(full);
39
+ else if (entry.isFile())
40
+ total += statSync(full).size;
41
+ }
42
+ catch {
43
+ // Skip unreadable entries — GC is best-effort.
44
+ }
45
+ }
46
+ return total;
47
+ }
48
+ /** Remove stale/over-budget offload directories. Never throws. */
49
+ export function sweepOffloads(root) {
50
+ const result = { removedSessions: 0, removedBytes: 0 };
51
+ let base;
52
+ try {
53
+ base = root ?? join(getConfigDir(), "offloads");
54
+ }
55
+ catch {
56
+ return result;
57
+ }
58
+ if (!existsSync(base))
59
+ return result;
60
+ let sessionDirs;
61
+ try {
62
+ sessionDirs = readdirSync(base);
63
+ }
64
+ catch {
65
+ return result;
66
+ }
67
+ const ttlMs = getOffloadTtlMs();
68
+ const now = Date.now();
69
+ const removeDir = (dir) => {
70
+ try {
71
+ result.removedBytes += dirSizeBytes(dir);
72
+ rmSync(dir, { recursive: true, force: true });
73
+ result.removedSessions++;
74
+ }
75
+ catch {
76
+ // Best-effort.
77
+ }
78
+ };
79
+ // Pass 1: TTL expiry by directory mtime.
80
+ const survivors = [];
81
+ for (const name of sessionDirs) {
82
+ const dir = join(base, name);
83
+ let stat;
84
+ try {
85
+ stat = statSync(dir);
86
+ if (!stat.isDirectory())
87
+ continue;
88
+ }
89
+ catch {
90
+ continue;
91
+ }
92
+ if (now - stat.mtimeMs > ttlMs)
93
+ removeDir(dir);
94
+ else
95
+ survivors.push({ dir, mtime: stat.mtimeMs, size: dirSizeBytes(dir) });
96
+ }
97
+ // Pass 2: total-size budget, oldest first.
98
+ let total = survivors.reduce((sum, s) => sum + s.size, 0);
99
+ const maxBytes = getOffloadMaxBytes();
100
+ if (total <= maxBytes)
101
+ return result;
102
+ survivors.sort((a, b) => a.mtime - b.mtime);
103
+ for (const s of survivors) {
104
+ if (total <= maxBytes)
105
+ break;
106
+ const before = s.size;
107
+ removeDir(s.dir);
108
+ total -= before;
109
+ }
110
+ return result;
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.9.209",
3
+ "version": "0.9.211",
4
4
  "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,