billion-context 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,10 +3,40 @@
3
3
  // src/config.ts
4
4
  import { defaultConfig } from "acp-kernel";
5
5
  import { readFileSync } from "fs";
6
- function safeReadJson(path) {
6
+
7
+ // src/paths.ts
8
+ import { homedir } from "os";
9
+ import path from "path";
10
+ function xdg(envVar, fallback) {
11
+ const v = process.env[envVar];
12
+ if (v && v.length > 0) return path.resolve(v);
13
+ return path.join(homedir(), fallback);
14
+ }
15
+ function configDir() {
16
+ return path.join(xdg("XDG_CONFIG_HOME", ".config"), "billion-context");
17
+ }
18
+ function configFile() {
19
+ const env = process.env.BILI_CONFIG_FILE;
20
+ if (env && env.length > 0) return path.resolve(env);
21
+ return path.join(configDir(), "billion-context.json");
22
+ }
23
+ function dataDir() {
24
+ return path.join(xdg("XDG_DATA_HOME", ".local/share"), "billion-context");
25
+ }
26
+ function sessionsDir() {
27
+ const env = process.env.BILI_SESSIONS_DIR;
28
+ if (env && env.length > 0) return path.resolve(env);
29
+ return path.join(dataDir(), "sessions");
30
+ }
31
+
32
+ // src/config.ts
33
+ function safeReadJson(path4) {
7
34
  try {
8
- return JSON.parse(readFileSync(path, "utf8"));
9
- } catch {
35
+ return JSON.parse(readFileSync(path4, "utf8"));
36
+ } catch (e) {
37
+ if (e.code !== "ENOENT") {
38
+ console.error(`[acp-config] failed to parse ${path4}: ${String(e)}`);
39
+ }
10
40
  return void 0;
11
41
  }
12
42
  }
@@ -20,6 +50,7 @@ var CONTEXT_LIMIT_TABLE = [
20
50
  { match: /^gemini-2\.5/i, limit: 1e6 },
21
51
  { match: /^gemini-1\.5/i, limit: 1e6 },
22
52
  { match: /^glm-4\.6/i, limit: 128e3 },
53
+ { match: /^glm-5/i, limit: 1e6 },
23
54
  { match: /^glm-/i, limit: 128e3 },
24
55
  { match: /^deepseek/i, limit: 64e3 },
25
56
  { match: /^qwen/i, limit: 128e3 },
@@ -33,25 +64,44 @@ function lookupContextLimit(model) {
33
64
  }
34
65
  return void 0;
35
66
  }
67
+ function resolveContextLimit(routes, provider, model) {
68
+ if (!model) return void 0;
69
+ if (provider) {
70
+ const route = routes[provider];
71
+ if (route?.models) {
72
+ const m = route.models[model];
73
+ if (m?.context && m.context > 0) return m.context;
74
+ }
75
+ }
76
+ return lookupContextLimit(model);
77
+ }
36
78
  function loadOptions(env = process.env) {
37
- const port = parseInt(env.ACP_PORT ?? env.PORT ?? "8787", 10);
38
- const host = env.ACP_HOST ?? "127.0.0.1";
39
- const upstream = (env.ACP_UPSTREAM ?? "https://api.anthropic.com").replace(/\/$/, "");
79
+ const fileConfig = loadConfigFile();
80
+ const port = parseInt(env.ACP_PORT ?? env.PORT ?? `${fileConfig.port ?? 8787}`, 10);
81
+ const host = env.ACP_HOST ?? fileConfig.host ?? "127.0.0.1";
82
+ const upstream = (env.ACP_UPSTREAM ?? fileConfig.upstream ?? "https://api.anthropic.com").replace(/\/$/, "");
40
83
  let routes = {};
41
- const routesPath = env.ACP_PROVIDERS ?? "";
84
+ const routesPath = env.ACP_PROVIDERS ?? fileConfig.providersPath ?? "";
42
85
  if (routesPath) {
43
86
  const parsed = safeReadJson(routesPath);
44
87
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
45
88
  for (const [k, v] of Object.entries(parsed)) {
46
- if (typeof v === "string" && v.length > 0) routes[k] = v.replace(/\/$/, "");
89
+ const route = parseRouteEntry(v);
90
+ if (route) routes[k] = route;
47
91
  }
48
92
  }
49
93
  }
50
- const modelContextLimit = parseInt(env.ACP_MODEL_CONTEXT_LIMIT ?? "200000", 10);
51
- const enabled = (env.ACP_CONDENSE_ENABLED ?? "1") !== "0";
52
- const keepRecentToolResults = parseInt(env.ACP_KEEP_RECENT_TOOL_RESULTS ?? "6", 10);
53
- const minCharsToCondense = parseInt(env.ACP_MIN_CHARS_TO_CONDENSE ?? "1500", 10);
54
- const maxKeptChars = parseInt(env.ACP_MAX_KEPT_CHARS ?? "400", 10);
94
+ if (fileConfig.providers) {
95
+ for (const [k, v] of Object.entries(fileConfig.providers)) {
96
+ const route = parseRouteEntry(v);
97
+ if (route && !routes[k]) routes[k] = route;
98
+ }
99
+ }
100
+ const modelContextLimit = parseInt(env.ACP_MODEL_CONTEXT_LIMIT ?? `${fileConfig.modelContextLimit ?? 2e5}`, 10);
101
+ const enabled = (env.ACP_CONDENSE_ENABLED ?? (fileConfig.condense?.enabled === false ? "0" : "1")) !== "0";
102
+ const keepRecentToolResults = parseInt(env.ACP_KEEP_RECENT_TOOL_RESULTS ?? `${fileConfig.condense?.keepRecentToolResults ?? 6}`, 10);
103
+ const minCharsToCondense = parseInt(env.ACP_MIN_CHARS_TO_CONDENSE ?? `${fileConfig.condense?.minCharsToCondense ?? 1500}`, 10);
104
+ const maxKeptChars = parseInt(env.ACP_MAX_KEPT_CHARS ?? `${fileConfig.condense?.maxKeptChars ?? 400}`, 10);
55
105
  return {
56
106
  port: Number.isFinite(port) ? port : 8787,
57
107
  host,
@@ -61,23 +111,83 @@ function loadOptions(env = process.env) {
61
111
  kernelConfig: defaultConfig(modelContextLimit),
62
112
  condense: { enabled, keepRecentToolResults, minCharsToCondense, maxKeptChars },
63
113
  compress: {
64
- injectTool: (env.ACP_COMPRESS_TOOL ?? "1") !== "0",
65
- injectNudge: (env.ACP_COMPRESS_NUDGE ?? "1") !== "0"
114
+ injectTool: (env.ACP_COMPRESS_TOOL ?? (fileConfig.compress?.injectTool === false ? "0" : "1")) !== "0",
115
+ injectNudge: (env.ACP_COMPRESS_NUDGE ?? (fileConfig.compress?.injectNudge === false ? "0" : "1")) !== "0"
66
116
  },
67
- sessionHeader: env.ACP_SESSION_HEADER ?? "x-acp-session",
68
- log: env.ACP_LOG !== "0",
69
- debug: (env.ACP_DEBUG ?? "0") === "1",
70
- dumpSse: env.ACP_DUMP_SSE || void 0,
71
- passthrough: (env.ACP_PASSTHROUGH ?? "0") === "1"
117
+ sessionHeader: env.ACP_SESSION_HEADER ?? fileConfig.sessionHeader ?? "x-acp-session",
118
+ log: env.ACP_LOG !== "0" && fileConfig.log !== false,
119
+ debug: (env.ACP_DEBUG ?? (fileConfig.debug ? "1" : "0")) === "1",
120
+ dumpSse: env.ACP_DUMP_SSE || fileConfig.dumpSse || void 0,
121
+ passthrough: (env.ACP_PASSTHROUGH ?? (fileConfig.passthrough ? "1" : "0")) === "1"
72
122
  };
73
123
  }
124
+ function loadConfigFile() {
125
+ const parsed = safeReadJson(configFile());
126
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
127
+ return parsed;
128
+ }
129
+ return {};
130
+ }
131
+ function parseRouteEntry(v) {
132
+ if (typeof v === "string" && v.length > 0) {
133
+ return { url: v.replace(/\/$/, "") };
134
+ }
135
+ if (v && typeof v === "object" && !Array.isArray(v) && typeof v.url === "string" && v.url.length > 0) {
136
+ const obj = v;
137
+ return { url: obj.url.replace(/\/$/, ""), models: obj.models };
138
+ }
139
+ return void 0;
140
+ }
74
141
 
75
142
  // src/server.ts
76
143
  import http from "http";
77
- import fs from "fs";
78
- import { createCore, estimateTokensFast as estimateTokensFast2, renderNudgeText } from "acp-kernel";
144
+ import fs2 from "fs";
145
+ import { createCore, estimateTokensFast as estimateTokensFast3, renderNudgeText, deactivateBlock as deactivateBlock4 } from "acp-kernel";
146
+
147
+ // src/fetch-util.ts
148
+ var MAX_REQUEST_BYTES = 100 * 1024 * 1024;
149
+ var UPSTREAM_TIMEOUT_MS = 10 * 60 * 1e3;
150
+ async function fetchWithTimeout(url, init, timeoutMs = UPSTREAM_TIMEOUT_MS) {
151
+ const controller = new AbortController();
152
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
153
+ try {
154
+ const response = await fetch(url, { ...init, signal: controller.signal });
155
+ return { response, clearTimer: () => clearTimeout(timer) };
156
+ } catch (e) {
157
+ clearTimeout(timer);
158
+ throw e;
159
+ }
160
+ }
161
+
162
+ // src/util.ts
163
+ import { createHash } from "crypto";
164
+ function hashId(s) {
165
+ return createHash("sha256").update(s, "utf8").digest("hex").slice(0, 16);
166
+ }
167
+ function safeJsonParse(s) {
168
+ try {
169
+ return s ? JSON.parse(s) : {};
170
+ } catch {
171
+ return {};
172
+ }
173
+ }
174
+
175
+ // src/message-id.ts
176
+ function deriveMessageId(role, contentType, text, options = {}) {
177
+ const seed = `${role}|${contentType}|${options.toolCallId ?? ""}|${options.toolName ?? ""}|${text}`;
178
+ return "h_" + hashId(seed);
179
+ }
180
+ var ClusterCounter = class {
181
+ counts = /* @__PURE__ */ new Map();
182
+ next(baseId) {
183
+ const n = this.counts.get(baseId) ?? 0;
184
+ this.counts.set(baseId, n + 1);
185
+ return n === 0 ? baseId : `${baseId}_${n}`;
186
+ }
187
+ };
79
188
 
80
189
  // src/anthropic.ts
