billion-context 0.1.0 → 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,28 +3,105 @@
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
  }
43
+ var CONTEXT_LIMIT_TABLE = [
44
+ { match: /^claude-/i, limit: 2e5 },
45
+ { match: /^gpt-5/i, limit: 4e5 },
46
+ { match: /^gpt-4\.1/i, limit: 1e6 },
47
+ { match: /^gpt-4o/i, limit: 128e3 },
48
+ { match: /^gpt-4-turbo/i, limit: 128e3 },
49
+ { match: /^o[13]-/i, limit: 2e5 },
50
+ { match: /^gemini-2\.5/i, limit: 1e6 },
51
+ { match: /^gemini-1\.5/i, limit: 1e6 },
52
+ { match: /^glm-4\.6/i, limit: 128e3 },
53
+ { match: /^glm-5/i, limit: 1e6 },
54
+ { match: /^glm-/i, limit: 128e3 },
55
+ { match: /^deepseek/i, limit: 64e3 },
56
+ { match: /^qwen/i, limit: 128e3 },
57
+ { match: /^kimi/i, limit: 128e3 },
58
+ { match: /^llama-/i, limit: 128e3 }
59
+ ];
60
+ function lookupContextLimit(model) {
61
+ if (!model) return void 0;
62
+ for (const entry of CONTEXT_LIMIT_TABLE) {
63
+ if (entry.match.test(model)) return entry.limit;
64
+ }
65
+ return void 0;
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
+ }
13
78
  function loadOptions(env = process.env) {
14
- const port = parseInt(env.ACP_PORT ?? env.PORT ?? "8787", 10);
15
- const host = env.ACP_HOST ?? "127.0.0.1";
16
- const upstream = (env.ACP_UPSTREAM ?? "https://api.anthropic.com").replace(/\/$/, "");
17
- let routes = [];
18
- const routesPath = env.ACP_PROVIDERS ?? "";
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(/\/$/, "");
83
+ let routes = {};
84
+ const routesPath = env.ACP_PROVIDERS ?? fileConfig.providersPath ?? "";
19
85
  if (routesPath) {
20
86
  const parsed = safeReadJson(routesPath);
21
- if (Array.isArray(parsed)) routes = parsed.filter((r) => r && typeof r.baseURL === "string");
87
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
88
+ for (const [k, v] of Object.entries(parsed)) {
89
+ const route = parseRouteEntry(v);
90
+ if (route) routes[k] = route;
91
+ }
92
+ }
93
+ }
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
+ }
22
99
  }
23
- const modelContextLimit = parseInt(env.ACP_MODEL_CONTEXT_LIMIT ?? "200000", 10);
24
- const enabled = (env.ACP_CONDENSE_ENABLED ?? "1") !== "0";
25
- const keepRecentToolResults = parseInt(env.ACP_KEEP_RECENT_TOOL_RESULTS ?? "6", 10);
26
- const minCharsToCondense = parseInt(env.ACP_MIN_CHARS_TO_CONDENSE ?? "1500", 10);
27
- const maxKeptChars = parseInt(env.ACP_MAX_KEPT_CHARS ?? "400", 10);
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);
28
105
  return {
29
106
  port: Number.isFinite(port) ? port : 8787,
30
107
  host,
@@ -34,23 +111,83 @@ function loadOptions(env = process.env) {
34
111
  kernelConfig: defaultConfig(modelContextLimit),
35
112
  condense: { enabled, keepRecentToolResults, minCharsToCondense, maxKeptChars },
36
113
  compress: {
37
- injectTool: (env.ACP_COMPRESS_TOOL ?? "1") !== "0",
38
- 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"
39
116
  },
40
- sessionHeader: env.ACP_SESSION_HEADER ?? "x-acp-session",
41
- log: env.ACP_LOG !== "0",
42
- debug: (env.ACP_DEBUG ?? "0") === "1",
43
- dumpSse: env.ACP_DUMP_SSE || void 0,
44
- 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"
45
122
  };
46
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
+ }
47
141
 
48
142
  // src/server.ts
49
143
  import http from "http";
50
- import fs from "fs";
51
- 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
+ };
52
188
 
53
189
  // src/anthropic.ts
190
+ var CONDENSED_TAG = "[acp-proxy: condensed";
54
191
  function extractSystem(system) {
55
192
  if (!system) return "";
56
193
  if (typeof system === "string") return system;
@@ -65,19 +202,23 @@ function buildSystem(text, original) {
65
202
  }
66
203
  function anthropicToCore(body) {
67
204
  const msgs = [];
68
- let idx = 0;
205
+ const clusters = new ClusterCounter();
69
206
  for (const m of body.messages) {
70
207
  const blocks = typeof m.content === "string" ? [{ type: "text", text: m.content }] : m.content;
71
208
  for (const b of blocks) {
72
- const id = `raw-${idx}`;
73
- idx++;
74
209
  switch (b.type) {
75
- case "text":
76
- 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 });
77
213
  break;
78
- 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
+ });
79
220
  msgs.push({
80
- id,
221
+ id: clusters.next(base),
81
222
  role: "assistant",
82
223
  contentType: "tool-call",
83
224
  toolName: b.name,
@@ -85,10 +226,12 @@ function anthropicToCore(body) {
85
226
  text: safeStringify(b.input)
86
227
  });
87
228
  break;
229
+ }
88
230
  case "tool_result": {
89
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 });
90
233
  msgs.push({
91
- id,
234
+ id: clusters.next(base),
92
235
  role: "tool",
93
236
  contentType: "tool-result",
94
237
  toolCallId: b.tool_use_id,
@@ -96,12 +239,16 @@ function anthropicToCore(body) {
96
239
  });
97
240
  break;
98
241
  }
99
- case "thinking":
100
- 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 });
101
245
  break;
102
- case "image":
103
- 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]" });
104
250
  break;
251
+ }
105
252
  }
106
253
  }
107
254
  }
@@ -149,11 +296,38 @@ function coreToAnthropic(messages) {
149
296
  flush();
150
297
  return out;
151
298
  }
152
- 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) {
153
327
  if (headerValue2 && headerValue2.trim()) return headerValue2.trim();
154
328
  const firstUser = body.messages.find((m) => m.role === "user");
155
329
  const seed = firstUser ? JSON.stringify(firstUser.content).slice(0, 200) : "default";
156
- return hash(seed);
330
+ return hashId(seed);
157
331
  }
158
332
  function safeStringify(v) {
159
333
  try {
@@ -170,62 +344,59 @@ function safeParse(s) {
170
344
  return {};
171
345
  }
172
346
  }
173
- function hash(s) {
174
- let h = 2166136261;
175
- for (let i = 0; i < s.length; i++) {
176
- h ^= s.charCodeAt(i);
177
- h = Math.imul(h, 16777619);
178
- }
179
- return (h >>> 0).toString(36);
180
- }
181
347
 
182
348
  // src/openai.ts
183
349
  function openaiToCore(body) {
184
350
  const msgs = [];
185
- let idx = 0;
351
+ const clusters = new ClusterCounter();
186
352
  for (const m of body.messages) {
187
353
  switch (m.role) {
188
354
  case "system":
189
355
  case "developer": {
190
- msgs.push({ id: `raw-${idx}`, role: "system", contentType: "text", text: stringContent(m.content) });
191
- 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) });
192
358
  break;
193
359
  }
194
360
  case "user": {
195
- msgs.push({ id: `raw-${idx}`, role: "user", contentType: "text", text: stringContent(m.content) });
196
- 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) });
197
363
  break;
198
364
  }
199
365
  case "assistant": {
200
366
  const text = stringContent(m.content);
201
367
  if (text) {
202
- msgs.push({ id: `raw-${idx}`, role: "assistant", contentType: "text", text });
203
- idx++;
368
+ const base = deriveMessageId("assistant", "text", text);
369
+ msgs.push({ id: clusters.next(base), role: "assistant", contentType: "text", text });
204
370
  }
205
371
  if (Array.isArray(m.tool_calls)) {
206
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
+ });
207
377
  msgs.push({
208
- id: `raw-${idx}`,
378
+ id: clusters.next(base),
209
379
  role: "assistant",
210
380
  contentType: "tool-call",
211
381
  toolName: tc.function.name,
212
382
  toolCallId: tc.id,
213
383
  text: tc.function.arguments ?? ""
214
384
  });
215
- idx++;
216
385
  }
217
386
  }
218
387
  break;
219
388
  }
220
389
  case "tool": {
390
+ const base = deriveMessageId("tool", "tool-result", stringContent(m.content), {
391
+ toolCallId: m.tool_call_id ?? ""
392
+ });
221
393
  msgs.push({
222
- id: `raw-${idx}`,
394
+ id: clusters.next(base),
223
395
  role: "tool",
224
396
  contentType: "tool-result",
225
397
  toolCallId: m.tool_call_id ?? "",
226
398
  text: stringContent(m.content)
227
399
  });
228
- idx++;
229
400
  break;
230
401
  }
231
402
  }
@@ -289,11 +460,11 @@ ${extra}` : extra;
289
460
  }
290
461
  return [{ role: "system", content: extra }, ...messages];
291
462
  }
292
- function deriveSessionIdOpenai(body, headerValue2) {
463
+ function conversationSignalOpenai(body, headerValue2) {
293
464
  if (headerValue2 && headerValue2.trim()) return headerValue2.trim();
294
465
  const firstUser = body.messages.find((m) => m.role === "user");
295
466
  const seed = firstUser ? stringContent(firstUser.content).slice(0, 200) : "default";
296
- return hash2(seed);
467
+ return hashId(seed);
297
468
  }
298
469
  function stringContent(content) {
299
470
  if (content == null) return "";
@@ -303,56 +474,623 @@ function stringContent(content) {
303
474
  }
304
475
  return "";
305
476
  }
306
- function hash2(s) {
307
- let h = 2166136261;
308
- for (let i = 0; i < s.length; i++) {
309
- h ^= s.charCodeAt(i);
310
- 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
+ }
311
666
  }
312
- return (h >>> 0).toString(36);
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}`;
673
+ }
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);
313
687
  }
314
688
 
315
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";
316
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
317
985
  var sessions = /* @__PURE__ */ new Map();
318
- var MAX_SESSIONS = 256;
319
- 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) {
320
1005
  const existing = sessions.get(id);
321
1006
  if (existing) {
322
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;
323
1010
  return existing;
324
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
+ }
325
1020
  if (sessions.size >= MAX_SESSIONS) evictOldest();
326
1021
  const session = {
327
1022
  id,
328
- state: createInitialState(),
1023
+ protocol: meta?.protocol,
1024
+ upstreamOrigin: meta?.upstreamOrigin,
1025
+ state: createInitialState2(),
329
1026
  createdAt: Date.now(),
330
1027
  lastSeen: Date.now(),
331
1028
  requests: 0,
332
1029
  condensedToolResults: 0,
333
- tokensSaved: 0
1030
+ tokensSaved: 0,
1031
+ blockContents: /* @__PURE__ */ new Map(),
1032
+ inFlight: 0,
1033
+ persisted: false
334
1034
  };
335
1035
  sessions.set(id, session);
336
1036
  return session;
337
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
+ }
338
1058
  function listSessions() {
339
1059
  return [...sessions.values()].sort((a, b) => b.lastSeen - a.lastSeen);
340
1060
  }
1061
+ function markDirty(session) {
1062
+ getStore().scheduleSave(session);
1063
+ }
1064
+ function cacheBlockContent(session, blockId, content) {
1065
+ session.blockContents.set(blockId, content);
1066
+ }
341
1067
  function evictOldest() {
342
1068
  let oldestId;
343
1069
  let oldestSeen = Infinity;
344
- for (const [id, s] of sessions) {
345
- if (s.lastSeen < oldestSeen) {
346
- 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;
347
1074
  oldestId = id;
348
1075
  }
349
1076
  }
350
- 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());
351
1087
  }
352
1088
 
353
1089
  // src/compress-tool.ts
354
1090
  import { COMPRESS_PHILOSOPHY, HOW_TO_COMPRESS_RULES } from "acp-kernel";