190
+ var CONDENSED_TAG = "[acp-proxy: condensed";
81
191
  function extractSystem(system) {
82
192
  if (!system) return "";
83
193
  if (typeof system === "string") return system;
@@ -92,19 +202,23 @@ function buildSystem(text, original) {
92
202
  }
93
203
  function anthropicToCore(body) {
94
204
  const msgs = [];
95
- let idx = 0;
205
+ const clusters = new ClusterCounter();
96
206
  for (const m of body.messages) {
97
207
  const blocks = typeof m.content === "string" ? [{ type: "text", text: m.content }] : m.content;
98
208
  for (const b of blocks) {
99
- const id = `raw-${idx}`;
100
- idx++;
101
209
  switch (b.type) {
102
- case "text":
103
- msgs.push({ id, role: m.role, contentType: "text", text: b.text });
210
+ case "text": {
211
+ const base = deriveMessageId(m.role, "text", b.text);
212
+ msgs.push({ id: clusters.next(base), role: m.role, contentType: "text", text: b.text });
104
213
  break;
105
- case "tool_use":
214
+ }
215
+ case "tool_use": {
216
+ const base = deriveMessageId("assistant", "tool-call", safeStringify(b.input), {
217
+ toolCallId: b.id,
218
+ toolName: b.name
219
+ });
106
220
  msgs.push({
107
- id,
221
+ id: clusters.next(base),
108
222
  role: "assistant",
109
223
  contentType: "tool-call",
110
224
  toolName: b.name,
@@ -112,10 +226,12 @@ function anthropicToCore(body) {
112
226
  text: safeStringify(b.input)
113
227
  });
114
228
  break;
229
+ }
115
230
  case "tool_result": {
116
231
  const text = typeof b.content === "string" ? b.content : b.content.map((c) => c.text).join("\n");
232
+ const base = deriveMessageId("tool", "tool-result", text, { toolCallId: b.tool_use_id });
117
233
  msgs.push({
118
- id,
234
+ id: clusters.next(base),
119
235
  role: "tool",
120
236
  contentType: "tool-result",
121
237
  toolCallId: b.tool_use_id,
@@ -123,12 +239,16 @@ function anthropicToCore(body) {
123
239
  });
124
240
  break;
125
241
  }
126
- case "thinking":
127
- msgs.push({ id, role: "assistant", contentType: "reasoning", text: b.thinking });
242
+ case "thinking": {
243
+ const base = deriveMessageId("assistant", "reasoning", b.thinking);
244
+ msgs.push({ id: clusters.next(base), role: "assistant", contentType: "reasoning", text: b.thinking });
128
245
  break;
129
- case "image":
130
- msgs.push({ id, role: m.role, contentType: "text", text: "[image]" });
246
+ }
247
+ case "image": {
248
+ const base = deriveMessageId(m.role, "text", "[image]");
249
+ msgs.push({ id: clusters.next(base), role: m.role, contentType: "text", text: "[image]" });
131
250
  break;
251
+ }
132
252
  }
133
253
  }
134
254
  }
@@ -176,11 +296,38 @@ function coreToAnthropic(messages) {
176
296
  flush();
177
297
  return out;
178
298
  }
179
- function deriveSessionId(body, headerValue2) {
299
+ function condenseOldToolResults(messages, opts) {
300
+ if (!opts.enabled) return { messages, condensedCount: 0, charsSaved: 0 };
301
+ const toolResultIndices = [];
302
+ for (let i = 0; i < messages.length; i++) {
303
+ const m = messages[i];
304
+ if (m && m.contentType === "tool-result") toolResultIndices.push(i);
305
+ }
306
+ if (toolResultIndices.length <= opts.keepRecent) {
307
+ return { messages, condensedCount: 0, charsSaved: 0 };
308
+ }
309
+ const toCondense = new Set(toolResultIndices.slice(0, toolResultIndices.length - opts.keepRecent));
310
+ let condensedCount = 0;
311
+ let charsSaved = 0;
312
+ const out = messages.map((m, i) => {
313
+ if (!toCondense.has(i)) return m;
314
+ const text = m.text ?? "";
315
+ if (text.length < opts.minChars) return m;
316
+ const head = text.slice(0, opts.maxKeptChars);
317
+ const stub = `${CONDENSED_TAG} ${text.length.toLocaleString()} chars]
318
+ ${head}
319
+ [/acp-proxy]`;
320
+ charsSaved += text.length - stub.length;
321
+ condensedCount++;
322
+ return { ...m, text: stub };
323
+ });
324
+ return { messages: out, condensedCount, charsSaved };
325
+ }
326
+ function conversationSignalAnthropic(body, headerValue2) {
180
327
  if (headerValue2 && headerValue2.trim()) return headerValue2.trim();
181
328
  const firstUser = body.messages.find((m) => m.role === "user");
182
329
  const seed = firstUser ? JSON.stringify(firstUser.content).slice(0, 200) : "default";
183
- return hash(seed);
330
+ return hashId(seed);
184
331
  }
185
332
  function safeStringify(v) {
186
333
  try {
@@ -197,62 +344,59 @@ function safeParse(s) {
197
344
  return {};
198
345
  }
199
346
  }
200
- function hash(s) {
201
- let h = 2166136261;
202
- for (let i = 0; i < s.length; i++) {
203
- h ^= s.charCodeAt(i);
204
- h = Math.imul(h, 16777619);
205
- }
206
- return (h >>> 0).toString(36);
207
- }
208
347
 
209
348
  // src/openai.ts
210
349
  function openaiToCore(body) {
211
350
  const msgs = [];
212
- let idx = 0;
351
+ const clusters = new ClusterCounter();
213
352
  for (const m of body.messages) {
214
353
  switch (m.role) {
215
354
  case "system":
216
355
  case "developer": {
217
- msgs.push({ id: `raw-${idx}`, role: "system", contentType: "text", text: stringContent(m.content) });
218
- idx++;
356
+ const base = deriveMessageId(m.role, "text", stringContent(m.content));
357
+ msgs.push({ id: clusters.next(base), role: "system", contentType: "text", text: stringContent(m.content) });
219
358
  break;
220
359
  }
221
360
  case "user": {
222
- msgs.push({ id: `raw-${idx}`, role: "user", contentType: "text", text: stringContent(m.content) });
223
- idx++;
361
+ const base = deriveMessageId("user", "text", stringContent(m.content));
362
+ msgs.push({ id: clusters.next(base), role: "user", contentType: "text", text: stringContent(m.content) });
224
363
  break;
225
364
  }
226
365
  case "assistant": {
227
366
  const text = stringContent(m.content);
228
367
  if (text) {
229
- msgs.push({ id: `raw-${idx}`, role: "assistant", contentType: "text", text });
230
- idx++;
368
+ const base = deriveMessageId("assistant", "text", text);
369
+ msgs.push({ id: clusters.next(base), role: "assistant", contentType: "text", text });
231
370
  }
232
371
  if (Array.isArray(m.tool_calls)) {
233
372
  for (const tc of m.tool_calls) {
373
+ const base = deriveMessageId("assistant", "tool-call", tc.function.arguments ?? "", {
374
+ toolCallId: tc.id,
375
+ toolName: tc.function.name
376
+ });
234
377
  msgs.push({
235
- id: `raw-${idx}`,
378
+ id: clusters.next(base),
236
379
  role: "assistant",
237
380
  contentType: "tool-call",
238
381
  toolName: tc.function.name,
239
382
  toolCallId: tc.id,
240
383
  text: tc.function.arguments ?? ""
241
384
  });
242
- idx++;
243
385
  }
244
386
  }
245
387
  break;
246
388
  }
247
389
  case "tool": {
390
+ const base = deriveMessageId("tool", "tool-result", stringContent(m.content), {
391
+ toolCallId: m.tool_call_id ?? ""
392
+ });
248
393
  msgs.push({
249
- id: `raw-${idx}`,
394
+ id: clusters.next(base),
250
395
  role: "tool",
251
396
  contentType: "tool-result",
252
397
  toolCallId: m.tool_call_id ?? "",
253
398
  text: stringContent(m.content)
254
399
  });
255
- idx++;
256
400
  break;
257
401
  }
258
402
  }
@@ -316,11 +460,11 @@ ${extra}` : extra;
316
460
  }
317
461
  return [{ role: "system", content: extra }, ...messages];
318
462
  }
319
- function deriveSessionIdOpenai(body, headerValue2) {
463
+ function conversationSignalOpenai(body, headerValue2) {
320
464
  if (headerValue2 && headerValue2.trim()) return headerValue2.trim();
321
465
  const firstUser = body.messages.find((m) => m.role === "user");
322
466
  const seed = firstUser ? stringContent(firstUser.content).slice(0, 200) : "default";
323
- return hash2(seed);
467
+ return hashId(seed);
324
468
  }
325
469
  function stringContent(content) {
326
470
  if (content == null) return "";
@@ -330,56 +474,623 @@ function stringContent(content) {
330
474
  }
331
475
  return "";
332
476
  }
333
- function hash2(s) {
334
- let h = 2166136261;
335
- for (let i = 0; i < s.length; i++) {
336
- h ^= s.charCodeAt(i);
337
- h = Math.imul(h, 16777619);
477
+
478
+ // src/responses.ts
479
+ var OPAQUE_ITEM_TYPES = /* @__PURE__ */ new Set([
480
+ "additional_tools",
481
+ "reasoning",
482
+ "computer_call",
483
+ "computer_call_output",
484
+ "file_search_call",
485
+ "web_search_call",
486
+ "image_generation_call",
487
+ "code_interpreter_call",
488
+ "mcp_list_tools",
489
+ "mcp_call"
490
+ ]);
491
+ function isOpaqueItem(it) {
492
+ return OPAQUE_ITEM_TYPES.has(it.type);
493
+ }
494
+ function partText(p) {
495
+ if (p.type === "input_text" || p.type === "output_text") {
496
+ const t = p.text;
497
+ return typeof t === "string" ? t : "";
498
+ }
499
+ return "";
500
+ }
501
+ function messageContent(c) {
502
+ if (typeof c === "string") return c;
503
+ if (Array.isArray(c)) return c.map(partText).join("\n");
504
+ return "";
505
+ }
506
+ function responsesToCore(body) {
507
+ const msgs = [];
508
+ const systemParts = [];
509
+ const preamble = [];
510
+ const customToolCallIds = /* @__PURE__ */ new Set();
511
+ if (typeof body.instructions === "string" && body.instructions.trim()) {
512
+ systemParts.push(body.instructions);
513
+ }
514
+ let idx = 0;
515
+ const clusters = new ClusterCounter();
516
+ const items = Array.isArray(body.input) ? body.input : [];
517
+ if (typeof body.input === "string") {
518
+ const base = deriveMessageId("user", "text", body.input);
519
+ msgs.push({ id: clusters.next(base), role: "user", contentType: "text", text: body.input });
520
+ idx++;
521
+ return { msgs, systemParts, preamble, customToolCallIds };
522
+ }
523
+ for (const it of items) {
524
+ if (isOpaqueItem(it)) {
525
+ preamble.push(it);
526
+ continue;
527
+ }
528
+ switch (it.type) {
529
+ case "message": {
530
+ const m = it;
531
+ const text = messageContent(m.content);
532
+ if (m.role === "system" || m.role === "developer") {
533
+ systemParts.push(text);
534
+ } else if (m.role === "user") {
535
+ const base = deriveMessageId("user", "text", text);
536
+ msgs.push({ id: clusters.next(base), role: "user", contentType: "text", text });
537
+ idx++;
538
+ } else if (m.role === "assistant") {
539
+ if (text) {
540
+ const base = deriveMessageId("assistant", "text", text);
541
+ msgs.push({ id: clusters.next(base), role: "assistant", contentType: "text", text });
542
+ idx++;
543
+ }
544
+ }
545
+ break;
546
+ }
547
+ case "function_call": {
548
+ const fc = it;
549
+ const base = deriveMessageId("assistant", "tool-call", fc.arguments ?? "", {
550
+ toolCallId: fc.call_id,
551
+ toolName: fc.name
552
+ });
553
+ msgs.push({
554
+ id: clusters.next(base),
555
+ role: "assistant",
556
+ contentType: "tool-call",
557
+ toolName: fc.name,
558
+ toolCallId: fc.call_id,
559
+ text: fc.arguments ?? ""
560
+ });
561
+ idx++;
562
+ break;
563
+ }
564
+ case "function_call_output": {
565
+ const fco = it;
566
+ const outText = typeof fco.output === "string" ? fco.output : JSON.stringify(fco.output);
567
+ const base = deriveMessageId("tool", "tool-result", outText, { toolCallId: fco.call_id });
568
+ msgs.push({
569
+ id: clusters.next(base),
570
+ role: "tool",
571
+ contentType: "tool-result",
572
+ toolCallId: fco.call_id,
573
+ text: outText
574
+ });
575
+ idx++;
576
+ break;
577
+ }
578
+ case "custom_tool_call": {
579
+ const ctc = it;
580
+ const callId = ctc.call_id ?? `call_${idx}`;
581
+ customToolCallIds.add(callId);
582
+ const argText = ctc.input ?? ctc.arguments ?? "";
583
+ const base = deriveMessageId("assistant", "tool-call", argText, {
584
+ toolCallId: callId,
585
+ toolName: ctc.name ?? "custom"
586
+ });
587
+ msgs.push({
588
+ id: clusters.next(base),
589
+ role: "assistant",
590
+ contentType: "tool-call",
591
+ toolName: ctc.name ?? "custom",
592
+ toolCallId: callId,
593
+ text: argText
594
+ });
595
+ idx++;
596
+ break;
597
+ }
598
+ case "custom_tool_call_output": {
599
+ const ctco = it;
600
+ const callId = ctco.call_id ?? `call_${idx}`;
601
+ customToolCallIds.add(callId);
602
+ const outText = typeof ctco.output === "string" ? ctco.output : JSON.stringify(ctco.output ?? "");
603
+ const base = deriveMessageId("tool", "tool-result", outText, { toolCallId: callId });
604
+ msgs.push({
605
+ id: clusters.next(base),
606
+ role: "tool",
607
+ contentType: "tool-result",
608
+ toolCallId: callId,
609
+ text: outText
610
+ });
611
+ idx++;
612
+ break;
613
+ }
614
+ default:
615
+ preamble.push(it);
616
+ break;
617
+ }
618
+ }
619
+ return { msgs, systemParts, preamble, customToolCallIds };
620
+ }
621
+ function coreToResponses(messages, customToolCallIds = /* @__PURE__ */ new Set()) {
622
+ const out = [];
623
+ for (const m of messages) {
624
+ if (m.role === "system") {
625
+ out.push({ type: "message", role: "developer", content: m.text ?? "" });
626
+ } else if (m.role === "user") {
627
+ out.push({ type: "message", role: "user", content: m.text ?? "" });
628
+ } else if (m.role === "assistant") {
629
+ if (m.contentType === "text") {
630
+ out.push({ type: "message", role: "assistant", content: m.text ?? "" });
631
+ } else if (m.contentType === "tool-call") {
632
+ const callId = m.toolCallId ?? `call_${m.id}`;
633
+ if (customToolCallIds.has(callId)) {
634
+ out.push({
635
+ type: "custom_tool_call",
636
+ call_id: callId,
637
+ name: m.toolName ?? "unknown",
638
+ input: m.text ?? "",
639
+ status: "completed"
640
+ });
641
+ } else {
642
+ out.push({
643
+ type: "function_call",
644
+ call_id: callId,
645
+ name: m.toolName ?? "unknown",
646
+ arguments: m.text ?? ""
647
+ });
648
+ }
649
+ }
650
+ } else if (m.role === "tool") {
651
+ const callId = m.toolCallId ?? "";
652
+ if (customToolCallIds.has(callId)) {
653
+ out.push({
654
+ type: "custom_tool_call_output",
655
+ call_id: callId,
656
+ output: m.text ?? ""
657
+ });
658
+ } else {
659
+ out.push({
660
+ type: "function_call_output",
661
+ call_id: callId,
662
+ output: m.text ?? ""
663
+ });
664
+ }
665
+ }
666
+ }
667
+ return out;
668
+ }
669
+ function conversationSignalResponses(body, headerValue2) {
670
+ if (headerValue2 && headerValue2.trim()) return headerValue2.trim();
671
+ if (typeof body.session_id === "string" && body.session_id.length > 0) {
672
+ return `codex-${body.session_id}`;
338
673
  }
339
- return (h >>> 0).toString(36);
674
+ if (typeof body.previous_response_id === "string" && body.previous_response_id.length > 0) {
675
+ return `resp-${body.previous_response_id}`;
676
+ }
677
+ let seed = "default";
678
+ if (Array.isArray(body.input)) {
679
+ const firstUser = body.input.find(
680
+ (i) => i.type === "message" && i.role === "user"
681
+ );
682
+ if (firstUser) seed = messageContent(firstUser.content).slice(0, 200);
683
+ } else if (typeof body.input === "string") {
684
+ seed = body.input.slice(0, 200);
685
+ }
686
+ return hashId(seed);
340
687
  }
341
688
 
342
689
  // src/session.ts
690
+ import { createInitialState as createInitialState2 } from "acp-kernel";
691
+
692
+ // src/persist.ts
693
+ import { promises as fs } from "fs";
694
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
695
+ import { createHash as createHash2 } from "crypto";
696
+ import * as path2 from "path";
343
697
  import { createInitialState } from "acp-kernel";
698
+ var PERSIST_VERSION = 1;
699
+ function mergeState(parsed) {
700
+ const fresh = createInitialState();
701
+ return {
702
+ blocks: parsed.blocks ?? fresh.blocks,
703
+ messageRefs: parsed.messageRefs ?? fresh.messageRefs,
704
+ nudge: { ...fresh.nudge, ...parsed.nudge ?? {} },
705
+ stats: { ...fresh.stats, ...parsed.stats ?? {} },
706
+ nextBlockId: parsed.nextBlockId ?? fresh.nextBlockId,
707
+ nextRunId: parsed.nextRunId ?? fresh.nextRunId
708
+ };
709
+ }
710
+ function hostLabel(upstreamOrigin) {
711
+ if (!upstreamOrigin) return "unknown";
712
+ try {
713
+ const host = new URL(upstreamOrigin).hostname || "unknown";
714
+ return host.replace(/[^a-zA-Z0-9.-]/g, "-").slice(0, 48) || "unknown";
715
+ } catch {
716
+ return "unknown-" + createHash2("sha256").update(upstreamOrigin, "utf8").digest("hex").slice(0, 6);
717
+ }
718
+ }
719
+ function relPathFor(id, protocol, upstreamOrigin) {
720
+ const proto = protocol ?? "_unknown";
721
+ const host = protocol ? hostLabel(upstreamOrigin) + "_" : "";
722
+ return path2.join(proto, `${host}${createHash2("sha256").update(id, "utf8").digest("hex").slice(0, 24)}.json`);
723
+ }
724
+ function legacyFileNameFor(id) {
725
+ return createHash2("sha256").update(id, "utf8").digest("hex").slice(0, 24) + ".json";
726
+ }
727
+ var SessionStore = class {
728
+ dir;
729
+ debounceMs;
730
+ enabled;
731
+ timers = /* @__PURE__ */ new Map();
732
+ /** Monotonic counter for unique temp filenames within a process. */
733
+ tmpSeq = 0;
734
+ log;
735
+ constructor(opts) {
736
+ this.dir = opts?.dir ?? defaultDir();
737
+ this.debounceMs = opts?.debounceMs ?? defaultDebounce();
738
+ this.enabled = (opts?.enabled ?? true) && this.debounceMs >= 0;
739
+ this.log = opts?.log ?? defaultLogger;
740
+ }
741
+ filePath(id, protocol, upstreamOrigin) {
742
+ return path2.join(this.dir, relPathFor(id, protocol, upstreamOrigin));
743
+ }
744
+ /** A unique temp path per write (per process). Two overlapping writes for
745
+ * the same session must not share a temp file, or one rename invalidates
746
+ * the other. */
747
+ tempPath(id) {
748
+ return path2.join(this.dir, `.tmp-${legacyFileNameFor(id)}-${process.pid}-${this.tmpSeq++}`);
749
+ }
750
+ /** Bulk-load every persisted session from disk into a map keyed by the
751
+ * REAL session id (read from the file body, not the filename). Called once
752
+ * at startup before the server accepts traffic. Corrupt individual files
753
+ * are skipped (logged) — one bad file never blocks boot. */
754
+ async loadAll() {
755
+ const out = /* @__PURE__ */ new Map();
756
+ if (!this.enabled) return out;
757
+ try {
758
+ await fs.mkdir(this.dir, { recursive: true });
759
+ } catch {
760
+ return out;
761
+ }
762
+ const files = [];
763
+ const walk = async (dir) => {
764
+ let entries;
765
+ try {
766
+ entries = await fs.readdir(dir, { withFileTypes: true });
767
+ } catch {
768
+ return;
769
+ }
770
+ for (const e of entries) {
771
+ if (e.name.startsWith(".tmp-")) continue;
772
+ const full = path2.join(dir, e.name);
773
+ if (e.isDirectory()) {
774
+ await walk(full);
775
+ } else if (e.isFile() && e.name.endsWith(".json")) {
776
+ files.push(full);
777
+ }
778
+ }
779
+ };
780
+ await walk(this.dir);
781
+ for (const full of files) {
782
+ const name = path2.basename(full);
783
+ try {
784
+ const parsed = JSON.parse(await fs.readFile(full, "utf8"));
785
+ if (!isValidRecord(parsed)) continue;
786
+ const proto = parsed.protocol;
787
+ const origin = parsed.upstreamOrigin;
788
+ const expectedNamespaced = path2.basename(relPathFor(parsed.id, proto, origin));
789
+ const expectedLegacy = legacyFileNameFor(parsed.id);
790
+ if (name !== expectedNamespaced && name !== expectedLegacy) {
791
+ this.log("warn", `[persist] skipping ${full}: filename does not match body id (expected ${expectedNamespaced})`);
792
+ continue;
793
+ }
794
+ out.set(parsed.id, buildSession(parsed));
795
+ } catch (e) {
796
+ this.log("warn", `[persist] skipping corrupt session file ${full}: ${msg(e)}`);
797
+ }
798
+ }
799
+ return out;
800
+ }
801
+ /** Synchronous reload of a single session. Used on a memory miss (after
802
+ * LRU eviction). Sync fs is acceptable here because a miss is rare and
803
+ * reads a single small file (~1ms). Returns null if missing/corrupt or the
804
+ * body id does not match what we asked for. */
805
+ loadSync(id, meta) {
806
+ if (!this.enabled) return null;
807
+ const candidates = [this.filePath(id, meta?.protocol, meta?.upstreamOrigin)];
808
+ if (meta?.protocol) candidates.push(this.filePath(id));
809
+ for (const file of candidates) {
810
+ if (!existsSync(file)) continue;
811
+ try {
812
+ const parsed = JSON.parse(readFileSync2(file, "utf8"));
813
+ if (!isValidRecord(parsed) || parsed.id !== id) continue;
814
+ return buildSession(parsed);
815
+ } catch (e) {
816
+ this.log("warn", `[persist] failed to load session ${id}: ${msg(e)}`);
817
+ }
818
+ }
819
+ return null;
820
+ }
821
+ /** Schedule a debounced write for a session. Multiple calls within the
822
+ * window coalesce. Safe to call on the hot path. No-op if disabled. */
823
+ scheduleSave(session) {
824
+ if (!this.enabled) return;
825
+ const existing = this.timers.get(session.id);
826
+ if (existing) clearTimeout(existing);
827
+ const timer = setTimeout(() => {
828
+ this.timers.delete(session.id);
829
+ void this.writeNow(session).catch((e) => {
830
+ this.log("error", `[persist] debounced write failed for ${session.id}: ${msg(e)}`);
831
+ });
832
+ }, this.debounceMs);
833
+ timer.unref?.();
834
+ this.timers.set(session.id, timer);
835
+ }
836
+ /** Asynchronously persist a session right now (skips the debounce). Throws
837
+ * on write failure so callers can react (e.g. avoid evicting). */
838
+ async writeNow(session) {
839
+ if (!this.enabled) return;
840
+ const record = buildRecord(session);
841
+ const file = this.filePath(session.id, session.protocol, session.upstreamOrigin);
842
+ try {
843
+ await fs.mkdir(path2.dirname(file), { recursive: true });
844
+ } catch (e) {
845
+ this.log("warn", `[persist] could not create session dir ${this.dir}: ${msg(e)}`);
846
+ }
847
+ const tmp = this.tempPath(session.id);
848
+ const data = JSON.stringify(record);
849
+ await fs.writeFile(tmp, data, "utf8");
850
+ await fs.rename(tmp, file);
851
+ }
852
+ /** Synchronous flush for a single session. Used on memory eviction so a
853
+ * dirty evicted session is not lost. Sync because eviction runs in the
854
+ * sync getSession path; a single small write is acceptable.
855
+ * Returns true on success, false on failure (caller must NOT evict on
856
+ * failure for a never-persisted session or it is lost permanently). */
857
+ flushSync(session) {
858
+ if (!this.enabled) return true;
859
+ const existing = this.timers.get(session.id);
860
+ if (existing) {
861
+ clearTimeout(existing);
862
+ this.timers.delete(session.id);
863
+ }
864
+ const record = buildRecord(session);
865
+ const file = this.filePath(session.id, session.protocol, session.upstreamOrigin);
866
+ try {
867
+ mkdirSync(path2.dirname(file), { recursive: true });
868
+ } catch (e) {
869
+ this.log("warn", `[persist] could not create session dir ${this.dir}: ${msg(e)}`);
870
+ }
871
+ const tmp = this.tempPath(session.id);
872
+ try {
873
+ writeFileSync(tmp, JSON.stringify(record), "utf8");
874
+ renameSync(tmp, file);
875
+ return true;
876
+ } catch (e) {
877
+ this.log("error", `[persist] flushSync FAILED for ${session.id}: ${msg(e)} \u2014 session NOT evicted to prevent loss`);
878
+ try {
879
+ unlinkSync(tmp);
880
+ } catch {
881
+ }
882
+ return false;
883
+ }
884
+ }
885
+ /** Flush all dirty sessions with a pending debounce timer. Called on
886
+ * SIGTERM/SIGINT for graceful shutdown. Clears timers first, then writes
887
+ * every session that had a pending write. */
888
+ async flushAll(sessions2) {
889
+ if (!this.enabled) return;
890
+ const dirty = new Set(this.timers.keys());
891
+ for (const timer of this.timers.values()) clearTimeout(timer);
892
+ this.timers.clear();
893
+ const pending = [];
894
+ for (const s of sessions2) {
895
+ if (!dirty.has(s.id)) continue;
896
+ pending.push(
897
+ this.writeNow(s).catch((e) => {
898
+ this.log("error", `[persist] shutdown flush failed for ${s.id}: ${msg(e)}`);
899
+ })
900
+ );
901
+ }
902
+ await Promise.all(pending);
903
+ }
904
+ /** Whether a write is currently pending (debounce timer armed) for a id. */
905
+ hasPending(id) {
906
+ return this.timers.has(id);
907
+ }
908
+ /** Cancel all pending writes without flushing (e.g. for tests). */
909
+ cancelAll() {
910
+ for (const timer of this.timers.values()) clearTimeout(timer);
911
+ this.timers.clear();
912
+ }
913
+ };
914
+ function buildRecord(session) {
915
+ return {
916
+ version: PERSIST_VERSION,
917
+ savedAt: Date.now(),
918
+ id: session.id,
919
+ protocol: session.protocol,
920
+ upstreamOrigin: session.upstreamOrigin,
921
+ createdAt: session.createdAt,
922
+ requests: session.requests,
923
+ condensedToolResults: session.condensedToolResults,
924
+ tokensSaved: session.tokensSaved,
925
+ state: session.state,
926
+ blockContents: Object.fromEntries(session.blockContents)
927
+ };
928
+ }
929
+ function buildSession(parsed) {
930
+ const blockContents = /* @__PURE__ */ new Map();
931
+ for (const [bid, content] of Object.entries(parsed.blockContents ?? {})) {
932
+ if (content && typeof content === "object") blockContents.set(bid, content);
933
+ }
934
+ return {
935
+ id: parsed.id,
936
+ protocol: parsed.protocol,
937
+ upstreamOrigin: parsed.upstreamOrigin,
938
+ state: mergeState(parsed.state),
939
+ createdAt: parsed.createdAt ?? Date.now(),
940
+ lastSeen: Date.now(),
941
+ requests: parsed.requests ?? 0,
942
+ condensedToolResults: parsed.condensedToolResults ?? 0,
943
+ tokensSaved: parsed.tokensSaved ?? 0,
944
+ blockContents,
945
+ inFlight: 0,
946
+ persisted: true
947
+ };
948
+ }
949
+ function isValidRecord(parsed) {
950
+ if (!parsed || typeof parsed !== "object") return false;
951
+ const r = parsed;
952
+ return typeof r.id === "string" && typeof r.state === "object" && r.state !== null && Array.isArray(r.state.blocks);
953
+ }
954
+ function msg(e) {
955
+ return e instanceof Error ? e.message : String(e);
956
+ }
957
+ function defaultDir() {
958
+ return sessionsDir();
959
+ }
960
+ function defaultDebounce() {
961
+ const env = process.env.BILI_PERSIST_DEBOUNCE_MS;
962
+ if (env) {
963
+ const n = Number.parseInt(env, 10);
964
+ if (Number.isFinite(n) && n >= 0) return n;
965
+ }
966
+ return 500;
967
+ }
968
+ function persistEnabled() {
969
+ const env = process.env.BILI_PERSIST;
970
+ if (env === "0" || env === "false") return false;
971
+ return true;
972
+ }
973
+ function defaultLogger(level, m) {
974
+ console.error(`[${level}] ${m}`);
975
+ }
976
+ var _store = null;
977
+ function getStore() {
978
+ if (!_store) {
979
+ _store = new SessionStore({ enabled: persistEnabled() });
980
+ }
981
+ return _store;
982
+ }
983
+
984
+ // src/session.ts
344
985
  var sessions = /* @__PURE__ */ new Map();
345
- var MAX_SESSIONS = 256;
346
- function getSession(id) {
986
+ var MAX_SESSIONS = Number.parseInt(process.env.BILI_MAX_SESSIONS ?? "256", 10) || 256;
987
+ var initialized = false;
988
+ async function initSessions() {
989
+ if (initialized) return;
990
+ initialized = true;
991
+ const store = getStore();
992
+ if (!store.enabled) return;
993
+ const loaded = await store.loadAll();
994
+ if (loaded.size > MAX_SESSIONS) {
995
+ const entries = [...loaded.entries()].sort((a, b) => (b[1].createdAt ?? 0) - (a[1].createdAt ?? 0));
996
+ for (const [id, s] of entries) {
997
+ if (sessions.size >= MAX_SESSIONS) break;
998
+ sessions.set(id, s);
999
+ }
1000
+ } else {
1001
+ for (const [id, s] of loaded) sessions.set(id, s);
1002
+ }
1003
+ }
1004
+ function getSession(id, meta) {
347
1005
  const existing = sessions.get(id);
348
1006
  if (existing) {
349
1007
  existing.lastSeen = Date.now();
1008
+ if (meta?.protocol && !existing.protocol) existing.protocol = meta.protocol;
1009
+ if (meta?.upstreamOrigin && !existing.upstreamOrigin) existing.upstreamOrigin = meta.upstreamOrigin;
350
1010
  return existing;
351
1011
  }
1012
+ const store = getStore();
1013
+ const reloaded = store.loadSync(id, meta);
1014
+ if (reloaded) {
1015
+ reloaded.lastSeen = Date.now();
1016
+ reloaded.persisted = true;
1017
+ sessions.set(id, reloaded);
1018
+ return reloaded;
1019
+ }
352
1020
  if (sessions.size >= MAX_SESSIONS) evictOldest();
353
1021
  const session = {
354
1022
  id,
355
- state: createInitialState(),
1023
+ protocol: meta?.protocol,
1024
+ upstreamOrigin: meta?.upstreamOrigin,
1025
+ state: createInitialState2(),
356
1026
  createdAt: Date.now(),
357
1027
  lastSeen: Date.now(),
358
1028
  requests: 0,
359
1029
  condensedToolResults: 0,
360
- tokensSaved: 0
1030
+ tokensSaved: 0,
1031
+ blockContents: /* @__PURE__ */ new Map(),
1032
+ inFlight: 0,
1033
+ persisted: false
361
1034
  };
362
1035
  sessions.set(id, session);
363
1036
  return session;
364
1037
  }
1038
+ function acquireInFlight(session) {
1039
+ session.inFlight++;
1040
+ }
1041
+ function releaseInFlight(session) {
1042
+ if (session.inFlight > 0) session.inFlight--;
1043
+ }
1044
+ async function withSessionLock(session, fn) {
1045
+ const prev = session.lockChain ?? Promise.resolve();
1046
+ let release;
1047
+ const done = new Promise((resolve) => {
1048
+ release = resolve;
1049
+ });
1050
+ session.lockChain = prev.then(() => done);
1051
+ await prev;
1052
+ try {
1053
+ return await fn();
1054
+ } finally {
1055
+ release();
1056
+ }
1057
+ }
365
1058
  function listSessions() {
366
1059
  return [...sessions.values()].sort((a, b) => b.lastSeen - a.lastSeen);
367
1060
  }
1061
+ function markDirty(session) {
1062
+ getStore().scheduleSave(session);
1063
+ }
1064
+ function cacheBlockContent(session, blockId, content) {
1065
+ session.blockContents.set(blockId, content);
1066
+ }
368
1067
  function evictOldest() {
369
1068
  let oldestId;
370
1069
  let oldestSeen = Infinity;
371
- for (const [id, s] of sessions) {
372
- if (s.lastSeen < oldestSeen) {
373
- oldestSeen = s.lastSeen;
1070
+ for (const [id, s2] of sessions) {
1071
+ if (s2.inFlight > 0) continue;
1072
+ if (s2.lastSeen < oldestSeen) {
1073
+ oldestSeen = s2.lastSeen;
374
1074
  oldestId = id;
375
1075
  }
376
1076
  }
377
- if (oldestId) sessions.delete(oldestId);
1077
+ if (!oldestId) return;
1078
+ const s = sessions.get(oldestId);
1079
+ const ok = getStore().flushSync(s);
1080
+ if (!ok && !s.persisted) {
1081
+ return;
1082
+ }
1083
+ sessions.delete(oldestId);
1084
+ }
1085
+ async function flushAllSessions() {
1086
+ await getStore().flushAll(sessions.values());
378
1087
  }
379
1088
 
380
1089
  // src/compress-tool.ts
381
1090
  import { COMPRESS_PHILOSOPHY, HOW_TO_COMPRESS_RULES } from "acp-kernel";
382
1091
  var COMPRESS_TOOL_NAME = "compress";
1092
+ var ACP_TEXT_OPEN = "<acp_compress>";
1093
+ var ACP_TEXT_CLOSE = "</acp_compress>";
383
1094
  var COMPRESS_TOOL = {
384
1095
  name: COMPRESS_TOOL_NAME,
385
1096
  description: "Replace a contiguous range of older conversation with a detailed summary you write. Use when content is genuinely consumed. Batch form: content=[{startId,endId,summary,topic?}].",
@@ -405,12 +1116,18 @@ var COMPRESS_TOOL = {
405
1116
  }
406
1117
  };
407
1118
  function parseCompressInput(input) {
408
- if (!input || typeof input !== "object") return [];
1119
+ if (!input || typeof input !== "object") {
1120
+ console.error(`[acp-compress-input] rejected: not object (${typeof input})`);
1121
+ return [];
1122
+ }
409
1123
  const obj = input;
410
1124
  if (Array.isArray(obj.content)) {
411
- return obj.content.map((r) => toRange(r)).filter((r) => r !== null);
1125
+ const out = obj.content.map((r) => toRange(r)).filter((r) => r !== null);
1126
+ if (out.length === 0) console.error(`[acp-compress-input] content array but 0 valid ranges. keys per item: ${obj.content.map((c) => Object.keys(c ?? {}).join(",")).join(" | ")}`);
1127
+ return out;
412
1128
  }
413
1129
  const single = toRange(obj);
1130
+ if (!single) console.error(`[acp-compress-input] no content array, single-parse failed. top keys: ${Object.keys(obj).join(",")}`);
414
1131
  return single ? [single] : [];
415
1132
  }
416
1133
  function toRange(r) {
@@ -463,7 +1180,7 @@ ${HOW_TO_COMPRESS_RULES}
463
1180
 
464
1181
  ACP TAGS
465
1182
 
466
- Each message in the conversation is annotated with a <acp tokens="2.1K" type="tool:bash">m00175</acp> tag showing its reference ID, approximate token size, and content type. Use these annotations to assess which messages are consuming the most context and prioritize compression accordingly. The token size is approximate \u2014 treat it as a relative guide, not an exact count.
1183
+ Each message in the conversation is annotated with a <acp tokens="2.1K" type="tool:bash">m00175</acp> tag showing its reference ID, approximate token size, and content type. These tags are system metadata injected by the proxy. NEVER echo, repeat, or reference these XML tags in your responses \u2014 the tags must not appear in your output. Use only the ref ID (e.g. m00005) inside compress calls, never the XML wrapper. The token size is approximate \u2014 treat it as a relative guide, not an exact count.
467
1184
 
468
1185
  TOOLS
469
1186
 
@@ -533,6 +1250,36 @@ var ACP_TOOLS_OPENAI = [
533
1250
  SEARCH_CONTEXT_TOOL_OPENAI,
534
1251
  ACP_STATUS_TOOL_OPENAI
535
1252
  ];
1253
+ var COMPRESS_TOOL_RESPONSES = {
1254
+ type: "function",
1255
+ name: COMPRESS_TOOL_NAME,
1256
+ description: COMPRESS_TOOL.description,
1257
+ parameters: COMPRESS_TOOL_OPENAI.function.parameters
1258
+ };
1259
+ var DECOMPRESS_TOOL_RESPONSES = {
1260
+ type: "function",
1261
+ name: DECOMPRESS_TOOL_OPENAI.function.name,
1262
+ description: DECOMPRESS_TOOL_OPENAI.function.description,
1263
+ parameters: DECOMPRESS_TOOL_OPENAI.function.parameters
1264
+ };
1265
+ var SEARCH_CONTEXT_TOOL_RESPONSES = {
1266
+ type: "function",
1267
+ name: SEARCH_CONTEXT_TOOL_OPENAI.function.name,
1268
+ description: SEARCH_CONTEXT_TOOL_OPENAI.function.description,
1269
+ parameters: SEARCH_CONTEXT_TOOL_OPENAI.function.parameters
1270
+ };
1271
+ var ACP_STATUS_TOOL_RESPONSES = {
1272
+ type: "function",
1273
+ name: ACP_STATUS_TOOL_OPENAI.function.name,
1274
+ description: ACP_STATUS_TOOL_OPENAI.function.description,
1275
+ parameters: ACP_STATUS_TOOL_OPENAI.function.parameters
1276
+ };
1277
+ var ACP_TOOLS_RESPONSES = [
1278
+ COMPRESS_TOOL_RESPONSES,
1279
+ DECOMPRESS_TOOL_RESPONSES,
1280
+ SEARCH_CONTEXT_TOOL_RESPONSES,
1281
+ ACP_STATUS_TOOL_RESPONSES
1282
+ ];
536
1283
  var PROXY_TOOL_NAMES = /* @__PURE__ */ new Set([
537
1284
  COMPRESS_TOOL_NAME,
538
1285
  DECOMPRESS_TOOL_NAME,
@@ -540,6 +1287,15 @@ var PROXY_TOOL_NAMES = /* @__PURE__ */ new Set([
540
1287
  ACP_STATUS_TOOL_NAME
541
1288
  ]);
542
1289
 
1290
+ // src/stream.ts
1291
+ import { collectBlockContent } from "acp-kernel";
1292
+
1293
+ // src/sse-util.ts
1294
+ function normalizeSseLineEndings(buf) {
1295
+ if (buf.indexOf("\r") === -1) return buf;
1296
+ return buf.replace(/\r\n|\r/g, "\n");
1297
+ }
1298
+
543
1299
  // src/stream.ts
544
1300
  var NOOP = /* @__PURE__ */ Symbol("noop");
545
1301
  async function* rewriteSseStream(upstream, ctx) {
@@ -560,6 +1316,7 @@ async function* rewriteSseStream(upstream, ctx) {
560
1316
  const { done, value } = await reader.read();
561
1317
  if (done) break;
562
1318
  buf += decoder.decode(value, { stream: true });
1319
+ buf = normalizeSseLineEndings(buf);
563
1320
  let idx;
564
1321
  while ((idx = buf.indexOf("\n\n")) !== -1) {
565
1322
  const rawEvent = buf.slice(0, idx);
@@ -608,6 +1365,10 @@ function routeEvent(ev, blocks, ctx, markConverted, markRealToolUse, getConverte
608
1365
  if (typeof partial === "string") st.json += partial;
609
1366
  return NOOP;
610
1367
  }
1368
+ const dt = d.delta;
1369
+ if (dt?.text && (dt.text.includes("<acp ") || dt.text.includes("</acp"))) {
1370
+ ctx.log(`[warn: tag echo] model emitted <acp tag in text delta: ${dt.text.slice(0, 120).replace(/\n/g, " ")}`);
1371
+ }
611
1372
  return emitEvent(ev);
612
1373
  }
613
1374
  if (t === "content_block_stop") {
@@ -665,7 +1426,19 @@ function applyRanges(ranges, ctx) {
665
1426
  state: ctx.session.state,
666
1427
  config: ctx.config
667
1428
  });
1429
+ const beforeIds = new Set(ctx.session.state.blocks.map((b) => b.blockId));
668
1430
  ctx.session.state = res.state;
1431
+ for (const b of res.state.blocks) {
1432
+ if (beforeIds.has(b.blockId)) continue;
1433
+ const full = collectBlockContent(res.state, b, ctx.messages, { full: true });
1434
+ const one = collectBlockContent(res.state, b, ctx.messages, { full: false });
1435
+ if (full.count > 0 || one.count > 0) {
1436
+ cacheBlockContent(ctx.session, b.blockId, {
1437
+ one: { text: one.text, count: one.count },
1438
+ full: { text: full.text, count: full.count }
1439
+ });
1440
+ }
1441
+ }
669
1442
  const r = res.result;
670
1443
  const detail = ranges.map((rg) => `${rg.startRef}\u2013${rg.endRef}`).join(", ");
671
1444
  if (r.blocksCreated === 0) {
@@ -674,9 +1447,9 @@ function applyRanges(ranges, ctx) {
674
1447
  return `[Compression FAILED: ${errs} Do not retry the same range.]`;
675
1448
  }
676
1449
  const warn = r.warnings.length > 0 ? ` ${r.warnings.join("; ")}` : "";
677
- const msg = `[Compressed ${detail} \u2192 ${r.blocksCreated} block(s), ~${r.tokensCompressed} tokens saved.${warn}]`;
678
- ctx.log(`[acp-proxy: ${msg}]`);
679
- return msg;
1450
+ const msg2 = `[Compressed ${detail} \u2192 ${r.blocksCreated} block(s), ~${r.tokensCompressed} tokens saved.${warn}]`;
1451
+ ctx.log(`[acp-proxy: ${msg2}]`);
1452
+ return msg2;
680
1453
  } catch (err) {
681
1454
  ctx.log(`[acp-proxy: compress failed: ${String(err)}]`);
682
1455
  return `[Compression FAILED: ${String(err)} Do not retry the same range.]`;
@@ -730,56 +1503,110 @@ function rewriteJsonResponse(body, ctx) {
730
1503
  }
731
1504
  b.content = newContent;
732
1505
  if (converted && !sawRealToolUse) b.stop_reason = "end_turn";
1506
+ for (const blk of newContent) {
1507
+ const t = blk.text;
1508
+ if (typeof t === "string" && (t.includes("<acp ") || t.includes("</acp"))) {
1509
+ ctx.log(`[warn: tag echo] non-stream model output contains <acp tag: ${t.slice(0, 120).replace(/\n/g, " ")}`);
1510
+ }
1511
+ }
733
1512
  return body;
734
1513
  }
735
1514
 
1515
+ // src/orphan-gc.ts
1516
+ var ORPHAN_THRESHOLD = 3;
1517
+ var orphanStreaks = /* @__PURE__ */ new WeakMap();
1518
+ function reapOrphanBlocks(session, visible, deactivate) {
1519
+ if (session.state.blocks.length === 0) return { reaped: [] };
1520
+ const presentIds = new Set(visible.map((m) => m.id));
1521
+ const reaped = [];
1522
+ for (const block of session.state.blocks) {
1523
+ if (!block.active) continue;
1524
+ const hasHit = block.effectiveMessageIds.some((id) => presentIds.has(id));
1525
+ if (hasHit) {
1526
+ orphanStreaks.delete(block);
1527
+ continue;
1528
+ }
1529
+ const streak = (orphanStreaks.get(block) ?? 0) + 1;
1530
+ orphanStreaks.set(block, streak);
1531
+ if (streak >= ORPHAN_THRESHOLD) {
1532
+ reaped.push(block.blockId);
1533
+ }
1534
+ }
1535
+ if (reaped.length === 0) return { reaped: [] };
1536
+ session.state = deactivate(session.state, reaped);
1537
+ for (const id of reaped) session.blockContents.delete(id);
1538
+ return { reaped };
1539
+ }
1540
+
736
1541
  // src/compress-loop.ts
737
1542
  import {
738
1543
  buildStatusReport,
739
- collectBlockContent,
740
- deactivateBlock,
741
1544
  estimateTokensFast
742
1545
  } from "acp-kernel";
743
- import { writeFileSync, mkdirSync } from "fs";
744
- import { dirname, join } from "path";
1546
+
1547
+ // src/decompress-shared.ts
1548
+ import {
1549
+ collectBlockContent as collectBlockContent2,
1550
+ deactivateBlock
1551
+ } from "acp-kernel";
1552
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1553
+ import { dirname as dirname2, join as join2 } from "path";
745
1554
  import { tmpdir } from "os";
746
- function executeProxyTool(toolName, args, ctx) {
747
- if (toolName === "compress") {
748
- return applyRanges(parseCompressInput(args), ctx);
1555
+ function resolveDecompress(args, ctx) {
1556
+ const rawBlockId = args.blockId;
1557
+ if (typeof rawBlockId !== "string" || rawBlockId.length === 0) {
1558
+ return "[decompress FAILED: blockId is required]";
1559
+ }
1560
+ const blockId = rawBlockId.trim();
1561
+ const block = ctx.core.decompress(blockId, ctx.session.state);
1562
+ if (!block) return `[Block ${blockId} not found]`;
1563
+ const full = args.full === true;
1564
+ const cached = ctx.session.blockContents.get(blockId);
1565
+ let body;
1566
+ let count;
1567
+ if (cached) {
1568
+ const view = full ? cached.full : cached.one;
1569
+ body = view.text;
1570
+ count = view.count;
1571
+ } else {
1572
+ const collected = collectBlockContent2(ctx.session.state, block, ctx.messages, { full });
1573
+ body = collected.text || block.summary;
1574
+ count = collected.count;
749
1575
  }
750
- if (toolName === "decompress") {
751
- const rawBlockId = args.blockId;
752
- if (typeof rawBlockId !== "string" || rawBlockId.length === 0) {
753
- return "[decompress FAILED: blockId is required]";
754
- }
755
- const blockId = rawBlockId.trim();
756
- const block = ctx.core.decompress(blockId, ctx.session.state);
757
- if (!block) return `[Block ${blockId} not found]`;
758
- const full = args.full === true;
759
- const collected = collectBlockContent(ctx.session.state, block, ctx.messages, { full });
1576
+ if (count > 0 || cached) {
760
1577
  ctx.session.state = deactivateBlock(ctx.session.state, [blockId]);
761
- const header = `[Restored block ${blockId} \u2014 ${collected.count} item(s)${full ? ", full" : ""}]`;
762
- const body = collected.text || block.summary;
763
- const safeBlockId = blockId.replace(/[^a-zA-Z0-9_-]/g, "-");
764
- const outPath = body.length > 1e4 ? join(tmpdir(), `acp-decompress-${safeBlockId}-${Date.now()}.txt`) : null;
765
- if (outPath) {
766
- try {
767
- mkdirSync(dirname(outPath), { recursive: true });
768
- writeFileSync(outPath, body, "utf8");
769
- return `${header}
1578
+ ctx.session.blockContents.delete(blockId);
1579
+ }
1580
+ const header = `[Restored block ${blockId} \u2014 ${count} item(s)${full ? ", full" : ""}]`;
1581
+ const safeBlockId = blockId.replace(/[^a-zA-Z0-9_-]/g, "-");
1582
+ const outPath = body.length > 1e4 ? join2(tmpdir(), `acp-decompress-${safeBlockId}-${Date.now()}.txt`) : null;
1583
+ if (outPath) {
1584
+ try {
1585
+ mkdirSync2(dirname2(outPath), { recursive: true });
1586
+ writeFileSync2(outPath, body, "utf8");
1587
+ return `${header}
770
1588
  Content (${body.length} chars) written to: ${outPath}
771
1589
  Use the read tool to access it.`;
772
- } catch (e) {
773
- return `${header}
1590
+ } catch (e) {
1591
+ return `${header}
774
1592
  [Failed to write to ${outPath}: ${String(e)}]
775
1593
  ${body.slice(0, 4e3)}...`;
776
- }
777
1594
  }
778
- return `${header}
779
- ${body}`;
780
1595
  }
781
- if (toolName === "search_context") {
782
- const query = typeof args.query === "string" ? args.query : "";
1596
+ return `${header}
1597
+ ${body}`;
1598
+ }
1599
+
1600
+ // src/compress-loop.ts
1601
+ function executeProxyTool(toolName, args, ctx) {
1602
+ if (toolName === "compress") {
1603
+ return applyRanges(parseCompressInput(args), ctx);
1604
+ }
1605
+ if (toolName === "decompress") {
1606
+ return resolveDecompress(args, ctx);
1607
+ }
1608
+ if (toolName === "search_context") {
1609
+ const query = typeof args.query === "string" ? args.query : "";
783
1610
  if (query.length === 0) return "[search_context FAILED: query is required]";
784
1611
  const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 5;
785
1612
  const blocks = ctx.core.search(query, ctx.session.state).slice(0, limit);
@@ -912,27 +1739,469 @@ ${icon} [ACP] ${inner}
912
1739
  }
913
1740
  async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOptions) {
914
1741
  let upstream = initialUpstream;
915
- const model = requestBody.model ?? "unknown";
916
- let responseId = `chatcmpl-proxy-${Date.now()}`;
917
- const makeBase = () => ({
918
- id: responseId,
919
- object: "chat.completion.chunk",
920
- created: Date.now(),
921
- model
922
- });
1742
+ let activeClearTimer = null;
1743
+ try {
1744
+ const model = requestBody.model ?? "unknown";
1745
+ let responseId = `chatcmpl-proxy-${Date.now()}`;
1746
+ const makeBase = () => ({
1747
+ id: responseId,
1748
+ object: "chat.completion.chunk",
1749
+ created: Date.now(),
1750
+ model
1751
+ });
1752
+ let loopCount = 0;
1753
+ for (; ; ) {
1754
+ loopCount++;
1755
+ if (loopCount > 10) {
1756
+ ctx.log("[acp-proxy: compress loop limit (10) reached, forwarding as-is]");
1757
+ yield Buffer.from(buildFinishSse(makeBase(), "stop", null), "utf8");
1758
+ yield Buffer.from("data: [DONE]\n\n", "utf8");
1759
+ return;
1760
+ }
1761
+ const toolCallByIndex = /* @__PURE__ */ new Map();
1762
+ let contentText = "";
1763
+ let finishReason = null;
1764
+ let usage = null;
1765
+ const isFirstRound = loopCount === 1;
1766
+ const reader = upstream.getReader();
1767
+ const decoder = new TextDecoder("utf-8");
1768
+ let sseBuffer = "";
1769
+ try {
1770
+ for (; ; ) {
1771
+ const { done, value } = await reader.read();
1772
+ if (done) break;
1773
+ sseBuffer += decoder.decode(value, { stream: true });
1774
+ sseBuffer = normalizeSseLineEndings(sseBuffer);
1775
+ let sep;
1776
+ while ((sep = sseBuffer.indexOf("\n\n")) >= 0) {
1777
+ const eventStr = sseBuffer.slice(0, sep);
1778
+ sseBuffer = sseBuffer.slice(sep + 2);
1779
+ if (!eventStr.trim()) continue;
1780
+ const d = classifySseEvent(eventStr);
1781
+ if (d.done) {
1782
+ continue;
1783
+ }
1784
+ if (isFirstRound) {
1785
+ if (d.yieldChunk) {
1786
+ if (!responseId) {
1787
+ const dataLine = eventStr.split("\n").find((l) => l.startsWith("data:"));
1788
+ if (dataLine) {
1789
+ try {
1790
+ const p = JSON.parse(dataLine.slice(5).trim());
1791
+ if (typeof p.id === "string") responseId = p.id;
1792
+ } catch {
1793
+ }
1794
+ }
1795
+ }
1796
+ yield d.yieldChunk;
1797
+ }
1798
+ } else {
1799
+ if (d.contentDelta) {
1800
+ yield Buffer.from(buildContentSse(responseId, model, d.contentDelta), "utf8");
1801
+ }
1802
+ }
1803
+ if (d.contentDelta) contentText += d.contentDelta;
1804
+ if (d.finishReason) finishReason = d.finishReason;
1805
+ if (d.usage !== void 0) usage = d.usage;
1806
+ if (d.toolCalls) {
1807
+ for (const tc of d.toolCalls) {
1808
+ const existing = toolCallByIndex.get(tc.index);
1809
+ if (existing) {
1810
+ if (tc.name) existing.name = tc.name;
1811
+ if (tc.id) existing.id = tc.id;
1812
+ existing.arguments += tc.arguments;
1813
+ } else {
1814
+ toolCallByIndex.set(tc.index, tc);
1815
+ }
1816
+ }
1817
+ }
1818
+ }
1819
+ }
1820
+ sseBuffer += decoder.decode();
1821
+ sseBuffer = normalizeSseLineEndings(sseBuffer);
1822
+ let resSep;
1823
+ while ((resSep = sseBuffer.indexOf("\n\n")) >= 0) {
1824
+ const eventStr = sseBuffer.slice(0, resSep);
1825
+ sseBuffer = sseBuffer.slice(resSep + 2);
1826
+ if (!eventStr.trim()) continue;
1827
+ const d = classifySseEvent(eventStr);
1828
+ if (d.done) continue;
1829
+ if (isFirstRound) {
1830
+ if (d.yieldChunk) yield d.yieldChunk;
1831
+ } else {
1832
+ if (d.contentDelta) yield Buffer.from(buildContentSse(responseId, model, d.contentDelta), "utf8");
1833
+ }
1834
+ if (d.contentDelta) contentText += d.contentDelta;
1835
+ if (d.finishReason) finishReason = d.finishReason;
1836
+ if (d.usage !== void 0) usage = d.usage;
1837
+ if (d.toolCalls) {
1838
+ for (const tc of d.toolCalls) {
1839
+ const existing = toolCallByIndex.get(tc.index);
1840
+ if (existing) {
1841
+ if (tc.name) existing.name = tc.name;
1842
+ if (tc.id) existing.id = tc.id;
1843
+ existing.arguments += tc.arguments;
1844
+ } else {
1845
+ toolCallByIndex.set(tc.index, tc);
1846
+ }
1847
+ }
1848
+ }
1849
+ }
1850
+ } finally {
1851
+ reader.releaseLock();
1852
+ }
1853
+ const sortedIndices = [...toolCallByIndex.keys()].sort((a, b) => a - b);
1854
+ const toolCalls = sortedIndices.map((i) => {
1855
+ const tc = toolCallByIndex.get(i);
1856
+ return { ...tc, id: tc.id || `call_${tc.index}` };
1857
+ }).filter((tc) => tc.name.length > 0);
1858
+ const proxyCalls = toolCalls.filter((tc) => PROXY_TOOL_NAMES.has(tc.name));
1859
+ const realCalls = toolCalls.filter((tc) => !PROXY_TOOL_NAMES.has(tc.name));
1860
+ const hasOnlyProxy = proxyCalls.length > 0 && realCalls.length === 0;
1861
+ if (!hasOnlyProxy) {
1862
+ for (const tc of realCalls) {
1863
+ yield Buffer.from(buildToolCallSse(makeBase(), tc), "utf8");
1864
+ }
1865
+ const fr = realCalls.length > 0 ? "tool_calls" : finishReason ?? "stop";
1866
+ yield Buffer.from(buildFinishSse(makeBase(), fr, usage), "utf8");
1867
+ yield Buffer.from("data: [DONE]\n\n", "utf8");
1868
+ return;
1869
+ }
1870
+ const names = proxyCalls.map((c) => c.name).join(", ");
1871
+ ctx.log(`[acp-proxy: round ${loopCount} \u2014 ${proxyCalls.length} proxy call(s): ${names}]`);
1872
+ const messages = requestBody.messages ?? [];
1873
+ messages.push({
1874
+ role: "assistant",
1875
+ content: contentText || null,
1876
+ tool_calls: proxyCalls.map((tc) => ({
1877
+ id: tc.id,
1878
+ type: "function",
1879
+ function: { name: tc.name, arguments: tc.arguments }
1880
+ }))
1881
+ });
1882
+ for (const tc of proxyCalls) {
1883
+ let args = {};
1884
+ try {
1885
+ args = JSON.parse(tc.arguments);
1886
+ } catch {
1887
+ args = {};
1888
+ }
1889
+ const result = executeProxyTool(tc.name, args, ctx);
1890
+ const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
1891
+ ctx.log(`[acp-proxy: ${tc.name} (${tc.id}) \u2192 ${preview.replace(/\n/g, " ")}]`);
1892
+ yield Buffer.from(
1893
+ buildContentSse(responseId, model, buildVisibilityMarker(tc.name, result)),
1894
+ "utf8"
1895
+ );
1896
+ messages.push({
1897
+ role: "tool",
1898
+ tool_call_id: tc.id,
1899
+ content: result
1900
+ });
1901
+ }
1902
+ requestBody.messages = messages;
1903
+ const { response: resp, clearTimer } = await fetchWithTimeout(requestOptions.url, {
1904
+ method: "POST",
1905
+ headers: requestOptions.headers,
1906
+ body: JSON.stringify(requestBody)
1907
+ });
1908
+ if (!resp.ok || !resp.body) {
1909
+ const errText = await resp.text().catch(() => "upstream error");
1910
+ ctx.log(`[acp-proxy: compress loop upstream error ${resp.status}: ${errText.slice(0, 200)}]`);
1911
+ yield Buffer.from(
1912
+ `data: ${JSON.stringify({
1913
+ ...makeBase(),
1914
+ choices: [{
1915
+ index: 0,
1916
+ delta: { content: `
1917
+ [acp-proxy: upstream error ${resp.status}: ${errText.slice(0, 200)}]
1918
+ ` },
1919
+ finish_reason: null
1920
+ }]
1921
+ })}
1922
+
1923
+ `,
1924
+ "utf8"
1925
+ );
1926
+ yield Buffer.from(buildFinishSse(makeBase(), "stop", null), "utf8");
1927
+ yield Buffer.from("data: [DONE]\n\n", "utf8");
1928
+ return;
1929
+ }
1930
+ upstream = resp.body;
1931
+ if (activeClearTimer) activeClearTimer();
1932
+ activeClearTimer = clearTimer;
1933
+ }
1934
+ } finally {
1935
+ if (activeClearTimer) {
1936
+ activeClearTimer();
1937
+ activeClearTimer = null;
1938
+ }
1939
+ }
1940
+ }
1941
+
1942
+ // src/compress-loop-responses.ts
1943
+ import {
1944
+ buildStatusReport as buildStatusReport2,
1945
+ estimateTokensFast as estimateTokensFast2
1946
+ } from "acp-kernel";
1947
+ var TEXT_PROTOCOL = process.env.ACP_COMPRESS_PROTOCOL === "text";
1948
+ function extractTextTriggers(text) {
1949
+ const calls = [];
1950
+ let clean = "";
1951
+ let i = 0;
1952
+ let n = 0;
1953
+ while (i < text.length) {
1954
+ const open = text.indexOf(ACP_TEXT_OPEN, i);
1955
+ if (open === -1) {
1956
+ clean += text.slice(i);
1957
+ break;
1958
+ }
1959
+ clean += text.slice(i, open);
1960
+ const after = open + ACP_TEXT_OPEN.length;
1961
+ const close = text.indexOf(ACP_TEXT_CLOSE, after);
1962
+ if (close === -1) {
1963
+ clean += text.slice(open);
1964
+ break;
1965
+ }
1966
+ const payload = text.slice(after, close).trim();
1967
+ if (payload) {
1968
+ const stamp = `${Date.now()}_${n++}`;
1969
+ calls.push({
1970
+ itemId: `fc_text_${stamp}`,
1971
+ callId: `call_text_${stamp}`,
1972
+ name: COMPRESS_TOOL_NAME,
1973
+ arguments: payload
1974
+ });
1975
+ }
1976
+ i = close + ACP_TEXT_CLOSE.length;
1977
+ }
1978
+ return { clean, calls };
1979
+ }
1980
+ function executeProxyTool2(toolName, args, ctx) {
1981
+ if (toolName === "compress") {
1982
+ return applyRanges(parseCompressInput(args), ctx);
1983
+ }
1984
+ if (toolName === "decompress") {
1985
+ return resolveDecompress(args, ctx);
1986
+ }
1987
+ if (toolName === "search_context") {
1988
+ const query = typeof args.query === "string" ? args.query : "";
1989
+ if (query.length === 0) return "[search_context FAILED: query is required]";
1990
+ const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 5;
1991
+ const blocks = ctx.core.search(query, ctx.session.state).slice(0, limit);
1992
+ if (blocks.length === 0) return `[No blocks matched "${query}"]`;
1993
+ const lines = blocks.map((b) => {
1994
+ const topic = b.topic ?? "(no topic)";
1995
+ const preview = b.summary.length > 200 ? b.summary.slice(0, 200) + "..." : b.summary;
1996
+ return `${b.blockId} (T${b.tier}) "${topic}"
1997
+ ${preview}`;
1998
+ });
1999
+ return `Found ${blocks.length} block(s) for "${query}":
2000
+
2001
+ ${lines.join("\n\n")}`;
2002
+ }
2003
+ if (toolName === "acp_status") {
2004
+ return buildStatusReport2(ctx.session.state, ctx.messages, estimateTokensFast2);
2005
+ }
2006
+ return `[Unknown proxy tool: ${toolName}]`;
2007
+ }
2008
+ function extractEventType(rawEvent) {
2009
+ for (const l of rawEvent.split("\n")) {
2010
+ if (l.startsWith("event:")) return l.slice(6).trim();
2011
+ }
2012
+ return null;
2013
+ }
2014
+ function extractDataLine(rawEvent) {
2015
+ const parts = [];
2016
+ for (const l of rawEvent.split("\n")) {
2017
+ if (l.startsWith("data:")) {
2018
+ let v = l.slice(5);
2019
+ if (v.startsWith(" ")) v = v.slice(1);
2020
+ parts.push(v);
2021
+ }
2022
+ }
2023
+ return parts.length ? parts.join("\n") : null;
2024
+ }
2025
+ function classifyResponsesSseEvent(eventStr) {
2026
+ const type = extractEventType(eventStr);
2027
+ const dataLine = extractDataLine(eventStr);
2028
+ if (!type || !dataLine) return {};
2029
+ let obj;
2030
+ try {
2031
+ obj = JSON.parse(dataLine);
2032
+ } catch {
2033
+ return {};
2034
+ }
2035
+ const out = {};
2036
+ switch (type) {
2037
+ case "response.created":
2038
+ case "response.in_progress":
2039
+ out.isMeta = true;
2040
+ out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
2041
+ return out;
2042
+ case "response.output_item.added": {
2043
+ const item = obj.item;
2044
+ if (item?.type === "function_call") {
2045
+ const name = typeof item.name === "string" ? item.name : "";
2046
+ out.fcStart = {
2047
+ itemId: typeof item.id === "string" ? item.id : "",
2048
+ callId: typeof item.call_id === "string" ? item.call_id : "",
2049
+ name
2050
+ };
2051
+ return out;
2052
+ }
2053
+ out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
2054
+ return out;
2055
+ }
2056
+ case "response.content_part.added":
2057
+ case "response.content_part.done":
2058
+ case "response.output_text.done":
2059
+ out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
2060
+ return out;
2061
+ case "response.output_text.delta": {
2062
+ const delta = typeof obj.delta === "string" ? obj.delta : "";
2063
+ if (delta) {
2064
+ out.contentDelta = delta;
2065
+ out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
2066
+ }
2067
+ return out;
2068
+ }
2069
+ case "response.function_call_arguments.delta": {
2070
+ const itemId = typeof obj.item_id === "string" ? obj.item_id : "";
2071
+ const delta = typeof obj.delta === "string" ? obj.delta : "";
2072
+ out.fcArgs = { itemId, delta };
2073
+ return out;
2074
+ }
2075
+ case "response.output_item.done": {
2076
+ const item = obj.item;
2077
+ if (item?.type === "function_call") {
2078
+ out.fcDone = { itemId: typeof item.id === "string" ? item.id : "" };
2079
+ return out;
2080
+ }
2081
+ out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
2082
+ return out;
2083
+ }
2084
+ case "response.completed":
2085
+ out.isMeta = true;
2086
+ out.terminal = true;
2087
+ out.terminalKind = "completed";
2088
+ out.responseObj = obj.response ?? null;
2089
+ return out;
2090
+ case "response.incomplete":
2091
+ out.isMeta = true;
2092
+ out.terminal = true;
2093
+ out.terminalKind = "incomplete";
2094
+ out.terminalRaw = eventStr;
2095
+ return out;
2096
+ case "response.failed":
2097
+ case "response.error":
2098
+ out.isMeta = true;
2099
+ out.terminal = true;
2100
+ out.terminalKind = "failed";
2101
+ out.terminalRaw = eventStr;
2102
+ return out;
2103
+ default:
2104
+ out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
2105
+ return out;
2106
+ }
2107
+ }
2108
+ function buildMessageItemSequence(itemId, outputIndex, text) {
2109
+ const item = { type: "message", id: itemId, role: "assistant", content: [] };
2110
+ const part = { type: "output_text", text: "" };
2111
+ const doneItem = { type: "message", id: itemId, role: "assistant", content: [{ type: "output_text", text }] };
2112
+ return [
2113
+ `event: response.output_item.added
2114
+ data: ${JSON.stringify({ type: "response.output_item.added", output_index: outputIndex, item })}
2115
+
2116
+ `,
2117
+ `event: response.content_part.added
2118
+ data: ${JSON.stringify({ type: "response.content_part.added", item_id: itemId, output_index: outputIndex, part })}
2119
+
2120
+ `,
2121
+ `event: response.output_text.delta
2122
+ data: ${JSON.stringify({ type: "response.output_text.delta", item_id: itemId, output_index: outputIndex, delta: text })}
2123
+
2124
+ `,
2125
+ `event: response.output_text.done
2126
+ data: ${JSON.stringify({ type: "response.output_text.done", item_id: itemId, output_index: outputIndex, text })}
2127
+
2128
+ `,
2129
+ `event: response.content_part.done
2130
+ data: ${JSON.stringify({ type: "response.content_part.done", item_id: itemId, output_index: outputIndex, part: { type: "output_text", text } })}
2131
+
2132
+ `,
2133
+ `event: response.output_item.done
2134
+ data: ${JSON.stringify({ type: "response.output_item.done", output_index: outputIndex, item: doneItem })}
2135
+
2136
+ `
2137
+ ].join("");
2138
+ }
2139
+ function buildFunctionCallEvents(fc, outputIndex) {
2140
+ return [
2141
+ `event: response.output_item.added
2142
+ data: ${JSON.stringify({
2143
+ type: "response.output_item.added",
2144
+ output_index: outputIndex,
2145
+ item: { type: "function_call", id: fc.itemId, call_id: fc.callId, name: fc.name, arguments: "" }
2146
+ })}
2147
+
2148
+ `,
2149
+ `event: response.function_call_arguments.delta
2150
+ data: ${JSON.stringify({
2151
+ type: "response.function_call_arguments.delta",
2152
+ item_id: fc.itemId,
2153
+ delta: fc.arguments
2154
+ })}
2155
+
2156
+ `,
2157
+ `event: response.function_call_arguments.done
2158
+ data: ${JSON.stringify({
2159
+ type: "response.function_call_arguments.done",
2160
+ item_id: fc.itemId,
2161
+ arguments: fc.arguments
2162
+ })}
2163
+
2164
+ `,
2165
+ `event: response.output_item.done
2166
+ data: ${JSON.stringify({
2167
+ type: "response.output_item.done",
2168
+ output_index: outputIndex,
2169
+ item: { type: "function_call", id: fc.itemId, call_id: fc.callId, name: fc.name, arguments: fc.arguments }
2170
+ })}
2171
+
2172
+ `
2173
+ ].join("");
2174
+ }
2175
+ function buildCompleted(responseObj) {
2176
+ const resp = responseObj ?? { id: `resp-proxy-${Date.now()}`, status: "completed", output: [] };
2177
+ return `event: response.completed
2178
+ data: ${JSON.stringify({
2179
+ type: "response.completed",
2180
+ response: resp
2181
+ })}
2182
+
2183
+ `;
2184
+ }
2185
+ async function* compressLoopResponsesStream(initialUpstream, ctx, requestBody, requestOptions) {
2186
+ let upstream = initialUpstream;
923
2187
  let loopCount = 0;
2188
+ let responseObj = null;
2189
+ let activeClearTimer = null;
2190
+ let nextOutputIndex = 0;
924
2191
  for (; ; ) {
925
2192
  loopCount++;
926
- if (loopCount > 10) {
927
- ctx.log("[acp-proxy: compress loop limit (10) reached, forwarding as-is]");
928
- yield Buffer.from(buildFinishSse(makeBase(), "stop", null), "utf8");
929
- yield Buffer.from("data: [DONE]\n\n", "utf8");
2193
+ if (loopCount > 5) {
2194
+ ctx.log("[acp-proxy: responses compress loop limit (5) reached, forwarding completion as-is]");
2195
+ const limItemId = `msg_acp_limit_${Date.now()}`;
2196
+ yield Buffer.from(buildMessageItemSequence(limItemId, nextOutputIndex++, "\n[acp-proxy: compress loop limit reached]\n"), "utf8");
2197
+ yield Buffer.from(buildCompleted(responseObj), "utf8");
930
2198
  return;
931
2199
  }
932
- const toolCallByIndex = /* @__PURE__ */ new Map();
2200
+ const fcByItemId = /* @__PURE__ */ new Map();
933
2201
  let contentText = "";
934
- let finishReason = null;
935
- let usage = null;
2202
+ let completed = false;
2203
+ let terminalKind = null;
2204
+ let terminalRaw = null;
936
2205
  const isFirstRound = loopCount === 1;
937
2206
  const reader = upstream.getReader();
938
2207
  const decoder = new TextDecoder("utf-8");
@@ -942,156 +2211,179 @@ async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOpt
942
2211
  const { done, value } = await reader.read();
943
2212
  if (done) break;
944
2213
  sseBuffer += decoder.decode(value, { stream: true });
2214
+ if (sseBuffer.indexOf("\r") !== -1) sseBuffer = sseBuffer.replace(/\r\n|\r/g, "\n");
945
2215
  let sep;
946
2216
  while ((sep = sseBuffer.indexOf("\n\n")) >= 0) {
947
2217
  const eventStr = sseBuffer.slice(0, sep);
948
2218
  sseBuffer = sseBuffer.slice(sep + 2);
949
2219
  if (!eventStr.trim()) continue;
950
- const d = classifySseEvent(eventStr);
951
- if (d.done) {
952
- continue;
2220
+ const d = classifyResponsesSseEvent(eventStr);
2221
+ if (d.yieldChunk && (isFirstRound || !d.isMeta) && !(TEXT_PROTOCOL && !d.isMeta)) {
2222
+ yield d.yieldChunk;
953
2223
  }
954
- if (isFirstRound) {
955
- if (d.yieldChunk) {
956
- if (!responseId) {
957
- const dataLine = eventStr.split("\n").find((l) => l.startsWith("data:"));
958
- if (dataLine) {
959
- try {
960
- const p = JSON.parse(dataLine.slice(5).trim());
961
- if (typeof p.id === "string") responseId = p.id;
962
- } catch {
963
- }
964
- }
965
- }
966
- yield d.yieldChunk;
967
- }
968
- } else {
969
- if (d.contentDelta) {
970
- yield Buffer.from(buildContentSse(responseId, model, d.contentDelta), "utf8");
2224
+ if (d.contentDelta) contentText += d.contentDelta;
2225
+ if (d.fcStart) {
2226
+ fcByItemId.set(d.fcStart.itemId, {
2227
+ itemId: d.fcStart.itemId,
2228
+ callId: d.fcStart.callId,
2229
+ name: d.fcStart.name,
2230
+ arguments: ""
2231
+ });
2232
+ }
2233
+ if (d.fcArgs) {
2234
+ const existing = fcByItemId.get(d.fcArgs.itemId);
2235
+ if (existing) existing.arguments += d.fcArgs.delta;
2236
+ }
2237
+ if (d.fcDone) {
2238
+ const existing = fcByItemId.get(d.fcDone.itemId);
2239
+ if (existing && !existing.arguments) {
2240
+ const item = JSON.parse(extractDataLine(eventStr) ?? "{}").item;
2241
+ const args = typeof item?.arguments === "string" ? item.arguments : "";
2242
+ existing.arguments = args;
971
2243
  }
972
2244
  }
973
- if (d.contentDelta) contentText += d.contentDelta;
974
- if (d.finishReason) finishReason = d.finishReason;
975
- if (d.usage !== void 0) usage = d.usage;
976
- if (d.toolCalls) {
977
- for (const tc of d.toolCalls) {
978
- const existing = toolCallByIndex.get(tc.index);
979
- if (existing) {
980
- if (tc.name) existing.name = tc.name;
981
- if (tc.id) existing.id = tc.id;
982
- existing.arguments += tc.arguments;
983
- } else {
984
- toolCallByIndex.set(tc.index, tc);
985
- }
2245
+ if (d.terminal) {
2246
+ completed = true;
2247
+ terminalKind = d.terminalKind ?? null;
2248
+ terminalRaw = d.terminalRaw ?? null;
2249
+ responseObj = d.responseObj ?? responseObj;
2250
+ const resp2 = d.responseObj ?? {};
2251
+ const usage = resp2.usage;
2252
+ if (usage && d.terminalKind === "completed") {
2253
+ const prompt = usage.input_tokens ?? usage.prompt_tokens ?? "?";
2254
+ const inDet = usage.input_tokens_details;
2255
+ const prDet = usage.prompt_tokens_details;
2256
+ const cached = inDet?.cached_tokens ?? prDet?.cached_tokens ?? "?";
2257
+ const out = usage.output_tokens ?? "?";
2258
+ console.error(`[acp-usage] round ${loopCount} input=${prompt} cached=${cached} output=${out}${cached !== "?" && cached !== 0 && prompt !== "?" ? ` (cache hit ${Math.round(Number(cached) / Number(prompt) * 100)}%)` : ""}`);
986
2259
  }
987
2260
  }
988
2261
  }
989
2262
  }
990
- sseBuffer += decoder.decode();
991
2263
  } finally {
992
2264
  reader.releaseLock();
2265
+ if (activeClearTimer) {
2266
+ activeClearTimer();
2267
+ activeClearTimer = null;
2268
+ }
993
2269
  }
994
- const sortedIndices = [...toolCallByIndex.keys()].sort((a, b) => a - b);
995
- const toolCalls = sortedIndices.map((i) => {
996
- const tc = toolCallByIndex.get(i);
997
- return { ...tc, id: tc.id || `call_${tc.index}` };
998
- }).filter((tc) => tc.name.length > 0);
999
- const proxyCalls = toolCalls.filter((tc) => PROXY_TOOL_NAMES.has(tc.name));
1000
- const realCalls = toolCalls.filter((tc) => !PROXY_TOOL_NAMES.has(tc.name));
2270
+ if (TEXT_PROTOCOL) {
2271
+ const extracted = extractTextTriggers(contentText);
2272
+ contentText = extracted.clean;
2273
+ for (const c of extracted.calls) {
2274
+ fcByItemId.set(c.itemId, c);
2275
+ }
2276
+ if (contentText.trim()) {
2277
+ const textItemId = `msg_acp_text_r${loopCount}_${Date.now()}`;
2278
+ yield Buffer.from(buildMessageItemSequence(textItemId, nextOutputIndex++, contentText), "utf8");
2279
+ }
2280
+ }
2281
+ const allCalls = [...fcByItemId.values()].filter((c) => c.name.length > 0);
2282
+ const proxyCalls = allCalls.filter((c) => PROXY_TOOL_NAMES.has(c.name));
2283
+ const realCalls = allCalls.filter((c) => !PROXY_TOOL_NAMES.has(c.name));
2284
+ console.error(`[acp-diag] round ${loopCount} allCalls=[${allCalls.map((c) => c.name).join(",")}] realCalls=[${realCalls.map((c) => c.name).join(",")}] text=${JSON.stringify(contentText.slice(0, 120))}`);
1001
2285
  const hasOnlyProxy = proxyCalls.length > 0 && realCalls.length === 0;
1002
2286
  if (!hasOnlyProxy) {
1003
- for (const tc of realCalls) {
1004
- yield Buffer.from(buildToolCallSse(makeBase(), tc), "utf8");
2287
+ let oi = nextOutputIndex;
2288
+ for (const fc of realCalls) {
2289
+ yield Buffer.from(buildFunctionCallEvents(fc, oi), "utf8");
2290
+ oi++;
1005
2291
  }
1006
- const fr = realCalls.length > 0 ? "tool_calls" : finishReason ?? "stop";
1007
- yield Buffer.from(buildFinishSse(makeBase(), fr, usage), "utf8");
1008
- yield Buffer.from("data: [DONE]\n\n", "utf8");
2292
+ nextOutputIndex = oi;
2293
+ if (terminalKind && terminalKind !== "completed" && terminalRaw) {
2294
+ yield Buffer.from(terminalRaw + "\n\n", "utf8");
2295
+ return;
2296
+ }
2297
+ if (!completed && contentText.length === 0 && realCalls.length === 0) {
2298
+ ctx.log("[acp-proxy: responses stream ended without completion]");
2299
+ }
2300
+ yield Buffer.from(buildCompleted(responseObj), "utf8");
1009
2301
  return;
1010
2302
  }
1011
2303
  const names = proxyCalls.map((c) => c.name).join(", ");
1012
- ctx.log(`[acp-proxy: round ${loopCount} \u2014 ${proxyCalls.length} proxy call(s): ${names}]`);
1013
- const messages = requestBody.messages ?? [];
1014
- messages.push({
1015
- role: "assistant",
1016
- content: contentText || null,
1017
- tool_calls: proxyCalls.map((tc) => ({
1018
- id: tc.id,
1019
- type: "function",
1020
- function: { name: tc.name, arguments: tc.arguments }
1021
- }))
1022
- });
1023
- for (const tc of proxyCalls) {
2304
+ ctx.log(`[acp-proxy: responses round ${loopCount} \u2014 ${proxyCalls.length} proxy call(s): ${names}]`);
2305
+ const inputItems = Array.isArray(requestBody.input) ? [...requestBody.input] : [];
2306
+ if (contentText) {
2307
+ inputItems.push({
2308
+ type: "message",
2309
+ role: "assistant",
2310
+ content: [{ type: "output_text", text: contentText }]
2311
+ });
2312
+ }
2313
+ for (const fc of proxyCalls) {
2314
+ inputItems.push({
2315
+ type: "function_call",
2316
+ id: fc.itemId || `fc_${Date.now()}`,
2317
+ call_id: fc.callId || `call_${Date.now()}`,
2318
+ name: fc.name,
2319
+ arguments: fc.arguments
2320
+ });
2321
+ }
2322
+ for (const fc of proxyCalls) {
1024
2323
  let args = {};
1025
2324
  try {
1026
- args = JSON.parse(tc.arguments);
1027
- } catch {
2325
+ args = JSON.parse(fc.arguments);
2326
+ } catch (e) {
2327
+ console.error(`[acp-compress-args] ${fc.name} JSON.parse failed: ${String(e)}. raw arguments (len=${fc.arguments.length}): ${fc.arguments.slice(0, 300)}`);
1028
2328
  args = {};
1029
2329
  }
1030
- const result = executeProxyTool(tc.name, args, ctx);
2330
+ if (fc.name === "compress") {
2331
+ console.error(`[acp-compress-args] compress args parsed: ${JSON.stringify(args).slice(0, 400)}`);
2332
+ }
2333
+ const result = executeProxyTool2(fc.name, args, ctx);
1031
2334
  const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
1032
- ctx.log(`[acp-proxy: ${tc.name} (${tc.id}) \u2192 ${preview.replace(/\n/g, " ")}]`);
2335
+ ctx.log(`[acp-proxy: responses ${fc.name} (${fc.callId}) \u2192 ${preview.replace(/\n/g, " ")}]`);
2336
+ const markerItemId = `msg_acp_${Date.now()}_${nextOutputIndex}`;
1033
2337
  yield Buffer.from(
1034
- buildContentSse(responseId, model, buildVisibilityMarker(tc.name, result)),
2338
+ buildMessageItemSequence(markerItemId, nextOutputIndex++, buildVisibilityMarker(fc.name, result)),
1035
2339
  "utf8"
1036
2340
  );
1037
- messages.push({
1038
- role: "tool",
1039
- tool_call_id: tc.id,
1040
- content: result
2341
+ inputItems.push({
2342
+ type: "function_call_output",
2343
+ call_id: fc.callId || `call_${Date.now()}`,
2344
+ output: result
1041
2345
  });
1042
2346
  }
1043
- requestBody.messages = messages;
1044
- const resp = await fetch(requestOptions.url, {
2347
+ requestBody.input = inputItems;
2348
+ if (!("stream" in requestBody)) requestBody.stream = true;
2349
+ const { response: resp, clearTimer } = await fetchWithTimeout(requestOptions.url, {
1045
2350
  method: "POST",
1046
2351
  headers: requestOptions.headers,
1047
2352
  body: JSON.stringify(requestBody)
1048
2353
  });
1049
2354
  if (!resp.ok || !resp.body) {
2355
+ clearTimer();
1050
2356
  const errText = await resp.text().catch(() => "upstream error");
1051
- ctx.log(`[acp-proxy: compress loop upstream error ${resp.status}: ${errText.slice(0, 200)}]`);
2357
+ ctx.log(`[acp-proxy: responses compress loop upstream error ${resp.status}: ${errText.slice(0, 200)}]`);
2358
+ const errItemId = `msg_acp_err_${Date.now()}`;
1052
2359
  yield Buffer.from(
1053
- `data: ${JSON.stringify({
1054
- ...makeBase(),
1055
- choices: [{
1056
- index: 0,
1057
- delta: { content: `
1058
- [acp-proxy: upstream error ${resp.status}]
1059
- ` },
1060
- finish_reason: null
1061
- }]
1062
- })}
1063
-
1064
- `,
2360
+ buildMessageItemSequence(errItemId, nextOutputIndex++, `
2361
+ [acp-proxy: upstream error ${resp.status}: ${errText.slice(0, 200)}]
2362
+ `),
1065
2363
  "utf8"
1066
2364
  );
1067
- yield Buffer.from(buildFinishSse(makeBase(), "stop", null), "utf8");
1068
- yield Buffer.from("data: [DONE]\n\n", "utf8");
2365
+ yield Buffer.from(buildCompleted(responseObj), "utf8");
1069
2366
  return;
1070
2367
  }
1071
2368
  upstream = resp.body;
2369
+ if (activeClearTimer) activeClearTimer();
2370
+ activeClearTimer = clearTimer;
1072
2371
  }
1073
2372
  }
1074
2373
 
1075
2374
  // src/stream-openai.ts
1076
- function safeJsonParse(s) {
1077
- try {
1078
- return s ? JSON.parse(s) : {};
1079
- } catch {
1080
- return {};
1081
- }
1082
- }
1083
2375
  function rewriteOpenaiJsonResponse(body, ctx) {
1084
2376
  if (!body || typeof body !== "object") return body;
1085
2377
  const b = body;
1086
2378
  const choice = b.choices?.[0];
1087
- const msg = choice?.message;
1088
- if (!choice || !msg) return body;
2379
+ const msg2 = choice?.message;
2380
+ if (!choice || !msg2) return body;
1089
2381
  let converted = false;
1090
2382
  let sawReal = false;
1091
2383
  const noteParts = [];
1092
2384
  const keepToolCalls = [];
1093
- const existingText = typeof msg.content === "string" ? msg.content : "";
1094
- const toolCalls = msg.tool_calls;
2385
+ const existingText = typeof msg2.content === "string" ? msg2.content : "";
2386
+ const toolCalls = msg2.tool_calls;
1095
2387
  if (Array.isArray(toolCalls)) {
1096
2388
  for (const tc of toolCalls) {
1097
2389
  if (tc.function?.name === COMPRESS_TOOL_NAME) {
@@ -1103,14 +2395,17 @@ function rewriteOpenaiJsonResponse(body, ctx) {
1103
2395
  }
1104
2396
  }
1105
2397
  }
2398
+ if (existingText && (existingText.includes("<acp ") || existingText.includes("</acp"))) {
2399
+ ctx.log(`[warn: tag echo] non-stream openai output contains <acp tag: ${existingText.slice(0, 120).replace(/\n/g, " ")}`);
2400
+ }
1106
2401
  if (!converted) return body;
1107
2402
  const note = noteParts.join("\n");
1108
- msg.content = existingText ? `${existingText}
2403
+ msg2.content = existingText ? `${existingText}
1109
2404
  ${note}` : note;
1110
2405
  if (keepToolCalls.length > 0) {
1111
- msg.tool_calls = keepToolCalls;
2406
+ msg2.tool_calls = keepToolCalls;
1112
2407
  } else {
1113
- delete msg.tool_calls;
2408
+ delete msg2.tool_calls;
1114
2409
  }
1115
2410
  if (!sawReal) {
1116
2411
  choice.finish_reason = "stop";
@@ -1118,16 +2413,132 @@ ${note}` : note;
1118
2413
  return body;
1119
2414
  }
1120
2415
 
2416
+ // src/stream-responses.ts
2417
+ var TEXT_PROTOCOL2 = process.env.ACP_COMPRESS_PROTOCOL === "text";
2418
+ function rewriteResponsesJsonResponse(body, ctx) {
2419
+ if (!body || typeof body !== "object") return body;
2420
+ const b = body;
2421
+ if (!Array.isArray(b.output)) return body;
2422
+ let converted = false;
2423
+ let sawReal = false;
2424
+ const noteParts = [];
2425
+ const keep = [];
2426
+ for (const item of b.output) {
2427
+ if (item.type === "function_call" && item.name === COMPRESS_TOOL_NAME) {
2428
+ converted = true;
2429
+ noteParts.push(applyRanges(parseCompressInput(safeJsonParse(String(item.arguments ?? ""))), ctx));
2430
+ } else {
2431
+ if (item.type === "function_call") sawReal = true;
2432
+ keep.push(item);
2433
+ }
2434
+ }
2435
+ if (!converted) return body;
2436
+ const note = noteParts.join("\n");
2437
+ if (note) {
2438
+ keep.unshift({
2439
+ type: "message",
2440
+ role: "assistant",
2441
+ content: [{ type: "output_text", text: note }]
2442
+ });
2443
+ }
2444
+ b.output = keep;
2445
+ if (!sawReal) b.status = "completed";
2446
+ return body;
2447
+ }
2448
+
2449
+ // src/stream-error.ts
2450
+ function safeWrite(res, chunk) {
2451
+ try {
2452
+ res.write(chunk);
2453
+ } catch {
2454
+ }
2455
+ }
2456
+ function emitStreamError(res, protocol, message, log) {
2457
+ const visible = `
2458
+ \u274C [ACP] stream error: ${message}`;
2459
+ log?.(`[acp-proxy: stream aborted mid-response: ${message}]`);
2460
+ try {
2461
+ if (protocol === "openai") {
2462
+ safeWrite(res, `data: ${JSON.stringify({ choices: [{ index: 0, delta: { content: visible }, finish_reason: null }] })}
2463
+
2464
+ `);
2465
+ safeWrite(res, `data: ${JSON.stringify({ choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}
2466
+
2467
+ `);
2468
+ safeWrite(res, "data: [DONE]\n\n");
2469
+ } else if (protocol === "responses") {
2470
+ safeWrite(res, `event: response.output_text.delta
2471
+ data: ${JSON.stringify({ type: "response.output_text.delta", delta: visible })}
2472
+
2473
+ `);
2474
+ safeWrite(res, `event: response.completed
2475
+ data: ${JSON.stringify({ type: "response.completed", response: { status: "completed", output: [] } })}
2476
+
2477
+ `);
2478
+ } else {
2479
+ safeWrite(res, `event: content_block_delta
2480
+ data: ${JSON.stringify({ type: "content_block_delta", delta: { type: "text_delta", text: visible } })}
2481
+
2482
+ `);
2483
+ safeWrite(res, `event: message_delta
2484
+ data: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn" } })}
2485
+
2486
+ `);
2487
+ safeWrite(res, `event: message_stop
2488
+ data: ${JSON.stringify({ type: "message_stop" })}
2489
+
2490
+ `);
2491
+ }
2492
+ } catch {
2493
+ } finally {
2494
+ try {
2495
+ res.end();
2496
+ } catch {
2497
+ }
2498
+ }
2499
+ }
2500
+
2501
+ // src/session-id.ts
2502
+ function extractKey(headers) {
2503
+ const auth = headers["authorization"];
2504
+ if (typeof auth === "string" && auth.length > 0) return auth.trim().toLowerCase();
2505
+ const apiKey = headers["x-api-key"];
2506
+ if (typeof apiKey === "string" && apiKey.length > 0) return `key:${apiKey.trim().toLowerCase()}`;
2507
+ return "(no-key)";
2508
+ }
2509
+ function clientConversationHeader(headers) {
2510
+ const names = ["x-session-affinity", "x-acp-session", "x-session-id", "x-opencode-session"];
2511
+ for (const name of names) {
2512
+ const v = headers[name];
2513
+ if (typeof v === "string" && v.trim().length > 0) return v.trim();
2514
+ }
2515
+ return void 0;
2516
+ }
2517
+ function deriveSessionId(headers, protocol, upstream, conversation) {
2518
+ if (!conversation) throw new Error("deriveSessionId: conversation dimension is required (pass the conversationSignal* output)");
2519
+ const key = extractKey(headers);
2520
+ return hashId(`${protocol}|${upstream}|${key}|${conversation}`);
2521
+ }
2522
+ function affinityToken(headers, conversation) {
2523
+ const client = clientConversationHeader(headers);
2524
+ if (client) return client;
2525
+ return `ses_${conversation}`;
2526
+ }
2527
+
1121
2528
  // src/server.ts
1122
2529
  var UPSTREAM_HOP_HEADERS = /* @__PURE__ */ new Set([
1123
2530
  "host",
1124
2531
  "content-length",
1125
2532
  "connection",
1126
2533
  "keep-alive",
1127
- "transfer-encoding"
2534
+ "transfer-encoding",
2535
+ // Node's fetch transparently decodes compressed responses. Do not
2536
+ // forward the upstream encoding marker when the body is rewritten or
2537
+ // streamed from fetch, otherwise clients try to decompress plain bytes.
2538
+ "content-encoding"
1128
2539
  ]);
1129
- function resolveUpstream(opts2, reqUrl) {
1130
- const names = Object.keys(opts2.routes);
2540
+ function resolveUpstream(opts, reqUrl) {
2541
+ const names = Object.keys(opts.routes);
1131
2542
  if (names.length === 0) return void 0;
1132
2543
  const RESERVED = /* @__PURE__ */ new Set(["v1", "v2", "v4", "chat", "completions", "messages", "models", "api"]);
1133
2544
  const sorted = [...names].sort((a, b) => b.length - a.length);
@@ -1137,75 +2548,165 @@ function resolveUpstream(opts2, reqUrl) {
1137
2548
  if (RESERVED.has(name.toLowerCase())) continue;
1138
2549
  const idx = segments.indexOf(name);
1139
2550
  if (idx < 0) continue;
1140
- const base = opts2.routes[name].replace(/\/$/, "");
2551
+ const base = opts.routes[name].url.replace(/\/$/, "");
1141
2552
  const rest = [...segments.slice(0, idx), ...segments.slice(idx + 1)].join("/");
1142
2553
  const rewrittenUrl = base + rest;
1143
2554
  return { upstream: base, rewrittenUrl, provider: name };
1144
2555
  }
1145
2556
  return void 0;
1146
2557
  }
1147
- function startServer(opts2) {
2558
+ async function startServer(opts) {
1148
2559
  const core = createCore();
1149
- const config = opts2.kernelConfig;
1150
- const log = (level, msg) => logMsg(opts2, level, msg);
1151
- const server2 = http.createServer(async (req, res) => {
2560
+ const config = opts.kernelConfig;
2561
+ const log = (level, msg2) => logMsg(opts, level, msg2);
2562
+ await initSessions();
2563
+ log("info", `[persist] ${getStore().enabled ? "enabled" : "disabled"}`);
2564
+ const server = http.createServer(async (req, res) => {
1152
2565
  try {
1153
- await handle(req, res, opts2, core, config, log);
2566
+ await handle(req, res, opts, core, config, log);
1154
2567
  } catch (err) {
1155
- log("error", String(err));
2568
+ const msg2 = String(err);
2569
+ log("error", msg2);
1156
2570
  if (!res.headersSent) {
1157
- res.writeHead(502, { "content-type": "application/json" });
1158
- res.end(JSON.stringify({ error: "acp-proxy failure", detail: String(err) }));
2571
+ const status = msg2.includes("exceeds") ? 413 : 502;
2572
+ res.writeHead(status, { "content-type": "application/json" });
2573
+ res.end(JSON.stringify({ error: "acp-proxy failure", detail: msg2 }));
1159
2574
  } else {
1160
2575
  res.end();
1161
2576
  }
1162
2577
  }
1163
2578
  });
1164
- server2.listen(opts2.port, opts2.host, () => {
2579
+ server.listen(opts.port, opts.host, () => {
1165
2580
  log(
1166
2581
  "info",
1167
- `acp-proxy listening on http://${opts2.host}:${opts2.port}` + (Object.keys(opts2.routes).length ? ` \u2014 routes: ${Object.entries(opts2.routes).map(([n, u]) => `${n}=${u}`).join(", ")}` : ` \u2192 ${opts2.upstream}`)
2582
+ `acp-proxy listening on http://${opts.host}:${opts.port}` + (Object.keys(opts.routes).length ? ` \u2014 routes: ${Object.entries(opts.routes).map(([n, u]) => `${n}=${typeof u === "string" ? u : u.url}`).join(", ")}` : ` \u2192 ${opts.upstream}`)
1168
2583
  );
1169
2584
  });
1170
- return server2;
2585
+ let shuttingDown = false;
2586
+ const shutdown = (sig) => {
2587
+ if (shuttingDown) return;
2588
+ shuttingDown = true;
2589
+ log("info", `${sig} received \u2014 flushing sessions\u2026`);
2590
+ server.close();
2591
+ void flushAllSessions().finally(() => {
2592
+ process.exit(0);
2593
+ });
2594
+ };
2595
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
2596
+ process.on("SIGINT", () => shutdown("SIGINT"));
2597
+ return server;
2598
+ }
2599
+ function applyCondense(messages, opts, session) {
2600
+ const condenseOpts = {
2601
+ enabled: opts.condense.enabled,
2602
+ keepRecent: opts.condense.keepRecentToolResults,
2603
+ minChars: opts.condense.minCharsToCondense,
2604
+ maxKeptChars: opts.condense.maxKeptChars
2605
+ };
2606
+ const { messages: out, condensedCount, charsSaved } = condenseOldToolResults(messages, condenseOpts);
2607
+ if (condensedCount > 0) {
2608
+ session.condensedToolResults += condensedCount;
2609
+ session.tokensSaved += Math.ceil(charsSaved / 4);
2610
+ }
2611
+ return out;
1171
2612
  }
1172
- async function handle(req, res, opts2, core, config, log) {
2613
+ async function handle(req, res, opts, core, config, log) {
1173
2614
  if (req.method === "GET" && req.url === "/__acp/stats") return sendStats(res);
1174
2615
  if (req.method === "GET" && (req.url === "/" || req.url === "/__acp/health")) {
1175
2616
  res.writeHead(200, { "content-type": "application/json" });
1176
- res.end(JSON.stringify({ ok: true, upstream: opts2.upstream }));
2617
+ res.end(JSON.stringify({ ok: true, upstream: opts.upstream }));
2618
+ return;
2619
+ }
2620
+ let bodyBuffer;
2621
+ try {
2622
+ bodyBuffer = await readBody(req);
2623
+ } catch (err) {
2624
+ if (err instanceof BodyTooLargeError) {
2625
+ log("warn", `413: request body exceeds ${err.limit} bytes`);
2626
+ res.writeHead(413, { "content-type": "application/json" });
2627
+ res.end(JSON.stringify({ error: { type: "request_too_large", message: err.message } }));
2628
+ return;
2629
+ }
2630
+ log("warn", `read body failed: ${String(err)}`);
2631
+ res.writeHead(400, { "content-type": "application/json" });
2632
+ res.end(JSON.stringify({ error: { type: "invalid_request", message: String(err) } }));
1177
2633
  return;
1178
2634
  }
1179
- const bodyBuffer = await readBody(req);
1180
2635
  const url = req.url ?? "";
1181
- const protocol = req.method === "POST" && bodyBuffer.length > 0 ? url.endsWith("/chat/completions") ? "openai" : url.endsWith("/v1/messages") || url.endsWith("/messages") ? "anthropic" : null : null;
1182
- let reqConfig = config;
2636
+ const urlPath = url.split("?", 2)[0];
2637
+ const protocol = req.method === "POST" && bodyBuffer.length > 0 ? urlPath.endsWith("/chat/completions") ? "openai" : urlPath.endsWith("/v1/messages") || urlPath.endsWith("/messages") ? "anthropic" : urlPath.endsWith("/responses") ? "responses" : null : null;
2638
+ const route = resolveUpstream(opts, req.url ?? "");
2639
+ const upstreamOrigin = route ? route.upstream : opts.upstream;
2640
+ let parsed = null;
1183
2641
  if (protocol && bodyBuffer.length > 0) {
1184
- const m = bodyBuffer.toString("utf8").match(/"model"\s*:\s*"([^"]+)"/);
1185
- if (m) {
1186
- const limit = lookupContextLimit(m[1]);
2642
+ try {
2643
+ parsed = JSON.parse(bodyBuffer.toString("utf8"));
2644
+ } catch {
2645
+ parsed = null;
2646
+ }
2647
+ }
2648
+ let reqConfig = config;
2649
+ if (parsed && typeof parsed === "object") {
2650
+ const model = parsed.model;
2651
+ if (model) {
2652
+ const limit = resolveContextLimit(opts.routes, route?.provider, model);
1187
2653
  if (limit && limit !== config.modelContextLimit) {
1188
2654
  reqConfig = { ...config, modelContextLimit: limit };
1189
2655
  }
1190
2656
  }
1191
2657
  }
1192
- const prepared = opts2.passthrough ? null : protocol === "anthropic" ? prepareAnthropic(bodyBuffer, req, opts2, core, reqConfig, log) : protocol === "openai" ? prepareOpenai(bodyBuffer, req, opts2, core, reqConfig, log) : null;
1193
- const outBody = prepared ? prepared.body : bodyBuffer;
1194
- await forward(req, res, opts2, outBody, prepared, core, reqConfig, log);
2658
+ let prepared = null;
2659
+ if (!opts.passthrough && protocol && parsed && typeof parsed === "object") {
2660
+ const sessionHeader = headerValue(req, opts.sessionHeader);
2661
+ const conversation = protocol === "anthropic" ? conversationSignalAnthropic(parsed, sessionHeader) : protocol === "openai" ? conversationSignalOpenai(parsed, sessionHeader) : conversationSignalResponses(parsed, sessionHeader);
2662
+ const sessionId = deriveSessionId(req.headers, protocol, upstreamOrigin, conversation);
2663
+ const session = getSession(sessionId, { protocol, upstreamOrigin });
2664
+ const affinity = affinityToken(req.headers, conversation);
2665
+ await withSessionLock(session, async () => {
2666
+ prepared = protocol === "anthropic" ? prepareAnthropic(parsed, req, opts, core, reqConfig, log, session) : protocol === "openai" ? prepareOpenai(parsed, req, opts, core, reqConfig, log, session) : prepareResponses(parsed, req, opts, core, reqConfig, log, session);
2667
+ acquireInFlight(session);
2668
+ try {
2669
+ await forward(req, res, opts, prepared.body, prepared, core, reqConfig, log, route, affinity);
2670
+ } finally {
2671
+ releaseInFlight(session);
2672
+ }
2673
+ });
2674
+ }
2675
+ if (!prepared) {
2676
+ if (protocol === null && !opts.passthrough) {
2677
+ log("warn", `unrecognized path ${url} \u2014 not a known protocol (/chat/completions, /v1/messages, /responses); forwarding unchanged`);
2678
+ }
2679
+ await forward(req, res, opts, bodyBuffer, null, core, reqConfig, log, route, void 0);
2680
+ }
1195
2681
  }
1196
- var ACP_TAG_RE = /^\x3cacp [^>]*\x3e[^\x3c]*\x3c\/acp\x3e\n?/;
1197
- function stripToolTags(messages) {
2682
+ var ACP_TAG_MARK = "<acp ";
2683
+ function diagTagSummary(messages, sessionId, strategy) {
2684
+ let textTagged = 0;
2685
+ let toolTagged = 0;
1198
2686
  for (const m of messages) {
1199
- if (m.contentType === "tool-call" || m.contentType === "tool-result") {
1200
- m.text = (m.text ?? "").replace(ACP_TAG_RE, "");
1201
- }
2687
+ const hasTag = (m.text ?? "").includes(ACP_TAG_MARK);
2688
+ if (!hasTag) continue;
2689
+ if (m.contentType === "tool-call" || m.contentType === "tool-result") toolTagged++;
2690
+ else textTagged++;
1202
2691
  }
2692
+ return `[${sessionId}] processTurn: ${messages.length} msgs, renderTags=${strategy}, ${textTagged} text tagged, ${toolTagged} tool tagged (should be 0 with text-only)`;
1203
2693
  }
1204
- function prepareAnthropic(bodyBuffer, req, opts2, core, config, log) {
1205
- const parsed = JSON.parse(bodyBuffer.toString("utf8"));
2694
+ function diagNudge(turn, sessionId, tokenCount, limit) {
2695
+ const n = turn.nudge;
2696
+ if (!n) return `[${sessionId}] nudge: unavailable`;
2697
+ const b = n.breakdown ?? {};
2698
+ const pct = limit > 0 ? `${Math.round(tokenCount / limit * 100)}%` : "?";
2699
+ const growth = b["growth"] ?? 0;
2700
+ const floor = b["growthFloor"] ?? 0;
2701
+ const interval = b["nudgeGrowthTokens"] ?? 0;
2702
+ const pendingT1 = b["pendingT1"] ?? 0;
2703
+ const ref = b["growthReference"] ?? 0;
2704
+ const inject = n.shouldInject ? `INJECT T${n.tier ?? "?"}` : "idle";
2705
+ return `[${sessionId}] nudge ${inject}: usage=${pct} (${tokenCount}/${limit}), growth=${growth}/${floor} (ref=${ref}, interval=${interval}), pendingT1=${pendingT1}/${interval}, reason="${n.reason.slice(0, 120)}"`;
2706
+ }
2707
+ function prepareAnthropic(parsed, req, opts, core, config, log, session) {
2708
+ const sessionId = session.id;
1206
2709
  const stream = parsed.stream === true;
1207
- const sessionId = deriveSessionId(parsed, headerValue(req, opts2.sessionHeader));
1208
- const session = getSession(sessionId);
1209
2710
  session.requests++;
1210
2711
  let processedMessages = [];
1211
2712
  let rebuiltMessages = parsed.messages;
@@ -1213,74 +2714,148 @@ function prepareAnthropic(bodyBuffer, req, opts2, core, config, log) {
1213
2714
  let toolsOut = parsed.tools;
1214
2715
  try {
1215
2716
  const { msgs } = anthropicToCore(parsed);
1216
- const tokenCount = estimateTokensFast2(msgs.map((m) => m.text ?? "").join("\n"));
1217
- const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount });
2717
+ const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
2718
+ const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
1218
2719
  session.state = turn.state;
1219
- stripToolTags(turn.messages);
1220
- processedMessages = turn.messages;
1221
- rebuiltMessages = coreToAnthropic(turn.messages);
1222
- systemOut = injectSystem(parsed, turn.nudge, opts2);
1223
- if (opts2.compress.injectTool) {
2720
+ log("info", diagTagSummary(turn.messages, sessionId, "text-only"));
2721
+ log("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
2722
+ processedMessages = applyCondense(turn.messages, opts, session);
2723
+ reapOrphanBlocks(session, msgs, deactivateBlock4);
2724
+ rebuiltMessages = coreToAnthropic(processedMessages);
2725
+ systemOut = injectSystem(parsed, opts);
2726
+ if (opts.compress.injectTool) {
1224
2727
  toolsOut = injectTool(parsed.tools);
1225
2728
  }
2729
+ if (turn.nudge?.shouldInject) {
2730
+ try {
2731
+ const rendered = renderNudgeText(turn.nudge);
2732
+ if (rendered.text) {
2733
+ rebuiltMessages = [...rebuiltMessages, { role: "user", content: rendered.text }];
2734
+ }
2735
+ } catch {
2736
+ }
2737
+ }
1226
2738
  } catch (err) {
1227
2739
  log("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
1228
2740
  processedMessages = [];
1229
2741
  }
1230
2742
  const rebuilt = { ...parsed, messages: rebuiltMessages, system: systemOut, tools: toolsOut };
1231
- return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "anthropic", stream, compressInjected: opts2.compress.injectTool };
2743
+ markDirty(session);
2744
+ return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "anthropic", stream, compressInjected: opts.compress.injectTool };
1232
2745
  }
1233
- function prepareOpenai(bodyBuffer, req, opts2, core, config, log) {
1234
- const parsed = JSON.parse(bodyBuffer.toString("utf8"));
2746
+ function prepareOpenai(parsed, req, opts, core, config, log, session) {
2747
+ const sessionId = session.id;
1235
2748
  const stream = parsed.stream === true;
1236
- const sessionId = deriveSessionIdOpenai(parsed, headerValue(req, opts2.sessionHeader));
1237
- const session = getSession(sessionId);
1238
2749
  session.requests++;
1239
2750
  let processedMessages = [];
1240
2751
  let rebuiltMessages = parsed.messages;
1241
2752
  let toolsOut = parsed.tools;
1242
2753
  const maxTokens = typeof parsed.max_tokens === "number" ? parsed.max_tokens : 8192;
1243
2754
  const isTitleGen = maxTokens <= 200 || parsed.messages.length <= 2;
1244
- const shouldInject = opts2.compress.injectTool && !isTitleGen;
2755
+ const shouldInject = opts.compress.injectTool && !isTitleGen;
1245
2756
  try {
1246
2757
  const { msgs } = openaiToCore(parsed);
1247
- const tokenCount = estimateTokensFast2(msgs.map((m) => m.text ?? "").join("\n"));
1248
- const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount });
2758
+ const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
2759
+ const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
1249
2760
  session.state = turn.state;
1250
- stripToolTags(turn.messages);
1251
- processedMessages = turn.messages;
1252
- rebuiltMessages = coreToOpenai(turn.messages);
2761
+ log("info", diagTagSummary(turn.messages, sessionId, "text-only"));
2762
+ log("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
2763
+ processedMessages = applyCondense(turn.messages, opts, session);
2764
+ reapOrphanBlocks(session, msgs, deactivateBlock4);
2765
+ rebuiltMessages = coreToOpenai(processedMessages);
1253
2766
  const sysParts = [];
1254
2767
  if (shouldInject) sysParts.push(buildCompressSystemPrompt());
2768
+ rebuiltMessages = injectOpenaiSystem(rebuiltMessages, sysParts);
2769
+ if (shouldInject) {
2770
+ toolsOut = injectOpenaiTool(parsed.tools);
2771
+ }
1255
2772
  if (turn.nudge?.shouldInject && shouldInject) {
1256
2773
  try {
1257
2774
  const rendered = renderNudgeText(turn.nudge);
1258
- if (rendered.text) sysParts.push(rendered.text);
2775
+ if (rendered.text) {
2776
+ rebuiltMessages = [...rebuiltMessages, { role: "user", content: rendered.text }];
2777
+ }
1259
2778
  } catch {
1260
2779
  }
1261
2780
  }
1262
- rebuiltMessages = injectOpenaiSystem(rebuiltMessages, sysParts);
1263
- if (shouldInject) {
1264
- toolsOut = injectOpenaiTool(parsed.tools);
1265
- }
1266
2781
  } catch (err) {
1267
2782
  log("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
1268
2783
  processedMessages = [];
1269
2784
  }
1270
2785
  const rebuilt = { ...parsed, messages: rebuiltMessages, tools: toolsOut };
2786
+ markDirty(session);
1271
2787
  return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "openai", stream, compressInjected: shouldInject };
1272
2788
  }
1273
- function injectSystem(parsed, nudge, opts2) {
1274
- const baseText = extractSystem(parsed.system);
1275
- const parts = [];
1276
- if (opts2.compress.injectTool) parts.push(buildCompressSystemPrompt());
1277
- if (nudge?.shouldInject) {
1278
- try {
1279
- const rendered = renderNudgeText(nudge);
1280
- if (rendered.text) parts.push(rendered.text);
1281
- } catch {
2789
+ function prepareResponses(parsed, req, opts, core, config, log, session) {
2790
+ const sessionId = session.id;
2791
+ const stream = parsed.stream === true;
2792
+ session.requests++;
2793
+ let processedMessages = [];
2794
+ let rebuiltInput = parsed.input;
2795
+ let toolsOut = parsed.tools;
2796
+ const shouldInject = opts.compress.injectTool;
2797
+ try {
2798
+ const { msgs, systemParts, preamble, customToolCallIds } = responsesToCore(parsed);
2799
+ if (process.env.ACP_DEBUG) {
2800
+ log("info", `[${sessionId}] input items: ${Array.isArray(parsed.input) ? parsed.input.map((i) => i.type).join(",") : "(string)"}`);
1282
2801
  }
2802
+ const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
2803
+ const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: process.env.ACP_RENDER_NONE ? "none" : "text-only" });
2804
+ session.state = turn.state;
2805
+ log("info", diagTagSummary(turn.messages, sessionId, "text-only"));
2806
+ log("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
2807
+ processedMessages = applyCondense(turn.messages, opts, session);
2808
+ reapOrphanBlocks(session, msgs, deactivateBlock4);
2809
+ const conversationItems = coreToResponses(processedMessages, customToolCallIds);
2810
+ if (preamble.length > 0) {
2811
+ log("info", `[${sessionId}] preserved ${preamble.length} opaque preamble item(s): ${preamble.map((p) => p.type).join(",")}`);
2812
+ }
2813
+ const inputItems = [...preamble];
2814
+ if (shouldInject && !process.env.ACP_NO_COMPRESS_PROMPT) {
2815
+ const sysParts = [...systemParts, buildCompressSystemPrompt()];
2816
+ inputItems.push({ type: "message", role: "developer", content: sysParts.join("\n\n---\n\n") });
2817
+ if (!process.env.ACP_NO_INJECT_TOOL) {
2818
+ toolsOut = injectResponsesTool(parsed.tools);
2819
+ }
2820
+ } else if (systemParts.length > 0) {
2821
+ inputItems.push({ type: "message", role: "developer", content: systemParts.join("\n\n---\n\n") });
2822
+ }
2823
+ inputItems.push(...conversationItems);
2824
+ if (process.env.ACP_DEBUG) {
2825
+ const ctcs = conversationItems.filter((i) => i.type === "custom_tool_call").length;
2826
+ const ctcos = conversationItems.filter((i) => i.type === "custom_tool_call_output").length;
2827
+ log("info", `[${sessionId}] rebuilt: msgs=${msgs.length} -> conv=${conversationItems.length} (custom_tool_call=${ctcs} custom_tool_call_output=${ctcos})`);
2828
+ }
2829
+ if (turn.nudge?.shouldInject && shouldInject) {
2830
+ try {
2831
+ const rendered = renderNudgeText(turn.nudge);
2832
+ if (rendered.text) {
2833
+ inputItems.push({ type: "message", role: "user", content: rendered.text });
2834
+ }
2835
+ } catch {
2836
+ }
2837
+ }
2838
+ rebuiltInput = inputItems;
2839
+ } catch (err) {
2840
+ log("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
2841
+ processedMessages = [];
1283
2842
  }
2843
+ const rebuilt = { ...parsed, input: rebuiltInput, tools: toolsOut };
2844
+ if (process.env.ACP_DEBUG) {
2845
+ const fwdTools = (Array.isArray(toolsOut) ? toolsOut : []).map((t) => {
2846
+ const r = t;
2847
+ const sub = Array.isArray(r.tools) ? `(${r.tools.length} sub)` : "";
2848
+ return `${r.type}:${r.name ?? "?"}${sub}`;
2849
+ });
2850
+ log("info", `[${sessionId}] responses forward tools=[${fwdTools.join(",")}] injectTool=${shouldInject} NO_INJECT_TOOL=${!!process.env.ACP_NO_INJECT_TOOL} NO_COMPRESS_PROMPT=${!!process.env.ACP_NO_COMPRESS_PROMPT}`);
2851
+ }
2852
+ markDirty(session);
2853
+ return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "responses", stream, compressInjected: shouldInject };
2854
+ }
2855
+ function injectSystem(parsed, opts) {
2856
+ const baseText = extractSystem(parsed.system);
2857
+ const parts = [];
2858
+ if (opts.compress.injectTool) parts.push(buildCompressSystemPrompt());
1284
2859
  if (parts.length === 0) return parsed.system;
1285
2860
  const full = baseText ? `${baseText}
1286
2861
 
@@ -1302,21 +2877,44 @@ function injectOpenaiTool(tools) {
1302
2877
  const additions = ACP_TOOLS_OPENAI.filter((t) => !present.has(t.function.name));
1303
2878
  return [...tools, ...additions];
1304
2879
  }
1305
- async function forward(req, res, opts2, body, prepared, core, config, log) {
1306
- const route = resolveUpstream(opts2, req.url ?? "");
1307
- const upstreamUrl = route ? route.rewrittenUrl : opts2.upstream + (req.url ?? "");
2880
+ var TEXT_PROTOCOL3 = process.env.ACP_COMPRESS_PROTOCOL === "text";
2881
+ function injectResponsesTool(tools) {
2882
+ if (!Array.isArray(tools)) return [...ACP_TOOLS_RESPONSES];
2883
+ const present = new Set(
2884
+ tools.map((t) => t?.name).filter((n) => typeof n === "string")
2885
+ );
2886
+ const additions = ACP_TOOLS_RESPONSES.filter((t) => !present.has(t.name));
2887
+ return [...tools, ...additions];
2888
+ }
2889
+ async function forward(req, res, opts, body, prepared, core, config, log, route, affinity) {
2890
+ const upstreamUrl = route ? route.rewrittenUrl : opts.upstream + (req.url ?? "");
1308
2891
  log("info", `forward ${req.method} ${req.url ?? ""} \u2192 ${upstreamUrl}${route ? ` (${route.provider})` : ""}`);
1309
- if (opts2.debug && typeof body === "string") {
2892
+ if (process.env.ACP_DEBUG && prepared) {
2893
+ const sid = prepared.session.id;
2894
+ const hdrKeys = Object.keys(req.headers);
2895
+ log("info", `[${sid}] client headers: ${hdrKeys.join(",")}`);
2896
+ for (const k of ["authorization", "x-api-key", "x-session-id", "x-session-affinity", "x-acp-session", "x-opencode-session", "prompt-cache-key", "anthropic-beta"]) {
2897
+ const v = req.headers[k] ?? req.headers[k.toLowerCase()];
2898
+ if (v) {
2899
+ const s = Array.isArray(v) ? v.join(",") : String(v);
2900
+ const masked = /key|auth|token/i.test(k) ? s.slice(0, 8) + "..." + s.slice(-4) + ` (${s.length} chars)` : s.slice(0, 60);
2901
+ log("info", `[${sid}] client hdr ${k}=${masked}`);
2902
+ }
2903
+ }
2904
+ }
2905
+ if (opts.debug && typeof body === "string") {
1310
2906
  try {
1311
2907
  const parsed = JSON.parse(body);
1312
2908
  const toolNames = (parsed.tools ?? []).map((t) => {
1313
2909
  const fn = t.function;
1314
- return fn?.name ?? "?";
2910
+ return fn?.name ?? t.name ?? "?";
1315
2911
  });
1316
2912
  log("info", `[debug] tools=[${toolNames.join(",")}] msgs=${parsed.messages?.length ?? 0} stream=${parsed.stream ?? false} system_len=${JSON.stringify(parsed.messages?.find((m) => m.role === "system")?.content ?? "").length}`);
1317
- const out = `/tmp/acp-proxy-debug-req-${Date.now()}.json`;
1318
- fs.writeFileSync(out, body.slice(0, 5e4));
1319
- log("info", `[debug] forwarded body written to ${out}`);
2913
+ if (process.env.ACP_DUMP_REQ === "1") {
2914
+ const out = `/tmp/acp-proxy-debug-req-${Date.now()}.json`;
2915
+ fs2.writeFileSync(out, body.slice(0, 5e4));
2916
+ log("info", `[debug] forwarded body written to ${out}`);
2917
+ }
1320
2918
  } catch {
1321
2919
  }
1322
2920
  }
@@ -1325,26 +2923,37 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
1325
2923
  if (UPSTREAM_HOP_HEADERS.has(k.toLowerCase()) || v === void 0) continue;
1326
2924
  headers[k] = Array.isArray(v) ? v.join(", ") : v;
1327
2925
  }
1328
- headers["host"] = new URL(route ? route.upstream : opts2.upstream).host;
2926
+ headers["host"] = new URL(route ? route.upstream : opts.upstream).host;
2927
+ if (affinity && !clientConversationHeader(req.headers)) {
2928
+ headers["x-session-id"] = affinity;
2929
+ }
1329
2930
  const init = {
1330
2931
  method: req.method ?? "GET",
1331
2932
  headers,
1332
2933
  body: req.method === "GET" || req.method === "HEAD" ? void 0 : body
1333
2934
  };
1334
- const upstream = await fetch(upstreamUrl, init);
2935
+ const { response: upstream, clearTimer: clearUpstreamTimer } = await fetchWithTimeout(upstreamUrl, init);
1335
2936
  const respHeaders = {};
1336
2937
  upstream.headers.forEach((v, k) => {
1337
2938
  if (UPSTREAM_HOP_HEADERS.has(k.toLowerCase())) return;
1338
2939
  respHeaders[k] = v;
1339
2940
  });
2941
+ if (!upstream.ok) {
2942
+ res.writeHead(upstream.status, respHeaders);
2943
+ if (upstream.body) await pipeThrough(upstream.body, res);
2944
+ clearUpstreamTimer();
2945
+ return;
2946
+ }
1340
2947
  res.writeHead(upstream.status, respHeaders);
1341
2948
  if (!upstream.body) {
1342
2949
  res.end();
2950
+ clearUpstreamTimer();
1343
2951
  return;
1344
2952
  }
1345
- const useRewriter = prepared !== null && prepared.processedMessages.length > 0 && opts2.compress.injectTool;
2953
+ const useRewriter = prepared !== null && prepared.compressInjected && prepared.processedMessages.length > 0;
1346
2954
  if (!useRewriter || prepared === null) {
1347
2955
  await pipeThrough(upstream.body, res);
2956
+ clearUpstreamTimer();
1348
2957
  return;
1349
2958
  }
1350
2959
  const ctx = {
@@ -1352,50 +2961,83 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
1352
2961
  config,
1353
2962
  messages: prepared.processedMessages,
1354
2963
  session: prepared.session,
1355
- log: (msg) => log("info", `[${prepared.session.id}] ${msg}`),
1356
- debug: opts2.debug
2964
+ log: (msg2) => log("info", `[${prepared.session.id}] ${msg2}`),
2965
+ debug: opts.debug
1357
2966
  };
1358
2967
  if (prepared.stream) {
1359
2968
  let streamToRead = upstream.body;
1360
2969
  let dumpRaw;
1361
- if (opts2.dumpSse) {
2970
+ if (opts.dumpSse) {
1362
2971
  const [a, b] = upstream.body.tee();
1363
2972
  streamToRead = a;
1364
- dumpRaw = dumpStreamToFile(b, opts2.dumpSse, `${Date.now()}-${prepared.session.id}-raw.sse`);
1365
- }
1366
- const ctx2 = {
1367
- core,
1368
- config,
1369
- messages: prepared.processedMessages,
1370
- session: prepared.session,
1371
- log: (msg) => log("info", `[${prepared.session.id}] ${msg}`),
1372
- debug: opts2.debug
1373
- };
1374
- if (prepared.protocol === "openai" && prepared.compressInjected) {
1375
- const parsedReq = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
1376
- const reqHeaders = {};
1377
- for (const [k, v] of Object.entries(headers)) {
1378
- if (k.toLowerCase() === "content-length" || k.toLowerCase() === "host") continue;
1379
- reqHeaders[k] = v;
1380
- }
1381
- reqHeaders["content-type"] = "application/json";
1382
- const loop = compressLoopStream(
1383
- streamToRead,
1384
- { core, config, messages: prepared.processedMessages, session: prepared.session, log: ctx2.log },
1385
- parsedReq,
1386
- { url: upstreamUrl, headers: reqHeaders }
1387
- );
1388
- for await (const chunk of loop) {
1389
- if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
1390
- }
1391
- } else {
1392
- const rewriter = rewriteSseStream(streamToRead, ctx2);
1393
- for await (const chunk of rewriter) {
1394
- if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
2973
+ dumpRaw = dumpStreamToFile(b, opts.dumpSse, `${Date.now()}-${prepared.session.id}-raw.sse`);
2974
+ }
2975
+ try {
2976
+ if (prepared.protocol === "openai") {
2977
+ const parsedReq = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
2978
+ const reqHeaders = {};
2979
+ for (const [k, v] of Object.entries(headers)) {
2980
+ if (k.toLowerCase() === "content-length" || k.toLowerCase() === "host") continue;
2981
+ reqHeaders[k] = v;
2982
+ }
2983
+ reqHeaders["content-type"] = "application/json";
2984
+ const loop = compressLoopStream(
2985
+ streamToRead,
2986
+ { core, config, messages: prepared.processedMessages, session: prepared.session, log: ctx.log },
2987
+ parsedReq,
2988
+ { url: upstreamUrl, headers: reqHeaders }
2989
+ );
2990
+ for await (const chunk of loop) {
2991
+ {
2992
+ const s = chunk.toString("utf8");
2993
+ if (s.includes("<acp ") || s.includes("</acp")) {
2994
+ log("warn", `[${prepared.session.id}] tag echo: openai response stream contains <acp tag`);
2995
+ }
2996
+ }
2997
+ if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
2998
+ }
2999
+ } else if (prepared.protocol === "responses") {
3000
+ const parsedReq = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
3001
+ const reqHeaders = {};
3002
+ for (const [k, v] of Object.entries(headers)) {
3003
+ if (k.toLowerCase() === "content-length" || k.toLowerCase() === "host") continue;
3004
+ reqHeaders[k] = v;
3005
+ }
3006
+ reqHeaders["content-type"] = "application/json";
3007
+ const loop = compressLoopResponsesStream(
3008
+ streamToRead,
3009
+ { core, config, messages: prepared.processedMessages, session: prepared.session, log: ctx.log },
3010
+ parsedReq,
3011
+ { url: upstreamUrl, headers: reqHeaders }
3012
+ );
3013
+ for await (const chunk of loop) {
3014
+ {
3015
+ const s = chunk.toString("utf8");
3016
+ if (s.includes("<acp ") || s.includes("</acp")) {
3017
+ log("warn", `[${prepared.session.id}] tag echo: responses response stream contains <acp tag`);
3018
+ }
3019
+ }
3020
+ if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
3021
+ }
3022
+ } else {
3023
+ const rewriter = rewriteSseStream(streamToRead, ctx);
3024
+ for await (const chunk of rewriter) {
3025
+ {
3026
+ const s = chunk.toString("utf8");
3027
+ if (s.includes("<acp ") || s.includes("</acp")) {
3028
+ log("warn", `[${prepared.session.id}] tag echo: anthropic response stream contains <acp tag`);
3029
+ }
3030
+ }
3031
+ if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
3032
+ }
1395
3033
  }
3034
+ res.end();
3035
+ } catch (e) {
3036
+ emitStreamError(res, prepared.protocol, e?.message ?? String(e), (m) => log("error", `[${prepared.session.id}] ${m}`));
3037
+ } finally {
3038
+ clearUpstreamTimer();
3039
+ if (dumpRaw) await dumpRaw;
1396
3040
  }
1397
- res.end();
1398
- if (dumpRaw) await dumpRaw;
1399
3041
  } else {
1400
3042
  const buf = await upstream.arrayBuffer();
1401
3043
  const text = Buffer.from(buf).toString("utf8");
@@ -1403,6 +3045,8 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
1403
3045
  const json = JSON.parse(text);
1404
3046
  if (prepared.protocol === "openai") {
1405
3047
  rewriteOpenaiJsonResponse(json, ctx);
3048
+ } else if (prepared.protocol === "responses") {
3049
+ rewriteResponsesJsonResponse(json, ctx);
1406
3050
  } else {
1407
3051
  rewriteJsonResponse(json, ctx);
1408
3052
  }
@@ -1410,7 +3054,9 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
1410
3054
  } catch {
1411
3055
  res.end(text);
1412
3056
  }
3057
+ clearUpstreamTimer();
1413
3058
  }
3059
+ markDirty(prepared.session);
1414
3060
  }
1415
3061
  async function pipeThrough(stream, res) {
1416
3062
  const reader = stream.getReader();
@@ -1428,11 +3074,11 @@ async function pipeThrough(stream, res) {
1428
3074
  }
1429
3075
  }
1430
3076
  async function dumpStreamToFile(stream, dir, name) {
1431
- const { mkdirSync: mkdirSync2, createWriteStream } = await import("fs");
1432
- const { join: join2 } = await import("path");
3077
+ const { mkdirSync: mkdirSync3, createWriteStream } = await import("fs");
3078
+ const { join: join3 } = await import("path");
1433
3079
  try {
1434
- mkdirSync2(dir, { recursive: true });
1435
- const ws = createWriteStream(join2(dir, name));
3080
+ mkdirSync3(dir, { recursive: true });
3081
+ const ws = createWriteStream(join3(dir, name));
1436
3082
  const reader = stream.getReader();
1437
3083
  try {
1438
3084
  for (; ; ) {
@@ -1465,26 +3111,160 @@ function headerValue(req, name) {
1465
3111
  }
1466
3112
  return void 0;
1467
3113
  }
3114
+ var BodyTooLargeError = class extends Error {
3115
+ constructor(limit) {
3116
+ super(`request body exceeds ${limit} bytes`);
3117
+ this.limit = limit;
3118
+ this.name = "BodyTooLargeError";
3119
+ }
3120
+ limit;
3121
+ };
1468
3122
  function readBody(req) {
1469
3123
  return new Promise((resolve, reject) => {
1470
3124
  const chunks = [];
1471
- req.on("data", (c) => chunks.push(c));
1472
- req.on("end", () => resolve(Buffer.concat(chunks)));
1473
- req.on("error", reject);
3125
+ let size = 0;
3126
+ let aborted = false;
3127
+ req.on("data", (c) => {
3128
+ if (aborted) return;
3129
+ size += c.length;
3130
+ if (size > MAX_REQUEST_BYTES) {
3131
+ aborted = true;
3132
+ reject(new BodyTooLargeError(MAX_REQUEST_BYTES));
3133
+ return;
3134
+ }
3135
+ chunks.push(c);
3136
+ });
3137
+ req.on("end", () => {
3138
+ if (!aborted) resolve(Buffer.concat(chunks));
3139
+ });
3140
+ req.on("error", (e) => {
3141
+ if (!aborted) reject(e);
3142
+ });
1474
3143
  });
1475
3144
  }
1476
- function logMsg(opts2, level, msg) {
1477
- if (!opts2.log) return;
3145
+ function logMsg(opts, level, msg2) {
3146
+ if (!opts.log) return;
1478
3147
  const ts = (/* @__PURE__ */ new Date()).toISOString();
1479
- console.error(`${ts} [${level}] ${msg}`);
3148
+ console.error(`${ts} [${level}] ${msg2}`);
1480
3149
  }
1481
3150
 
1482
- // src/index.ts
1483
- var opts = loadOptions();
1484
- var server = startServer(opts);
1485
- for (const sig of ["SIGINT", "SIGTERM"]) {
1486
- process.on(sig, () => {
1487
- server.close(() => process.exit(0));
1488
- });
3151
+ // src/cli.ts
3152
+ import { readFileSync as readFileSync3 } from "fs";
3153
+ import { fileURLToPath } from "url";
3154
+ import path3 from "path";
3155
+ var VERSION = (() => {
3156
+ try {
3157
+ const here = fileURLToPath(import.meta.url);
3158
+ const pkg = path3.join(path3.dirname(here), "..", "package.json");
3159
+ return JSON.parse(readFileSync3(pkg, "utf8")).version ?? "dev";
3160
+ } catch {
3161
+ return "dev";
3162
+ }
3163
+ })();
3164
+ var HELP = `bili ${VERSION} \u2014 billion-context proxy
3165
+
3166
+ Usage:
3167
+ bili [start] [options] start the proxy (default: reads ${configFile()})
3168
+ bili --version print version
3169
+ bili --help show this help
3170
+
3171
+ Options (override config file / env):
3172
+ --port <N> listen port (default 8787)
3173
+ --host <ADDR> listen host (default 127.0.0.1)
3174
+ --config <FILE> path to config JSON (default: XDG location)
3175
+ --debug verbose logging
3176
+ --passthrough forward without compression
3177
+ --no-passthrough force compression on (overrides config)
3178
+
3179
+ Config: ${configFile()}
3180
+ Set port/host/debug/providers/condense/compress there. See README \xA7Configuration.
3181
+ Env vars (ACP_*, BILI_*) also work and override the file; CLI flags win.
3182
+
3183
+ Docs: https://github.com/ranxianglei/billion-context
3184
+ `;
3185
+ function parseArgs(argv) {
3186
+ const overrides = {};
3187
+ let command = "start";
3188
+ const positional = [];
3189
+ for (let i = 0; i < argv.length; i++) {
3190
+ const a = argv[i];
3191
+ switch (a) {
3192
+ case "--help":
3193
+ case "-h":
3194
+ command = "help";
3195
+ break;
3196
+ case "--version":
3197
+ case "-V":
3198
+ command = "version";
3199
+ break;
3200
+ case "--debug":
3201
+ overrides.ACP_DEBUG = "1";
3202
+ break;
3203
+ case "--passthrough":
3204
+ overrides.ACP_PASSTHROUGH = "1";
3205
+ break;
3206
+ case "--no-passthrough":
3207
+ overrides.ACP_PASSTHROUGH = "0";
3208
+ break;
3209
+ case "--port":
3210
+ case "--host":
3211
+ case "--config": {
3212
+ const val = argv[++i];
3213
+ if (val === void 0) {
3214
+ console.error(`bili: ${a} requires a value`);
3215
+ process.exit(2);
3216
+ }
3217
+ if (a === "--port") overrides.ACP_PORT = val;
3218
+ else if (a === "--host") overrides.ACP_HOST = val;
3219
+ else overrides.BILI_CONFIG_FILE = val;
3220
+ break;
3221
+ }
3222
+ default:
3223
+ if (a.startsWith("--")) {
3224
+ const eq = a.indexOf("=");
3225
+ if (eq > 0) {
3226
+ argv.splice(i, 1, a.slice(0, eq), a.slice(eq + 1));
3227
+ i--;
3228
+ break;
3229
+ }
3230
+ console.error(`bili: unknown option ${a}`);
3231
+ process.exit(2);
3232
+ }
3233
+ positional.push(a);
3234
+ }
3235
+ }
3236
+ if (positional.length > 0) {
3237
+ const cmd = positional[0];
3238
+ if (cmd === "start") {
3239
+ command = command === "help" || command === "version" ? command : "start";
3240
+ } else {
3241
+ console.error(`bili: unknown command "${cmd}" (try "bili --help")`);
3242
+ process.exit(2);
3243
+ }
3244
+ }
3245
+ return { command, overrides };
3246
+ }
3247
+ async function main() {
3248
+ const { command, overrides } = parseArgs(process.argv.slice(2));
3249
+ if (command === "help") {
3250
+ process.stdout.write(HELP);
3251
+ return;
3252
+ }
3253
+ if (command === "version") {
3254
+ process.stdout.write(VERSION + "\n");
3255
+ return;
3256
+ }
3257
+ for (const [k, v] of Object.entries(overrides)) {
3258
+ if (v !== void 0) process.env[k] = v;
3259
+ }
3260
+ const opts = loadOptions();
3261
+ await startServer(opts);
1489
3262
  }
3263
+ main().catch((err) => {
3264
+ console.error("bili: failed to start:", err);
3265
+ process.exit(1);
3266
+ });
3267
+
3268
+ // src/index.ts
3269
+ main();
1490
3270
  //# sourceMappingURL=index.js.map