355
1091
  var COMPRESS_TOOL_NAME = "compress";
1092
+ var ACP_TEXT_OPEN = "<acp_compress>";
1093
+ var ACP_TEXT_CLOSE = "</acp_compress>";
356
1094
  var COMPRESS_TOOL = {
357
1095
  name: COMPRESS_TOOL_NAME,
358
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?}].",
@@ -378,12 +1116,18 @@ var COMPRESS_TOOL = {
378
1116
  }
379
1117
  };
380
1118
  function parseCompressInput(input) {
381
- 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
+ }
382
1123
  const obj = input;
383
1124
  if (Array.isArray(obj.content)) {
384
- 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;
385
1128
  }
386
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(",")}`);
387
1131
  return single ? [single] : [];
388
1132
  }
389
1133
  function toRange(r) {
@@ -436,7 +1180,7 @@ ${HOW_TO_COMPRESS_RULES}
436
1180
 
437
1181
  ACP TAGS
438
1182
 
439
- 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.
440
1184
 
441
1185
  TOOLS
442
1186
 
@@ -506,6 +1250,36 @@ var ACP_TOOLS_OPENAI = [
506
1250
  SEARCH_CONTEXT_TOOL_OPENAI,
507
1251
  ACP_STATUS_TOOL_OPENAI
508
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
+ ];
509
1283
  var PROXY_TOOL_NAMES = /* @__PURE__ */ new Set([
510
1284
  COMPRESS_TOOL_NAME,
511
1285
  DECOMPRESS_TOOL_NAME,
@@ -513,6 +1287,15 @@ var PROXY_TOOL_NAMES = /* @__PURE__ */ new Set([
513
1287
  ACP_STATUS_TOOL_NAME
514
1288
  ]);
515
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
+
516
1299
  // src/stream.ts
517
1300
  var NOOP = /* @__PURE__ */ Symbol("noop");
518
1301
  async function* rewriteSseStream(upstream, ctx) {
@@ -533,6 +1316,7 @@ async function* rewriteSseStream(upstream, ctx) {
533
1316
  const { done, value } = await reader.read();
534
1317
  if (done) break;
535
1318
  buf += decoder.decode(value, { stream: true });
1319
+ buf = normalizeSseLineEndings(buf);
536
1320
  let idx;
537
1321
  while ((idx = buf.indexOf("\n\n")) !== -1) {
538
1322
  const rawEvent = buf.slice(0, idx);
@@ -581,6 +1365,10 @@ function routeEvent(ev, blocks, ctx, markConverted, markRealToolUse, getConverte
581
1365
  if (typeof partial === "string") st.json += partial;
582
1366
  return NOOP;
583
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
+ }
584
1372
  return emitEvent(ev);
585
1373
  }
586
1374
  if (t === "content_block_stop") {
@@ -638,7 +1426,19 @@ function applyRanges(ranges, ctx) {
638
1426
  state: ctx.session.state,
639
1427
  config: ctx.config
640
1428
  });
1429
+ const beforeIds = new Set(ctx.session.state.blocks.map((b) => b.blockId));
641
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
+ }
642
1442
  const r = res.result;
643
1443
  const detail = ranges.map((rg) => `${rg.startRef}\u2013${rg.endRef}`).join(", ");
644
1444
  if (r.blocksCreated === 0) {
@@ -647,9 +1447,9 @@ function applyRanges(ranges, ctx) {
647
1447
  return `[Compression FAILED: ${errs} Do not retry the same range.]`;
648
1448
  }
649
1449
  const warn = r.warnings.length > 0 ? ` ${r.warnings.join("; ")}` : "";
650
- const msg = `[Compressed ${detail} \u2192 ${r.blocksCreated} block(s), ~${r.tokensCompressed} tokens saved.${warn}]`;
651
- ctx.log(`[acp-proxy: ${msg}]`);
652
- 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;
653
1453
  } catch (err) {
654
1454
  ctx.log(`[acp-proxy: compress failed: ${String(err)}]`);
655
1455
  return `[Compression FAILED: ${String(err)} Do not retry the same range.]`;
@@ -703,55 +1503,109 @@ function rewriteJsonResponse(body, ctx) {
703
1503
  }
704
1504
  b.content = newContent;
705
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
+ }
706
1512
  return body;
707
1513
  }
708
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
+
709
1541
  // src/compress-loop.ts
710
1542
  import {
711
1543
  buildStatusReport,
712
- collectBlockContent,
713
- deactivateBlock,
714
1544
  estimateTokensFast
715
1545
  } from "acp-kernel";
716
- import { writeFileSync, mkdirSync } from "fs";
717
- 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";
718
1554
  import { tmpdir } from "os";
719
- function executeProxyTool(toolName, args, ctx) {
720
- if (toolName === "compress") {
721
- 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;
722
1575
  }
723
- if (toolName === "decompress") {
724
- const rawBlockId = args.blockId;
725
- if (typeof rawBlockId !== "string" || rawBlockId.length === 0) {
726
- return "[decompress FAILED: blockId is required]";
727
- }
728
- const blockId = rawBlockId.trim();
729
- const block = ctx.core.decompress(blockId, ctx.session.state);
730
- if (!block) return `[Block ${blockId} not found]`;
731
- const full = args.full === true;
732
- const collected = collectBlockContent(ctx.session.state, block, ctx.messages, { full });
1576
+ if (count > 0 || cached) {
733
1577
  ctx.session.state = deactivateBlock(ctx.session.state, [blockId]);
734
- const header = `[Restored block ${blockId} \u2014 ${collected.count} item(s)${full ? ", full" : ""}]`;
735
- const body = collected.text || block.summary;
736
- const safeBlockId = blockId.replace(/[^a-zA-Z0-9_-]/g, "-");
737
- const outPath = body.length > 1e4 ? join(tmpdir(), `acp-decompress-${safeBlockId}-${Date.now()}.txt`) : null;
738
- if (outPath) {
739
- try {
740
- mkdirSync(dirname(outPath), { recursive: true });
741
- writeFileSync(outPath, body, "utf8");
742
- 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}
743
1588
  Content (${body.length} chars) written to: ${outPath}
744
1589
  Use the read tool to access it.`;
745
- } catch (e) {
746
- return `${header}
1590
+ } catch (e) {
1591
+ return `${header}
747
1592
  [Failed to write to ${outPath}: ${String(e)}]
748
1593
  ${body.slice(0, 4e3)}...`;
749
- }
750
1594
  }
751
- return `${header}
752
- ${body}`;
753
1595
  }
754
- if (toolName === "search_context") {
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") {
755
1609
  const query = typeof args.query === "string" ? args.query : "";
756
1610
  if (query.length === 0) return "[search_context FAILED: query is required]";
757
1611
  const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 5;
@@ -885,27 +1739,469 @@ ${icon} [ACP] ${inner}
885
1739
  }
886
1740
  async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOptions) {
887
1741
  let upstream = initialUpstream;
888
- const model = requestBody.model ?? "unknown";
889
- let responseId = `chatcmpl-proxy-${Date.now()}`;
890
- const makeBase = () => ({
891
- id: responseId,
892
- object: "chat.completion.chunk",
893
- created: Date.now(),
894
- model
895
- });
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;
896
2187
  let loopCount = 0;
2188
+ let responseObj = null;
2189
+ let activeClearTimer = null;
2190
+ let nextOutputIndex = 0;
897
2191
  for (; ; ) {
898
2192
  loopCount++;
899
- if (loopCount > 10) {
900
- ctx.log("[acp-proxy: compress loop limit (10) reached, forwarding as-is]");
901
- yield Buffer.from(buildFinishSse(makeBase(), "stop", null), "utf8");
902
- 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");
903
2198
  return;
904
2199
  }
905
- const toolCallByIndex = /* @__PURE__ */ new Map();
2200
+ const fcByItemId = /* @__PURE__ */ new Map();
906
2201
  let contentText = "";
907
- let finishReason = null;
908
- let usage = null;
2202
+ let completed = false;
2203
+ let terminalKind = null;
2204
+ let terminalRaw = null;
909
2205
  const isFirstRound = loopCount === 1;
910
2206
  const reader = upstream.getReader();
911
2207
  const decoder = new TextDecoder("utf-8");
@@ -915,156 +2211,179 @@ async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOpt
915
2211
  const { done, value } = await reader.read();
916
2212
  if (done) break;
917
2213
  sseBuffer += decoder.decode(value, { stream: true });
2214
+ if (sseBuffer.indexOf("\r") !== -1) sseBuffer = sseBuffer.replace(/\r\n|\r/g, "\n");
918
2215
  let sep;
919
2216
  while ((sep = sseBuffer.indexOf("\n\n")) >= 0) {
920
2217
  const eventStr = sseBuffer.slice(0, sep);
921
2218
  sseBuffer = sseBuffer.slice(sep + 2);
922
2219
  if (!eventStr.trim()) continue;
923
- const d = classifySseEvent(eventStr);
924
- if (d.done) {
925
- continue;
2220
+ const d = classifyResponsesSseEvent(eventStr);
2221
+ if (d.yieldChunk && (isFirstRound || !d.isMeta) && !(TEXT_PROTOCOL && !d.isMeta)) {
2222
+ yield d.yieldChunk;
926
2223
  }
927
- if (isFirstRound) {
928
- if (d.yieldChunk) {
929
- if (!responseId) {
930
- const dataLine = eventStr.split("\n").find((l) => l.startsWith("data:"));
931
- if (dataLine) {
932
- try {
933
- const p = JSON.parse(dataLine.slice(5).trim());
934
- if (typeof p.id === "string") responseId = p.id;
935
- } catch {
936
- }
937
- }
938
- }
939
- yield d.yieldChunk;
940
- }
941
- } else {
942
- if (d.contentDelta) {
943
- 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;
944
2243
  }
945
2244
  }
946
- if (d.contentDelta) contentText += d.contentDelta;
947
- if (d.finishReason) finishReason = d.finishReason;
948
- if (d.usage !== void 0) usage = d.usage;
949
- if (d.toolCalls) {
950
- for (const tc of d.toolCalls) {
951
- const existing = toolCallByIndex.get(tc.index);
952
- if (existing) {
953
- if (tc.name) existing.name = tc.name;
954
- if (tc.id) existing.id = tc.id;
955
- existing.arguments += tc.arguments;
956
- } else {
957
- toolCallByIndex.set(tc.index, tc);
958
- }
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)}%)` : ""}`);
959
2259
  }
960
2260
  }
961
2261
  }
962
2262
  }
963
- sseBuffer += decoder.decode();
964
2263
  } finally {
965
2264
  reader.releaseLock();
2265
+ if (activeClearTimer) {
2266
+ activeClearTimer();
2267
+ activeClearTimer = null;
2268
+ }
2269
+ }
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
+ }
966
2280
  }
967
- const sortedIndices = [...toolCallByIndex.keys()].sort((a, b) => a - b);
968
- const toolCalls = sortedIndices.map((i) => {
969
- const tc = toolCallByIndex.get(i);
970
- return { ...tc, id: tc.id || `call_${tc.index}` };
971
- }).filter((tc) => tc.name.length > 0);
972
- const proxyCalls = toolCalls.filter((tc) => PROXY_TOOL_NAMES.has(tc.name));
973
- const realCalls = toolCalls.filter((tc) => !PROXY_TOOL_NAMES.has(tc.name));
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))}`);
974
2285
  const hasOnlyProxy = proxyCalls.length > 0 && realCalls.length === 0;
975
2286
  if (!hasOnlyProxy) {
976
- for (const tc of realCalls) {
977
- 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++;
978
2291
  }
979
- const fr = realCalls.length > 0 ? "tool_calls" : finishReason ?? "stop";
980
- yield Buffer.from(buildFinishSse(makeBase(), fr, usage), "utf8");
981
- 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");
982
2301
  return;
983
2302
  }
984
2303
  const names = proxyCalls.map((c) => c.name).join(", ");
985
- ctx.log(`[acp-proxy: round ${loopCount} \u2014 ${proxyCalls.length} proxy call(s): ${names}]`);
986
- const messages = requestBody.messages ?? [];
987
- messages.push({
988
- role: "assistant",
989
- content: contentText || null,
990
- tool_calls: proxyCalls.map((tc) => ({
991
- id: tc.id,
992
- type: "function",
993
- function: { name: tc.name, arguments: tc.arguments }
994
- }))
995
- });
996
- 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) {
997
2323
  let args = {};
998
2324
  try {
999
- args = JSON.parse(tc.arguments);
1000
- } 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)}`);
1001
2328
  args = {};
1002
2329
  }
1003
- 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);
1004
2334
  const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
1005
- 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}`;
1006
2337
  yield Buffer.from(
1007
- buildContentSse(responseId, model, buildVisibilityMarker(tc.name, result)),
2338
+ buildMessageItemSequence(markerItemId, nextOutputIndex++, buildVisibilityMarker(fc.name, result)),
1008
2339
  "utf8"
1009
2340
  );
1010
- messages.push({
1011
- role: "tool",
1012
- tool_call_id: tc.id,
1013
- content: result
2341
+ inputItems.push({
2342
+ type: "function_call_output",
2343
+ call_id: fc.callId || `call_${Date.now()}`,
2344
+ output: result
1014
2345
  });
1015
2346
  }
1016
- requestBody.messages = messages;
1017
- 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, {
1018
2350
  method: "POST",
1019
2351
  headers: requestOptions.headers,
1020
2352
  body: JSON.stringify(requestBody)
1021
2353
  });
1022
2354
  if (!resp.ok || !resp.body) {
2355
+ clearTimer();
1023
2356
  const errText = await resp.text().catch(() => "upstream error");
1024
- 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()}`;
1025
2359
  yield Buffer.from(
1026
- `data: ${JSON.stringify({
1027
- ...makeBase(),
1028
- choices: [{
1029
- index: 0,
1030
- delta: { content: `
1031
- [acp-proxy: upstream error ${resp.status}]
1032
- ` },
1033
- finish_reason: null
1034
- }]
1035
- })}
1036
-
1037
- `,
2360
+ buildMessageItemSequence(errItemId, nextOutputIndex++, `
2361
+ [acp-proxy: upstream error ${resp.status}: ${errText.slice(0, 200)}]
2362
+ `),
1038
2363
  "utf8"
1039
2364
  );
1040
- yield Buffer.from(buildFinishSse(makeBase(), "stop", null), "utf8");
1041
- yield Buffer.from("data: [DONE]\n\n", "utf8");
2365
+ yield Buffer.from(buildCompleted(responseObj), "utf8");
1042
2366
  return;
1043
2367
  }
1044
2368
  upstream = resp.body;
2369
+ if (activeClearTimer) activeClearTimer();
2370
+ activeClearTimer = clearTimer;
1045
2371
  }
1046
2372
  }
1047
2373
 
1048
2374
  // src/stream-openai.ts
1049
- function safeJsonParse(s) {
1050
- try {
1051
- return s ? JSON.parse(s) : {};
1052
- } catch {
1053
- return {};
1054
- }
1055
- }
1056
2375
  function rewriteOpenaiJsonResponse(body, ctx) {
1057
2376
  if (!body || typeof body !== "object") return body;
1058
2377
  const b = body;
1059
2378
  const choice = b.choices?.[0];
1060
- const msg = choice?.message;
1061
- if (!choice || !msg) return body;
2379
+ const msg2 = choice?.message;
2380
+ if (!choice || !msg2) return body;
1062
2381
  let converted = false;
1063
2382
  let sawReal = false;
1064
2383
  const noteParts = [];
1065
2384
  const keepToolCalls = [];
1066
- const existingText = typeof msg.content === "string" ? msg.content : "";
1067
- const toolCalls = msg.tool_calls;
2385
+ const existingText = typeof msg2.content === "string" ? msg2.content : "";
2386
+ const toolCalls = msg2.tool_calls;
1068
2387
  if (Array.isArray(toolCalls)) {
1069
2388
  for (const tc of toolCalls) {
1070
2389
  if (tc.function?.name === COMPRESS_TOOL_NAME) {
@@ -1076,14 +2395,17 @@ function rewriteOpenaiJsonResponse(body, ctx) {
1076
2395
  }
1077
2396
  }
1078
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
+ }
1079
2401
  if (!converted) return body;
1080
2402
  const note = noteParts.join("\n");
1081
- msg.content = existingText ? `${existingText}
2403
+ msg2.content = existingText ? `${existingText}
1082
2404
  ${note}` : note;
1083
2405
  if (keepToolCalls.length > 0) {
1084
- msg.tool_calls = keepToolCalls;
2406
+ msg2.tool_calls = keepToolCalls;
1085
2407
  } else {
1086
- delete msg.tool_calls;
2408
+ delete msg2.tool_calls;
1087
2409
  }
1088
2410
  if (!sawReal) {
1089
2411
  choice.finish_reason = "stop";
@@ -1091,70 +2413,300 @@ ${note}` : note;
1091
2413
  return body;
1092
2414
  }
1093
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
+
1094
2528
  // src/server.ts
1095
2529
  var UPSTREAM_HOP_HEADERS = /* @__PURE__ */ new Set([
1096
2530
  "host",
1097
2531
  "content-length",
1098
2532
  "connection",
1099
2533
  "keep-alive",
1100
- "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"
1101
2539
  ]);
1102
- function resolveUpstream(opts2, authHeader) {
1103
- if (!opts2.routes.length || !authHeader) return void 0;
1104
- const token = authHeader.replace(/^Bearer\s+/i, "").trim();
1105
- if (!token) return void 0;
1106
- const match = opts2.routes.find((r) => r.apiKey && r.apiKey === token);
1107
- return match?.baseURL.replace(/\/$/, "");
1108
- }
1109
- function startServer(opts2) {
2540
+ function resolveUpstream(opts, reqUrl) {
2541
+ const names = Object.keys(opts.routes);
2542
+ if (names.length === 0) return void 0;
2543
+ const RESERVED = /* @__PURE__ */ new Set(["v1", "v2", "v4", "chat", "completions", "messages", "models", "api"]);
2544
+ const sorted = [...names].sort((a, b) => b.length - a.length);
2545
+ const segments = reqUrl.split("/");
2546
+ for (const name of sorted) {
2547
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name)) continue;
2548
+ if (RESERVED.has(name.toLowerCase())) continue;
2549
+ const idx = segments.indexOf(name);
2550
+ if (idx < 0) continue;
2551
+ const base = opts.routes[name].url.replace(/\/$/, "");
2552
+ const rest = [...segments.slice(0, idx), ...segments.slice(idx + 1)].join("/");
2553
+ const rewrittenUrl = base + rest;
2554
+ return { upstream: base, rewrittenUrl, provider: name };
2555
+ }
2556
+ return void 0;
2557
+ }
2558
+ async function startServer(opts) {
1110
2559
  const core = createCore();
1111
- const config = opts2.kernelConfig;
1112
- const log = (level, msg) => logMsg(opts2, level, msg);
1113
- 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) => {
1114
2565
  try {
1115
- await handle(req, res, opts2, core, config, log);
2566
+ await handle(req, res, opts, core, config, log);
1116
2567
  } catch (err) {
1117
- log("error", String(err));
2568
+ const msg2 = String(err);
2569
+ log("error", msg2);
1118
2570
  if (!res.headersSent) {
1119
- res.writeHead(502, { "content-type": "application/json" });
1120
- 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 }));
1121
2574
  } else {
1122
2575
  res.end();
1123
2576
  }
1124
2577
  }
1125
2578
  });
1126
- server2.listen(opts2.port, opts2.host, () => {
1127
- log("info", `acp-proxy listening on http://${opts2.host}:${opts2.port} \u2192 ${opts2.upstream}`);
2579
+ server.listen(opts.port, opts.host, () => {
2580
+ log(
2581
+ "info",
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}`)
2583
+ );
1128
2584
  });
1129
- 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;
1130
2612
  }
1131
- async function handle(req, res, opts2, core, config, log) {
2613
+ async function handle(req, res, opts, core, config, log) {
1132
2614
  if (req.method === "GET" && req.url === "/__acp/stats") return sendStats(res);
1133
2615
  if (req.method === "GET" && (req.url === "/" || req.url === "/__acp/health")) {
1134
2616
  res.writeHead(200, { "content-type": "application/json" });
1135
- 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) } }));
1136
2633
  return;
1137
2634
  }
1138
- const bodyBuffer = await readBody(req);
1139
2635
  const url = req.url ?? "";
1140
- const protocol = req.method === "POST" && bodyBuffer.length > 0 ? url.endsWith("/chat/completions") ? "openai" : url.endsWith("/v1/messages") || url.endsWith("/messages") ? "anthropic" : null : null;
1141
- const prepared = opts2.passthrough ? null : protocol === "anthropic" ? prepareAnthropic(bodyBuffer, req, opts2, core, config, log) : protocol === "openai" ? prepareOpenai(bodyBuffer, req, opts2, core, config, log) : null;
1142
- const outBody = prepared ? prepared.body : bodyBuffer;
1143
- await forward(req, res, opts2, outBody, prepared, core, config, log);
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;
2641
+ if (protocol && bodyBuffer.length > 0) {
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);
2653
+ if (limit && limit !== config.modelContextLimit) {
2654
+ reqConfig = { ...config, modelContextLimit: limit };
2655
+ }
2656
+ }
2657
+ }
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
+ }
1144
2681
  }
1145
- var ACP_TAG_RE = /^\x3cacp [^>]*\x3e[^\x3c]*\x3c\/acp\x3e\n?/;
1146
- function stripToolTags(messages) {
2682
+ var ACP_TAG_MARK = "<acp ";
2683
+ function diagTagSummary(messages, sessionId, strategy) {
2684
+ let textTagged = 0;
2685
+ let toolTagged = 0;
1147
2686
  for (const m of messages) {
1148
- if (m.contentType === "tool-call" || m.contentType === "tool-result") {
1149
- m.text = (m.text ?? "").replace(ACP_TAG_RE, "");
1150
- }
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++;
1151
2691
  }
2692
+ return `[${sessionId}] processTurn: ${messages.length} msgs, renderTags=${strategy}, ${textTagged} text tagged, ${toolTagged} tool tagged (should be 0 with text-only)`;
1152
2693
  }
1153
- function prepareAnthropic(bodyBuffer, req, opts2, core, config, log) {
1154
- 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;
1155
2709
  const stream = parsed.stream === true;
1156
- const sessionId = deriveSessionId(parsed, headerValue(req, opts2.sessionHeader));
1157
- const session = getSession(sessionId);
1158
2710
  session.requests++;
1159
2711
  let processedMessages = [];
1160
2712
  let rebuiltMessages = parsed.messages;
@@ -1162,74 +2714,148 @@ function prepareAnthropic(bodyBuffer, req, opts2, core, config, log) {
1162
2714
  let toolsOut = parsed.tools;
1163
2715
  try {
1164
2716
  const { msgs } = anthropicToCore(parsed);
1165
- const tokenCount = estimateTokensFast2(msgs.map((m) => m.text ?? "").join("\n"));
1166
- 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" });
1167
2719
  session.state = turn.state;
1168
- stripToolTags(turn.messages);
1169
- processedMessages = turn.messages;
1170
- rebuiltMessages = coreToAnthropic(turn.messages);
1171
- systemOut = injectSystem(parsed, turn.nudge, opts2);
1172
- 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) {
1173
2727
  toolsOut = injectTool(parsed.tools);
1174
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
+ }
1175
2738
  } catch (err) {
1176
2739
  log("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
1177
2740
  processedMessages = [];
1178
2741
  }
1179
2742
  const rebuilt = { ...parsed, messages: rebuiltMessages, system: systemOut, tools: toolsOut };
1180
- 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 };
1181
2745
  }
1182
- function prepareOpenai(bodyBuffer, req, opts2, core, config, log) {
1183
- const parsed = JSON.parse(bodyBuffer.toString("utf8"));
2746
+ function prepareOpenai(parsed, req, opts, core, config, log, session) {
2747
+ const sessionId = session.id;
1184
2748
  const stream = parsed.stream === true;
1185
- const sessionId = deriveSessionIdOpenai(parsed, headerValue(req, opts2.sessionHeader));
1186
- const session = getSession(sessionId);
1187
2749
  session.requests++;
1188
2750
  let processedMessages = [];
1189
2751
  let rebuiltMessages = parsed.messages;
1190
2752
  let toolsOut = parsed.tools;
1191
2753
  const maxTokens = typeof parsed.max_tokens === "number" ? parsed.max_tokens : 8192;
1192
2754
  const isTitleGen = maxTokens <= 200 || parsed.messages.length <= 2;
1193
- const shouldInject = opts2.compress.injectTool && !isTitleGen;
2755
+ const shouldInject = opts.compress.injectTool && !isTitleGen;
1194
2756
  try {
1195
2757
  const { msgs } = openaiToCore(parsed);
1196
- const tokenCount = estimateTokensFast2(msgs.map((m) => m.text ?? "").join("\n"));
1197
- 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" });
1198
2760
  session.state = turn.state;
1199
- stripToolTags(turn.messages);
1200
- processedMessages = turn.messages;
1201
- 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);
1202
2766
  const sysParts = [];
1203
2767
  if (shouldInject) sysParts.push(buildCompressSystemPrompt());
2768
+ rebuiltMessages = injectOpenaiSystem(rebuiltMessages, sysParts);
2769
+ if (shouldInject) {
2770
+ toolsOut = injectOpenaiTool(parsed.tools);
2771
+ }
1204
2772
  if (turn.nudge?.shouldInject && shouldInject) {
1205
2773
  try {
1206
2774
  const rendered = renderNudgeText(turn.nudge);
1207
- if (rendered.text) sysParts.push(rendered.text);
2775
+ if (rendered.text) {
2776
+ rebuiltMessages = [...rebuiltMessages, { role: "user", content: rendered.text }];
2777
+ }
1208
2778
  } catch {
1209
2779
  }
1210
2780
  }
1211
- rebuiltMessages = injectOpenaiSystem(rebuiltMessages, sysParts);
1212
- if (shouldInject) {
1213
- toolsOut = injectOpenaiTool(parsed.tools);
1214
- }
1215
2781
  } catch (err) {
1216
2782
  log("warn", `[${sessionId}] kernel transform failed, forwarding unchanged: ${String(err)}`);
1217
2783
  processedMessages = [];
1218
2784
  }
1219
2785
  const rebuilt = { ...parsed, messages: rebuiltMessages, tools: toolsOut };
2786
+ markDirty(session);
1220
2787
  return { body: JSON.stringify(rebuilt), session, processedMessages, protocol: "openai", stream, compressInjected: shouldInject };
1221
2788
  }
1222
- function injectSystem(parsed, nudge, opts2) {
1223
- const baseText = extractSystem(parsed.system);
1224
- const parts = [];
1225
- if (opts2.compress.injectTool) parts.push(buildCompressSystemPrompt());
1226
- if (nudge?.shouldInject) {
1227
- try {
1228
- const rendered = renderNudgeText(nudge);
1229
- if (rendered.text) parts.push(rendered.text);
1230
- } 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)"}`);
1231
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 = [];
1232
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());
1233
2859
  if (parts.length === 0) return parsed.system;
1234
2860
  const full = baseText ? `${baseText}
1235
2861
 
@@ -1251,21 +2877,44 @@ function injectOpenaiTool(tools) {
1251
2877
  const additions = ACP_TOOLS_OPENAI.filter((t) => !present.has(t.function.name));
1252
2878
  return [...tools, ...additions];
1253
2879
  }
1254
- async function forward(req, res, opts2, body, prepared, core, config, log) {
1255
- const base = resolveUpstream(opts2, req.headers.authorization) ?? opts2.upstream;
1256
- const upstreamUrl = base + (req.url ?? "");
1257
- log("info", `forward ${req.method} ${req.url ?? ""} \u2192 ${upstreamUrl}`);
1258
- if (opts2.debug && typeof body === "string") {
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 ?? "");
2891
+ log("info", `forward ${req.method} ${req.url ?? ""} \u2192 ${upstreamUrl}${route ? ` (${route.provider})` : ""}`);
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") {
1259
2906
  try {
1260
2907
  const parsed = JSON.parse(body);
1261
2908
  const toolNames = (parsed.tools ?? []).map((t) => {
1262
2909
  const fn = t.function;
1263
- return fn?.name ?? "?";
2910
+ return fn?.name ?? t.name ?? "?";
1264
2911
  });
1265
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}`);
1266
- const out = `/tmp/acp-proxy-debug-req-${Date.now()}.json`;
1267
- fs.writeFileSync(out, body.slice(0, 5e4));
1268
- 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
+ }
1269
2918
  } catch {
1270
2919
  }
1271
2920
  }
@@ -1274,26 +2923,37 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
1274
2923
  if (UPSTREAM_HOP_HEADERS.has(k.toLowerCase()) || v === void 0) continue;
1275
2924
  headers[k] = Array.isArray(v) ? v.join(", ") : v;
1276
2925
  }
1277
- headers["host"] = new URL(base).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
+ }
1278
2930
  const init = {
1279
2931
  method: req.method ?? "GET",
1280
2932
  headers,
1281
2933
  body: req.method === "GET" || req.method === "HEAD" ? void 0 : body
1282
2934
  };
1283
- const upstream = await fetch(upstreamUrl, init);
2935
+ const { response: upstream, clearTimer: clearUpstreamTimer } = await fetchWithTimeout(upstreamUrl, init);
1284
2936
  const respHeaders = {};
1285
2937
  upstream.headers.forEach((v, k) => {
1286
2938
  if (UPSTREAM_HOP_HEADERS.has(k.toLowerCase())) return;
1287
2939
  respHeaders[k] = v;
1288
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
+ }
1289
2947
  res.writeHead(upstream.status, respHeaders);
1290
2948
  if (!upstream.body) {
1291
2949
  res.end();
2950
+ clearUpstreamTimer();
1292
2951
  return;
1293
2952
  }
1294
- const useRewriter = prepared !== null && prepared.processedMessages.length > 0 && opts2.compress.injectTool;
2953
+ const useRewriter = prepared !== null && prepared.compressInjected && prepared.processedMessages.length > 0;
1295
2954
  if (!useRewriter || prepared === null) {
1296
2955
  await pipeThrough(upstream.body, res);
2956
+ clearUpstreamTimer();
1297
2957
  return;
1298
2958
  }
1299
2959
  const ctx = {
@@ -1301,50 +2961,83 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
1301
2961
  config,
1302
2962
  messages: prepared.processedMessages,
1303
2963
  session: prepared.session,
1304
- log: (msg) => log("info", `[${prepared.session.id}] ${msg}`),
1305
- debug: opts2.debug
2964
+ log: (msg2) => log("info", `[${prepared.session.id}] ${msg2}`),
2965
+ debug: opts.debug
1306
2966
  };
1307
2967
  if (prepared.stream) {
1308
2968
  let streamToRead = upstream.body;
1309
2969
  let dumpRaw;
1310
- if (opts2.dumpSse) {
2970
+ if (opts.dumpSse) {
1311
2971
  const [a, b] = upstream.body.tee();
1312
2972
  streamToRead = a;
1313
- dumpRaw = dumpStreamToFile(b, opts2.dumpSse, `${Date.now()}-${prepared.session.id}-raw.sse`);
1314
- }
1315
- const ctx2 = {
1316
- core,
1317
- config,
1318
- messages: prepared.processedMessages,
1319
- session: prepared.session,
1320
- log: (msg) => log("info", `[${prepared.session.id}] ${msg}`),
1321
- debug: opts2.debug
1322
- };
1323
- if (prepared.protocol === "openai" && prepared.compressInjected) {
1324
- const parsedReq = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
1325
- const reqHeaders = {};
1326
- for (const [k, v] of Object.entries(headers)) {
1327
- if (k.toLowerCase() === "content-length" || k.toLowerCase() === "host") continue;
1328
- reqHeaders[k] = v;
1329
- }
1330
- reqHeaders["content-type"] = "application/json";
1331
- const loop = compressLoopStream(
1332
- streamToRead,
1333
- { core, config, messages: prepared.processedMessages, session: prepared.session, log: ctx2.log },
1334
- parsedReq,
1335
- { url: upstreamUrl, headers: reqHeaders }
1336
- );
1337
- for await (const chunk of loop) {
1338
- if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
1339
- }
1340
- } else {
1341
- const rewriter = rewriteSseStream(streamToRead, ctx2);
1342
- for await (const chunk of rewriter) {
1343
- 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
+ }
1344
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;
1345
3040
  }
1346
- res.end();
1347
- if (dumpRaw) await dumpRaw;
1348
3041
  } else {
1349
3042
  const buf = await upstream.arrayBuffer();
1350
3043
  const text = Buffer.from(buf).toString("utf8");
@@ -1352,6 +3045,8 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
1352
3045
  const json = JSON.parse(text);
1353
3046
  if (prepared.protocol === "openai") {
1354
3047
  rewriteOpenaiJsonResponse(json, ctx);
3048
+ } else if (prepared.protocol === "responses") {
3049
+ rewriteResponsesJsonResponse(json, ctx);
1355
3050
  } else {
1356
3051
  rewriteJsonResponse(json, ctx);
1357
3052
  }
@@ -1359,7 +3054,9 @@ async function forward(req, res, opts2, body, prepared, core, config, log) {
1359
3054
  } catch {
1360
3055
  res.end(text);
1361
3056
  }
3057
+ clearUpstreamTimer();
1362
3058
  }
3059
+ markDirty(prepared.session);
1363
3060
  }
1364
3061
  async function pipeThrough(stream, res) {
1365
3062
  const reader = stream.getReader();
@@ -1377,11 +3074,11 @@ async function pipeThrough(stream, res) {
1377
3074
  }
1378
3075
  }
1379
3076
  async function dumpStreamToFile(stream, dir, name) {
1380
- const { mkdirSync: mkdirSync2, createWriteStream } = await import("fs");
1381
- const { join: join2 } = await import("path");
3077
+ const { mkdirSync: mkdirSync3, createWriteStream } = await import("fs");
3078
+ const { join: join3 } = await import("path");
1382
3079
  try {
1383
- mkdirSync2(dir, { recursive: true });
1384
- const ws = createWriteStream(join2(dir, name));
3080
+ mkdirSync3(dir, { recursive: true });
3081
+ const ws = createWriteStream(join3(dir, name));
1385
3082
  const reader = stream.getReader();
1386
3083
  try {
1387
3084
  for (; ; ) {
@@ -1414,26 +3111,160 @@ function headerValue(req, name) {
1414
3111
  }
1415
3112
  return void 0;
1416
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
+ };
1417
3122
  function readBody(req) {
1418
3123
  return new Promise((resolve, reject) => {
1419
3124
  const chunks = [];
1420
- req.on("data", (c) => chunks.push(c));
1421
- req.on("end", () => resolve(Buffer.concat(chunks)));
1422
- 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
+ });
1423
3143
  });
1424
3144
  }
1425
- function logMsg(opts2, level, msg) {
1426
- if (!opts2.log) return;
3145
+ function logMsg(opts, level, msg2) {
3146
+ if (!opts.log) return;
1427
3147
  const ts = (/* @__PURE__ */ new Date()).toISOString();
1428
- console.error(`${ts} [${level}] ${msg}`);
3148
+ console.error(`${ts} [${level}] ${msg2}`);
1429
3149
  }
1430
3150
 
1431
- // src/index.ts
1432
- var opts = loadOptions();
1433
- var server = startServer(opts);
1434
- for (const sig of ["SIGINT", "SIGTERM"]) {
1435
- process.on(sig, () => {
1436
- server.close(() => process.exit(0));
1437
- });
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);
1438
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();
1439
3270
  //# sourceMappingURL=index.js.map