billion-context 0.1.32 → 0.1.34

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
@@ -43736,7 +43736,7 @@ function defaultConfig(modelContextLimit, overrides = {}) {
43736
43736
  iterationThreshold: 15,
43737
43737
  force: "soft",
43738
43738
  growthRatio: 0.05,
43739
- growthFloor: Math.max(2e4, Math.round(modelContextLimit * 0.05)),
43739
+ growthFloor: 5e4,
43740
43740
  growthCap: 5e4,
43741
43741
  minGrowthFloor: 2e4,
43742
43742
  minGrowthRatio: 0.45,
@@ -43930,14 +43930,49 @@ function truncateLargeToolOutputs(messages, tokenCount, config, countTokens, opt
43930
43930
  return { messages: updated, truncatedCount, savedTokens };
43931
43931
  }
43932
43932
  var KEEP_LAST_ORPHANED = 0;
43933
+ function rangeKey(startRef, endRef) {
43934
+ return `${startRef}::${endRef}`;
43935
+ }
43936
+ function rewriteCompressText(text, liveKeys) {
43937
+ let parsed;
43938
+ try {
43939
+ parsed = JSON.parse(text ?? "");
43940
+ } catch {
43941
+ return null;
43942
+ }
43943
+ if (!parsed || typeof parsed !== "object") return null;
43944
+ const obj = parsed;
43945
+ const content = obj.content;
43946
+ if (!Array.isArray(content) || content.length === 0) return null;
43947
+ const kept = content.filter((entry) => {
43948
+ if (!entry || typeof entry !== "object") return false;
43949
+ const s3 = typeof entry.startId === "string" ? entry.startId : typeof entry.messageId === "string" ? entry.messageId : "";
43950
+ const e = typeof entry.endId === "string" ? entry.endId : typeof entry.messageId === "string" ? entry.messageId : "";
43951
+ return liveKeys.has(rangeKey(s3, e));
43952
+ });
43953
+ if (kept.length === content.length || kept.length === 0) return null;
43954
+ return JSON.stringify({ ...obj, content: kept });
43955
+ }
43933
43956
  function hideConsumedCompressCalls(state, messages) {
43934
- const activeCallIds = /* @__PURE__ */ new Set();
43935
43957
  const allBlockCallIds = /* @__PURE__ */ new Set();
43958
+ const activeCallIds = /* @__PURE__ */ new Set();
43959
+ const liveRangeKeysByCallId = /* @__PURE__ */ new Map();
43960
+ const legacyLiveByCallId = /* @__PURE__ */ new Set();
43936
43961
  for (const block of state.blocks) {
43937
- if (block.compressCallId) {
43938
- allBlockCallIds.add(block.compressCallId);
43939
- if (block.active) activeCallIds.add(block.compressCallId);
43962
+ if (!block.compressCallId) continue;
43963
+ allBlockCallIds.add(block.compressCallId);
43964
+ if (!block.active) continue;
43965
+ activeCallIds.add(block.compressCallId);
43966
+ if (block.startRef === void 0 || block.endRef === void 0) {
43967
+ legacyLiveByCallId.add(block.compressCallId);
43968
+ continue;
43940
43969
  }
43970
+ let keys = liveRangeKeysByCallId.get(block.compressCallId);
43971
+ if (!keys) {
43972
+ keys = /* @__PURE__ */ new Set();
43973
+ liveRangeKeysByCallId.set(block.compressCallId, keys);
43974
+ }
43975
+ keys.add(rangeKey(block.startRef, block.endRef));
43941
43976
  }
43942
43977
  const lastOrphanedCallIds = [];
43943
43978
  for (let i = messages.length - 1; i >= 0 && lastOrphanedCallIds.length < KEEP_LAST_ORPHANED; i--) {
@@ -43966,6 +44001,16 @@ function hideConsumedCompressCalls(state, messages) {
43966
44001
  hidden++;
43967
44002
  continue;
43968
44003
  }
44004
+ if (message.toolName === "compress" && message.contentType === "tool-call" && message.toolCallId && keepCallIds.has(message.toolCallId)) {
44005
+ const liveKeys = liveRangeKeysByCallId.get(message.toolCallId);
44006
+ if (liveKeys && liveKeys.size > 0 && !legacyLiveByCallId.has(message.toolCallId)) {
44007
+ const rewritten = rewriteCompressText(message.text, liveKeys);
44008
+ if (rewritten !== null) {
44009
+ result.push({ ...message, text: rewritten });
44010
+ continue;
44011
+ }
44012
+ }
44013
+ }
43969
44014
  result.push(message);
43970
44015
  }
43971
44016
  return { messages: result, hidden };
@@ -44199,8 +44244,8 @@ function refNum(ref) {
44199
44244
  const n = parseInt(ref.slice(1), 10);
44200
44245
  return Number.isNaN(n) ? -1 : n;
44201
44246
  }
44202
- function estimateMessageTokens(message) {
44203
- return Math.ceil((message.text ?? "").length / 4);
44247
+ function estimateTextTokens(text) {
44248
+ return Math.ceil(text.length / 4);
44204
44249
  }
44205
44250
  function isToolMessage(message) {
44206
44251
  return message.contentType === "tool-call" || message.contentType === "tool-result";
@@ -44212,7 +44257,7 @@ function isSyntheticOrPruned(message, state) {
44212
44257
  }
44213
44258
  return false;
44214
44259
  }
44215
- function computeProtectedRefs(messages, state, config) {
44260
+ function computeProtectedRefs(messages, state, config, countTokens = estimateTextTokens) {
44216
44261
  const preserveN = config.preserveRecentMessages;
44217
44262
  const preserveTokens = config.preserveRecentTokens;
44218
44263
  const result = /* @__PURE__ */ new Set();
@@ -44222,7 +44267,7 @@ function computeProtectedRefs(messages, state, config) {
44222
44267
  if (isNeverPreserveRecent(msg2)) continue;
44223
44268
  const ref = state.messageRefs.byRaw[msg2.id];
44224
44269
  if (!ref || ref === "BLOCKED") continue;
44225
- visible.push({ ref, tokens: estimateMessageTokens(msg2) });
44270
+ visible.push({ ref, tokens: countTokens(msg2.text ?? "") });
44226
44271
  }
44227
44272
  if (preserveN > 0) {
44228
44273
  for (const m2 of visible.slice(-preserveN)) {
@@ -44247,7 +44292,7 @@ function computeProtectedRefs(messages, state, config) {
44247
44292
  }
44248
44293
  return result;
44249
44294
  }
44250
- function buildCompressibleRanges(messages, state, config, protectedZoneRefs) {
44295
+ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, countTokens = estimateTextTokens) {
44251
44296
  const compressibleMsgs = [];
44252
44297
  const protectedMsgs = [];
44253
44298
  const protectedCallIds = collectProtectedToolCallIds(messages, config);
@@ -44260,7 +44305,7 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs) {
44260
44305
  protectedMsgs.push({
44261
44306
  ref,
44262
44307
  refNum: rn2,
44263
- tokens: estimateMessageTokens(msg2),
44308
+ tokens: countTokens(msg2.text ?? ""),
44264
44309
  tools: msg2.toolName ? [msg2.toolName] : []
44265
44310
  });
44266
44311
  continue;
@@ -44271,7 +44316,7 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs) {
44271
44316
  compressibleMsgs.push({
44272
44317
  ref,
44273
44318
  refNum: rn2,
44274
- tokens: estimateMessageTokens(msg2),
44319
+ tokens: countTokens(msg2.text ?? ""),
44275
44320
  isTool: isToolMessage(msg2),
44276
44321
  isUser: msg2.role === "user"
44277
44322
  });
@@ -44358,7 +44403,7 @@ function createCore(ports = {}) {
44358
44403
  let tokensCompressed = 0;
44359
44404
  const errors = [];
44360
44405
  const warnings = [];
44361
- const protectedMessageIds = input.protectedMessageIds ?? computeProtectedRefs(input.messages, input.state, input.config);
44406
+ const protectedMessageIds = input.protectedMessageIds ?? computeProtectedRefs(input.messages, input.state, input.config, countTokens);
44362
44407
  const preExistingCoverage = collectCoverage(state);
44363
44408
  const rangeIndexSets = [];
44364
44409
  for (const spec of input.ranges) {
@@ -44383,29 +44428,25 @@ function createCore(ports = {}) {
44383
44428
  const bMin = b2.indices.length > 0 ? Math.min(...b2.indices) : Infinity;
44384
44429
  return aMin - bMin;
44385
44430
  });
44386
- for (let i = 1; i < sortedRanges.length; i++) {
44387
- const prev = sortedRanges[i - 1];
44388
- const curr = sortedRanges[i];
44389
- const prevMax = prev.indices.length > 0 ? Math.max(...prev.indices) : -1;
44390
- const currMin = curr.indices.length > 0 ? Math.min(...curr.indices) : -1;
44391
- if (prevMax >= currMin && prevMax >= 0) {
44392
- return {
44393
- state: input.state,
44394
- result: {
44395
- blocksCreated: 0,
44396
- tokensCompressed: 0,
44397
- errors: [
44398
- `content: range (${prev.spec.startRef}..${prev.spec.endRef}) overlaps (${curr.spec.startRef}..${curr.spec.endRef}). Overlapping ranges cannot be compressed in the same batch.`
44399
- ],
44400
- warnings: []
44401
- }
44402
- };
44431
+ const skipSpecs = /* @__PURE__ */ new Set();
44432
+ let acceptedMaxIndex = -1;
44433
+ for (const entry of sortedRanges) {
44434
+ const entryMax = entry.indices.length > 0 ? Math.max(...entry.indices) : -1;
44435
+ const entryMin = entry.indices.length > 0 ? Math.min(...entry.indices) : -1;
44436
+ if (entryMin >= 0 && entryMin <= acceptedMaxIndex) {
44437
+ skipSpecs.add(entry.spec);
44438
+ warnings.push(
44439
+ `Skipped range (${entry.spec.startRef}..${entry.spec.endRef}) \u2014 overlaps an earlier range in the batch; the earlier range takes precedence. Keep ranges disjoint.`
44440
+ );
44441
+ continue;
44403
44442
  }
44443
+ if (entryMax > acceptedMaxIndex) acceptedMaxIndex = entryMax;
44404
44444
  }
44405
44445
  if (input.config.compress.minCompressRange > 0 && input.ranges.length > 0) {
44406
44446
  let totalRangeChars = 0;
44407
44447
  let hasBlockBoundaryRange = false;
44408
44448
  for (const spec of input.ranges) {
44449
+ if (skipSpecs.has(spec)) continue;
44409
44450
  let resolved;
44410
44451
  try {
44411
44452
  resolved = resolveBoundaries({
@@ -44441,6 +44482,7 @@ function createCore(ports = {}) {
44441
44482
  }
44442
44483
  }
44443
44484
  for (const spec of input.ranges) {
44485
+ if (skipSpecs.has(spec)) continue;
44444
44486
  try {
44445
44487
  const outcome = applySingleRange({
44446
44488
  spec,
@@ -44581,13 +44623,15 @@ var recommendNode = {
44581
44623
  const protectedRefs = computeProtectedRefs(
44582
44624
  io2.messages,
44583
44625
  io2.state,
44584
- ctx.config
44626
+ ctx.config,
44627
+ ctx.countTokens
44585
44628
  );
44586
44629
  const contextRanges = buildCompressibleRanges(
44587
44630
  io2.messages,
44588
44631
  io2.state,
44589
44632
  ctx.config,
44590
- protectedRefs
44633
+ protectedRefs,
44634
+ ctx.countTokens
44591
44635
  );
44592
44636
  const nothingToCompress = contextRanges.compressible.length === 0;
44593
44637
  const recommendation = {
@@ -44765,7 +44809,9 @@ function applySingleRange(input) {
44765
44809
  survivedCount: 0,
44766
44810
  generation: "young",
44767
44811
  active: true,
44768
- compressCallId: input.spec.compressCallId
44812
+ compressCallId: input.spec.compressCallId,
44813
+ startRef: input.spec.startRef,
44814
+ endRef: input.spec.endRef
44769
44815
  };
44770
44816
  input.state.blocks.push(block);
44771
44817
  for (const consumedId of consumedBlockIds) {
@@ -45256,7 +45302,7 @@ function renderNudgeText(decision) {
45256
45302
  breakdownStr,
45257
45303
  "",
45258
45304
  `[TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"} TRIGGER]`,
45259
- isT2 ? `Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries.` : `Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries.`,
45305
+ isT2 ? `Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-2 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 2 distillation rules to the existing summaries, so the whole span is covered and nothing is lost.` : `Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-3 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 3 condensation rules to the existing summaries, so the whole span is covered and nothing is lost.`,
45260
45306
  blockList,
45261
45307
  `Example: compress({ content: [{ startId: "${startId}", endId: "${endId}", summary: "..." }] })`,
45262
45308
  "",
@@ -46045,7 +46091,7 @@ function resolveProxyDecision(routes, globalProxy, upstreamUrl, fallback = {}) {
46045
46091
  return { proxy: parsed.url, source: "provider" };
46046
46092
  }
46047
46093
  }
46048
- if (globalProxy === "") return { source: "direct" };
46094
+ if (globalProxy === "" && fallback.explicitDirect) return { source: "direct" };
46049
46095
  const explicit = parseHttpProxy(globalProxy, fallback.biliPort)?.url;
46050
46096
  if (explicit) return { proxy: explicit, source: fallback.globalSource ?? "global" };
46051
46097
  if (target && matchesNoProxy(target, fallback.noProxy)) return { source: "no-proxy" };
@@ -46283,34 +46329,19 @@ function loadRoutes(env = process.env) {
46283
46329
  function loadOptions(env = process.env) {
46284
46330
  const fileConfig = loadConfigFile();
46285
46331
  const port = parseInt(env.ACP_PORT ?? env.PORT ?? `${fileConfig.port ?? 8787}`, 10);
46332
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
46333
+ throw new Error(`Invalid port ${Number.isNaN(port) ? "(not a number)" : port}; must be 1-65535`);
46334
+ }
46286
46335
  const host = env.ACP_HOST ?? fileConfig.host ?? "127.0.0.1";
46287
46336
  const upstream = (env.ACP_UPSTREAM ?? fileConfig.upstream ?? "https://api.anthropic.com").replace(/\/$/, "");
46288
- let routes = {};
46289
- const routesPath = env.ACP_PROVIDERS ?? fileConfig.providersPath ?? "";
46290
- if (routesPath) {
46291
- const parsed = safeReadJson(routesPath);
46292
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
46293
- for (const [k2, v2] of Object.entries(parsed)) {
46294
- rejectLegacyRoute(k2, v2);
46295
- const route = parseRouteEntry(v2);
46296
- if (route) routes[normalizeUrlKey(k2)] = route;
46297
- }
46298
- }
46299
- }
46300
- if (fileConfig.providers) {
46301
- for (const [k2, v2] of Object.entries(fileConfig.providers)) {
46302
- rejectLegacyRoute(k2, v2);
46303
- const route = parseRouteEntry(v2);
46304
- if (route && !routes[normalizeUrlKey(k2)]) routes[normalizeUrlKey(k2)] = route;
46305
- }
46306
- }
46337
+ const routes = loadRoutes(env);
46307
46338
  const modelContextLimit = parseInt(env.ACP_MODEL_CONTEXT_LIMIT ?? `${fileConfig.modelContextLimit ?? 2e5}`, 10);
46308
46339
  const biliProxy = nonEmpty(env.BILI_UPSTREAM_PROXY);
46309
46340
  const webProxy = nonEmpty(fileConfig.upstreamProxy);
46310
46341
  const configProxy = nonEmpty(fileConfig.proxy);
46311
- const proxyMode = parseUpstreamProxyMode(
46312
- env.BILI_UPSTREAM_PROXY_MODE ?? fileConfig.upstreamProxyMode ?? (webProxy ? "manual" : void 0)
46313
- );
46342
+ const rawProxyMode = env.BILI_UPSTREAM_PROXY_MODE ?? fileConfig.upstreamProxyMode ?? (webProxy ? "manual" : void 0);
46343
+ const proxyMode = parseUpstreamProxyMode(rawProxyMode);
46344
+ const explicitDirect = proxyMode === "direct" && rawProxyMode === "direct";
46314
46345
  const proxy = biliProxy ?? (proxyMode === "direct" ? "" : proxyMode === "manual" ? webProxy ?? configProxy : configProxy);
46315
46346
  const proxySource = biliProxy ? "bili-env" : proxyMode === "direct" ? "direct" : proxyMode === "manual" && webProxy ? "web-manual" : configProxy ? "config" : "auto";
46316
46347
  const httpProxy = nonEmpty(env.HTTP_PROXY ?? env.http_proxy);
@@ -46322,8 +46353,9 @@ function loadOptions(env = process.env) {
46322
46353
  ...httpsProxy ? { httpsProxy } : {},
46323
46354
  ...allProxy ? { allProxy } : {},
46324
46355
  ...noProxy ? { noProxy } : {},
46325
- biliPort: Number.isFinite(port) ? port : 8787,
46326
- globalSource: proxySource
46356
+ biliPort: port,
46357
+ globalSource: proxySource,
46358
+ explicitDirect
46327
46359
  };
46328
46360
  validateHttpProxy(proxy, proxyFallback.biliPort);
46329
46361
  for (const [url, route] of Object.entries(routes)) {
@@ -46334,7 +46366,7 @@ function loadOptions(env = process.env) {
46334
46366
  }
46335
46367
  }
46336
46368
  return {
46337
- port: Number.isFinite(port) ? port : 8787,
46369
+ port,
46338
46370
  host,
46339
46371
  upstream,
46340
46372
  routes,
@@ -46544,15 +46576,30 @@ async function contextFromRegistry(model, host) {
46544
46576
  // src/fetch-util.ts
46545
46577
  var MAX_REQUEST_BYTES = 100 * 1024 * 1024;
46546
46578
  var UPSTREAM_TIMEOUT_MS = 10 * 60 * 1e3;
46547
- async function fetchWithTimeout(url, opts, timeoutMs = UPSTREAM_TIMEOUT_MS) {
46579
+ async function fetchWithTimeout(url, opts, timeoutMs = UPSTREAM_TIMEOUT_MS, externalSignal) {
46548
46580
  const controller = new AbortController();
46549
46581
  const timer2 = setTimeout(() => controller.abort(), timeoutMs);
46582
+ let onExternalAbort = null;
46583
+ if (externalSignal) {
46584
+ if (externalSignal.aborted) controller.abort();
46585
+ else {
46586
+ onExternalAbort = () => controller.abort();
46587
+ externalSignal.addEventListener("abort", onExternalAbort, { once: true });
46588
+ }
46589
+ }
46550
46590
  try {
46551
46591
  const finalOpts = { ...opts, signal: controller.signal };
46552
46592
  const response = await fetch(url, finalOpts);
46553
- return { response, clearTimer: () => clearTimeout(timer2) };
46593
+ return {
46594
+ response,
46595
+ clearTimer: () => {
46596
+ clearTimeout(timer2);
46597
+ if (onExternalAbort && externalSignal) externalSignal.removeEventListener("abort", onExternalAbort);
46598
+ }
46599
+ };
46554
46600
  } catch (e) {
46555
46601
  clearTimeout(timer2);
46602
+ if (onExternalAbort && externalSignal) externalSignal.removeEventListener("abort", onExternalAbort);
46556
46603
  throw e;
46557
46604
  }
46558
46605
  }
@@ -47562,7 +47609,7 @@ function getStore() {
47562
47609
 
47563
47610
  // src/session.ts
47564
47611
  var sessions = /* @__PURE__ */ new Map();
47565
- var MAX_SESSIONS = Number.parseInt(process.env.BILI_MAX_SESSIONS ?? "256", 10) || 256;
47612
+ var MAX_SESSIONS = Math.max(1, Number.parseInt(process.env.BILI_MAX_SESSIONS ?? "256", 10) || 256);
47566
47613
  var initialized = false;
47567
47614
  async function initSessions() {
47568
47615
  if (initialized) return;
@@ -47597,7 +47644,12 @@ function getSession(id, meta) {
47597
47644
  sessions.set(id, reloaded);
47598
47645
  return reloaded;
47599
47646
  }
47600
- if (sessions.size >= MAX_SESSIONS) evictOldest();
47647
+ if (sessions.size >= MAX_SESSIONS) {
47648
+ const evicted = evictOldest();
47649
+ if (!evicted) {
47650
+ throw new Error(`session pool exhausted (MAX_SESSIONS=${MAX_SESSIONS}; all in-flight)`);
47651
+ }
47652
+ }
47601
47653
  const session = {
47602
47654
  id,
47603
47655
  meta: { protocol: meta?.protocol, upstreamOrigin: meta?.upstreamOrigin, label: meta?.label },
@@ -47681,13 +47733,14 @@ function evictOldest() {
47681
47733
  oldestId = id;
47682
47734
  }
47683
47735
  }
47684
- if (!oldestId) return;
47736
+ if (!oldestId) return false;
47685
47737
  const s3 = sessions.get(oldestId);
47686
47738
  const ok = getStore().flushSync(s3);
47687
47739
  if (!ok && !s3.persisted) {
47688
- return;
47740
+ return false;
47689
47741
  }
47690
47742
  sessions.delete(oldestId);
47743
+ return true;
47691
47744
  }
47692
47745
  async function flushAllSessions() {
47693
47746
  await getStore().flushAll(sessions.values());
@@ -47968,15 +48021,38 @@ var MUTATING_PROXY_TOOLS = /* @__PURE__ */ new Set([
47968
48021
  COMPRESS_TOOL_NAME,
47969
48022
  DECOMPRESS_TOOL_NAME
47970
48023
  ]);
47971
- var READONLY_PROXY_TOOLS = /* @__PURE__ */ new Set([
47972
- SEARCH_CONTEXT_TOOL_NAME,
47973
- ACP_STATUS_TOOL_NAME
47974
- ]);
47975
48024
 
47976
48025
  // src/decompress-shared.ts
47977
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "fs";
48026
+ import { mkdirSync as mkdirSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
47978
48027
  import { dirname as dirname3, join as join2 } from "path";
47979
48028
  import { tmpdir } from "os";
48029
+ var trackedTempFiles = [];
48030
+ function getDecompressTmpCap() {
48031
+ const raw = process.env.BILI_DECOMPRESS_TMP_CAP;
48032
+ const parsed = raw ? Number.parseInt(raw, 10) : NaN;
48033
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 50;
48034
+ }
48035
+ function reapTempFiles() {
48036
+ const cap = getDecompressTmpCap();
48037
+ while (trackedTempFiles.length > cap) {
48038
+ trackedTempFiles.sort((a, b2) => a.mtimeMs - b2.mtimeMs);
48039
+ const oldest = trackedTempFiles.shift();
48040
+ if (!oldest) break;
48041
+ try {
48042
+ unlinkSync2(oldest.path);
48043
+ } catch {
48044
+ }
48045
+ }
48046
+ }
48047
+ process.on("beforeExit", () => {
48048
+ for (const f2 of trackedTempFiles) {
48049
+ try {
48050
+ unlinkSync2(f2.path);
48051
+ } catch {
48052
+ }
48053
+ }
48054
+ trackedTempFiles.length = 0;
48055
+ });
47980
48056
  function resolveDecompress(args, ctx) {
47981
48057
  const rawBlockId = args.blockId;
47982
48058
  if (typeof rawBlockId !== "string" || rawBlockId.length === 0) {
@@ -48009,6 +48085,8 @@ function resolveDecompress(args, ctx) {
48009
48085
  try {
48010
48086
  mkdirSync4(dirname3(outPath), { recursive: true });
48011
48087
  writeFileSync3(outPath, body, "utf8");
48088
+ trackedTempFiles.push({ path: outPath, mtimeMs: Date.now() });
48089
+ reapTempFiles();
48012
48090
  return `${header}
48013
48091
  Content (${body.length} chars) written to: ${outPath}
48014
48092
  Use the read tool to access it.`;
@@ -48022,12 +48100,6 @@ ${body.slice(0, 4e3)}...`;
48022
48100
  ${body}`;
48023
48101
  }
48024
48102
 
48025
- // src/sse-util.ts
48026
- function normalizeSseLineEndings(buf) {
48027
- if (buf.indexOf("\r") === -1) return buf;
48028
- return buf.replace(/\r\n|\r/g, "\n");
48029
- }
48030
-
48031
48103
  // src/stream.ts
48032
48104
  function executeAnthropicProxyTool(toolName, args, ctx) {
48033
48105
  if (toolName === COMPRESS_TOOL_NAME) {
@@ -48191,7 +48263,7 @@ function renderPage(origin, version2) {
48191
48263
  }
48192
48264
 
48193
48265
  // src/web/api.ts
48194
- import { closeSync, fsyncSync, mkdirSync as mkdirSync5, openSync, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
48266
+ import { closeSync, fsyncSync, mkdirSync as mkdirSync5, openSync, renameSync as renameSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "fs";
48195
48267
  import { dirname as dirname4 } from "path";
48196
48268
  import { randomUUID } from "crypto";
48197
48269
  function readConfig() {
@@ -48224,7 +48296,7 @@ function atomicWriteConfig(config) {
48224
48296
  } catch (error) {
48225
48297
  if (descriptor !== void 0) closeSync(descriptor);
48226
48298
  try {
48227
- unlinkSync2(tempPath);
48299
+ unlinkSync3(tempPath);
48228
48300
  } catch {
48229
48301
  }
48230
48302
  throw error;
@@ -48372,122 +48444,6 @@ function reapOrphanBlocks(session, visible, deactivate) {
48372
48444
  }
48373
48445
 
48374
48446
  // src/compress-loop.ts
48375
- function executeProxyTool(toolName, args, ctx) {
48376
- if (toolName === "compress") {
48377
- return applyRanges(parseCompressInput(args), ctx);
48378
- }
48379
- if (toolName === "decompress") {
48380
- return resolveDecompress(args, ctx);
48381
- }
48382
- if (toolName === "search_context") {
48383
- const query = typeof args.query === "string" ? args.query : "";
48384
- if (query.length === 0) return "[search_context FAILED: query is required]";
48385
- const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 5;
48386
- const blocks = ctx.core.search(query, ctx.session.state).slice(0, limit);
48387
- if (blocks.length === 0) return `[No blocks matched "${query}"]`;
48388
- const lines = blocks.map((b2) => {
48389
- const topic = b2.topic ?? "(no topic)";
48390
- const preview = b2.summary.length > 200 ? b2.summary.slice(0, 200) + "..." : b2.summary;
48391
- return `${b2.blockId} (T${b2.tier}) "${topic}"
48392
- ${preview}`;
48393
- });
48394
- return `Found ${blocks.length} block(s) for "${query}":
48395
-
48396
- ${lines.join("\n\n")}`;
48397
- }
48398
- if (toolName === "acp_status") {
48399
- return buildStatusReport(ctx.session.state, ctx.messages, estimateTokensFast);
48400
- }
48401
- return `[Unknown proxy tool: ${toolName}]`;
48402
- }
48403
- function classifySseEvent(eventStr) {
48404
- const dataLine = eventStr.split("\n").find((l) => l.startsWith("data:"));
48405
- if (!dataLine) return {};
48406
- const jsonStr = dataLine.slice(5).trim();
48407
- if (jsonStr === "[DONE]") return { done: true };
48408
- let parsed;
48409
- try {
48410
- parsed = JSON.parse(jsonStr);
48411
- } catch {
48412
- return {};
48413
- }
48414
- const choices = parsed.choices;
48415
- const choice = choices?.[0];
48416
- if (!choice) return {};
48417
- const delta = choice.delta;
48418
- const finishReason = choice.finish_reason;
48419
- const out = {};
48420
- if (finishReason) {
48421
- out.finishReason = finishReason;
48422
- out.usage = parsed.usage ?? null;
48423
- }
48424
- if (!delta) return out;
48425
- if (delta.tool_calls) {
48426
- const tcs = delta.tool_calls;
48427
- const toolCalls = [];
48428
- for (const tc of tcs) {
48429
- const idx = typeof tc.index === "number" ? tc.index : 0;
48430
- const fn = tc.function;
48431
- const name = typeof fn?.name === "string" ? fn.name : "";
48432
- const id = typeof tc.id === "string" ? tc.id : "";
48433
- const args = typeof fn?.arguments === "string" ? fn.arguments : "";
48434
- toolCalls.push({ index: idx, id, name, arguments: args });
48435
- }
48436
- if (typeof delta.content === "string" && delta.content.length > 0) {
48437
- out.contentDelta = delta.content;
48438
- }
48439
- out.toolCalls = toolCalls;
48440
- return out;
48441
- }
48442
- if (typeof delta.content === "string" && delta.content.length > 0) {
48443
- out.contentDelta = delta.content;
48444
- out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
48445
- return out;
48446
- }
48447
- if (delta.role || Object.keys(delta).length === 0 && !finishReason) {
48448
- out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
48449
- }
48450
- return out;
48451
- }
48452
- function buildToolCallSse(base, tc) {
48453
- return `data: ${JSON.stringify({
48454
- ...base,
48455
- choices: [{
48456
- index: 0,
48457
- delta: {
48458
- tool_calls: [{
48459
- index: tc.index,
48460
- id: tc.id,
48461
- type: "function",
48462
- function: { name: tc.name, arguments: tc.arguments }
48463
- }]
48464
- },
48465
- finish_reason: null
48466
- }]
48467
- })}
48468
-
48469
- `;
48470
- }
48471
- function buildFinishSse(base, finishReason, usage) {
48472
- return `data: ${JSON.stringify({
48473
- ...base,
48474
- choices: [{ index: 0, delta: {}, finish_reason: finishReason }],
48475
- ...usage ? { usage } : {}
48476
- })}
48477
-
48478
- `;
48479
- }
48480
- function buildContentSse(id, model, content) {
48481
- return `data: ${JSON.stringify({
48482
- id,
48483
- object: "chat.completion.chunk",
48484
- created: Date.now(),
48485
- model,
48486
- choices: [{ index: 0, delta: { content }, finish_reason: null }]
48487
- })}
48488
-
48489
- `;
48490
- }
48491
48447
  function buildVisibilityMarker(toolName, result) {
48492
48448
  const lines = result.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
48493
48449
  const failed = lines.some(
@@ -48500,10 +48456,10 @@ function buildVisibilityMarker(toolName, result) {
48500
48456
  acp_status: "\u{1F4CA}"
48501
48457
  };
48502
48458
  const icon = failed ? "\u274C" : icons[toolName] ?? "\u{1F4E6}";
48503
- if (toolName === "acp_status" && lines.length >= 2) {
48504
- const dataLine = lines.slice(0, 3).join(" | ").replace(/\s+/g, " ");
48459
+ if (toolName === "acp_status") {
48505
48460
  return `
48506
- ${icon} [ACP] ${dataLine}
48461
+ ${icon} [ACP] acp_status result:
48462
+ ${result.trim()}
48507
48463
  `;
48508
48464
  }
48509
48465
  const inner = (lines[0] ?? "").replace(/^\[/, "").replace(/\]$/, "").trim();
@@ -48511,250 +48467,12 @@ ${icon} [ACP] ${dataLine}
48511
48467
  ${icon} [ACP] ${inner}
48512
48468
  `;
48513
48469
  }
48514
- async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOptions) {
48515
- let upstream = initialUpstream;
48516
- let activeClearTimer = null;
48517
- try {
48518
- const model = requestBody.model ?? "unknown";
48519
- let responseId = `chatcmpl-proxy-${Date.now()}`;
48520
- const makeBase = () => ({
48521
- id: responseId,
48522
- object: "chat.completion.chunk",
48523
- created: Date.now(),
48524
- model
48525
- });
48526
- let loopCount = 0;
48527
- for (; ; ) {
48528
- loopCount++;
48529
- if (loopCount > 10) {
48530
- ctx.log("[acp-proxy: compress loop limit (10) reached, forwarding as-is]");
48531
- yield Buffer.from(buildFinishSse(makeBase(), "stop", null), "utf8");
48532
- yield Buffer.from("data: [DONE]\n\n", "utf8");
48533
- return;
48534
- }
48535
- const toolCallByIndex = /* @__PURE__ */ new Map();
48536
- let contentText = "";
48537
- let finishReason = null;
48538
- let usage = null;
48539
- const isFirstRound = loopCount === 1;
48540
- const reader = upstream.getReader();
48541
- const decoder = new TextDecoder("utf-8");
48542
- let sseBuffer = "";
48543
- try {
48544
- for (; ; ) {
48545
- const { done, value } = await reader.read();
48546
- if (done) break;
48547
- sseBuffer += decoder.decode(value, { stream: true });
48548
- sseBuffer = normalizeSseLineEndings(sseBuffer);
48549
- let sep;
48550
- while ((sep = sseBuffer.indexOf("\n\n")) >= 0) {
48551
- const eventStr = sseBuffer.slice(0, sep);
48552
- sseBuffer = sseBuffer.slice(sep + 2);
48553
- if (!eventStr.trim()) continue;
48554
- const d = classifySseEvent(eventStr);
48555
- if (d.done) {
48556
- continue;
48557
- }
48558
- if (isFirstRound) {
48559
- if (d.yieldChunk) {
48560
- if (!responseId) {
48561
- const dataLine = eventStr.split("\n").find((l) => l.startsWith("data:"));
48562
- if (dataLine) {
48563
- try {
48564
- const p2 = JSON.parse(dataLine.slice(5).trim());
48565
- if (typeof p2.id === "string") responseId = p2.id;
48566
- } catch {
48567
- }
48568
- }
48569
- }
48570
- yield d.yieldChunk;
48571
- }
48572
- } else {
48573
- if (d.contentDelta) {
48574
- yield Buffer.from(buildContentSse(responseId, model, d.contentDelta), "utf8");
48575
- }
48576
- }
48577
- if (d.contentDelta) contentText += d.contentDelta;
48578
- if (d.finishReason) finishReason = d.finishReason;
48579
- if (d.usage !== void 0) usage = d.usage;
48580
- if (d.toolCalls) {
48581
- for (const tc of d.toolCalls) {
48582
- const existing = toolCallByIndex.get(tc.index);
48583
- if (existing) {
48584
- if (tc.name) existing.name = tc.name;
48585
- if (tc.id) existing.id = tc.id;
48586
- existing.arguments += tc.arguments;
48587
- } else {
48588
- toolCallByIndex.set(tc.index, tc);
48589
- }
48590
- }
48591
- }
48592
- }
48593
- }
48594
- sseBuffer += decoder.decode();
48595
- sseBuffer = normalizeSseLineEndings(sseBuffer);
48596
- let resSep;
48597
- while ((resSep = sseBuffer.indexOf("\n\n")) >= 0) {
48598
- const eventStr = sseBuffer.slice(0, resSep);
48599
- sseBuffer = sseBuffer.slice(resSep + 2);
48600
- if (!eventStr.trim()) continue;
48601
- const d = classifySseEvent(eventStr);
48602
- if (d.done) continue;
48603
- if (isFirstRound) {
48604
- if (d.yieldChunk) yield d.yieldChunk;
48605
- } else {
48606
- if (d.contentDelta) yield Buffer.from(buildContentSse(responseId, model, d.contentDelta), "utf8");
48607
- }
48608
- if (d.contentDelta) contentText += d.contentDelta;
48609
- if (d.finishReason) finishReason = d.finishReason;
48610
- if (d.usage !== void 0) usage = d.usage;
48611
- if (d.toolCalls) {
48612
- for (const tc of d.toolCalls) {
48613
- const existing = toolCallByIndex.get(tc.index);
48614
- if (existing) {
48615
- if (tc.name) existing.name = tc.name;
48616
- if (tc.id) existing.id = tc.id;
48617
- existing.arguments += tc.arguments;
48618
- } else {
48619
- toolCallByIndex.set(tc.index, tc);
48620
- }
48621
- }
48622
- }
48623
- }
48624
- } finally {
48625
- reader.releaseLock();
48626
- }
48627
- const sortedIndices = [...toolCallByIndex.keys()].sort((a, b2) => a - b2);
48628
- const toolCalls = sortedIndices.map((i) => {
48629
- const tc = toolCallByIndex.get(i);
48630
- return { ...tc, id: tc.id || `call_${tc.index}` };
48631
- }).filter((tc) => tc.name.length > 0);
48632
- const proxyCalls = toolCalls.filter((tc) => PROXY_TOOL_NAMES.has(tc.name));
48633
- const realCalls = toolCalls.filter((tc) => !PROXY_TOOL_NAMES.has(tc.name));
48634
- const mutatingProxy = proxyCalls.filter((tc) => MUTATING_PROXY_TOOLS.has(tc.name));
48635
- const readonlyProxy = proxyCalls.filter((tc) => READONLY_PROXY_TOOLS.has(tc.name));
48636
- const hasMutatingOnly = mutatingProxy.length > 0 && realCalls.length === 0;
48637
- if (usage) {
48638
- const prompt = usage.prompt_tokens ?? usage.input_tokens;
48639
- const det = usage.prompt_tokens_details ?? usage.prompt_cache_hit_tokens;
48640
- const cached = det?.cached_tokens ?? usage.prompt_cache_hit_tokens;
48641
- const out = usage.completion_tokens ?? usage.output_tokens;
48642
- if (typeof prompt === "number") {
48643
- const ch = typeof cached === "number" ? cached : 0;
48644
- log("info", `[acp-usage] round ${loopCount} input=${prompt} cached=${typeof cached === "number" ? cached : "?"} output=${out ?? "?"}${ch > 0 ? ` (cache hit ${Math.round(ch / prompt * 100)}%)` : ""}`);
48645
- ctx.session.stats.inputTokens += prompt;
48646
- ctx.session.stats.lastInputTokens = prompt + (typeof cached === "number" ? cached : 0);
48647
- if (typeof cached === "number") ctx.session.stats.cachedTokens += cached;
48648
- if (typeof out === "number") ctx.session.stats.outputTokens += out;
48649
- ctx.session.stats.cacheSamples += 1;
48650
- }
48651
- }
48652
- if (!hasMutatingOnly) {
48653
- for (const tc of readonlyProxy) {
48654
- let args = {};
48655
- try {
48656
- args = JSON.parse(tc.arguments);
48657
- } catch {
48658
- args = {};
48659
- }
48660
- let result;
48661
- try {
48662
- result = executeProxyTool(tc.name, args, ctx);
48663
- } catch (e) {
48664
- result = `[${tc.name} FAILED: ${e instanceof Error ? e.message : String(e)}]`;
48665
- }
48666
- const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
48667
- ctx.log(`[acp-proxy: ${tc.name} (${tc.id}) \u2192 ${preview.replace(/\n/g, " ")}]`);
48668
- yield Buffer.from(
48669
- buildContentSse(responseId, model, buildVisibilityMarker(tc.name, result)),
48670
- "utf8"
48671
- );
48672
- }
48673
- for (const tc of realCalls) {
48674
- yield Buffer.from(buildToolCallSse(makeBase(), tc), "utf8");
48675
- }
48676
- const fr2 = realCalls.length > 0 ? "tool_calls" : finishReason ?? "stop";
48677
- yield Buffer.from(buildFinishSse(makeBase(), fr2, usage), "utf8");
48678
- yield Buffer.from("data: [DONE]\n\n", "utf8");
48679
- return;
48680
- }
48681
- const names = proxyCalls.map((c) => c.name).join(", ");
48682
- ctx.log(`[acp-proxy: round ${loopCount} \u2014 ${proxyCalls.length} proxy call(s): ${names}]`);
48683
- const messages = requestBody.messages ?? [];
48684
- messages.push({
48685
- role: "assistant",
48686
- content: contentText || null,
48687
- tool_calls: proxyCalls.map((tc) => ({
48688
- id: tc.id,
48689
- type: "function",
48690
- function: { name: tc.name, arguments: tc.arguments }
48691
- }))
48692
- });
48693
- for (const tc of proxyCalls) {
48694
- let args = {};
48695
- try {
48696
- args = JSON.parse(tc.arguments);
48697
- } catch {
48698
- args = {};
48699
- }
48700
- const result = executeProxyTool(tc.name, args, ctx);
48701
- const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
48702
- ctx.log(`[acp-proxy: ${tc.name} (${tc.id}) \u2192 ${preview.replace(/\n/g, " ")}]`);
48703
- yield Buffer.from(
48704
- buildContentSse(responseId, model, buildVisibilityMarker(tc.name, result)),
48705
- "utf8"
48706
- );
48707
- messages.push({
48708
- role: "tool",
48709
- tool_call_id: tc.id,
48710
- content: result
48711
- });
48712
- }
48713
- requestBody.messages = messages;
48714
- const { response: resp, clearTimer } = await fetchWithTimeout(requestOptions.url, {
48715
- method: "POST",
48716
- headers: requestOptions.headers,
48717
- body: JSON.stringify(requestBody),
48718
- ...ctx.proxyUrl ? { dispatcher: proxyDispatcher(ctx.proxyUrl) } : {}
48719
- });
48720
- if (!resp.ok || !resp.body) {
48721
- const errText = await resp.text().catch(() => "upstream error");
48722
- ctx.log(`[acp-proxy: compress loop upstream error ${resp.status}: ${errText.slice(0, 200)}]`);
48723
- yield Buffer.from(
48724
- `data: ${JSON.stringify({
48725
- ...makeBase(),
48726
- choices: [{
48727
- index: 0,
48728
- delta: { content: `
48729
- [acp-proxy: upstream error ${resp.status}: ${errText.slice(0, 200)}]
48730
- ` },
48731
- finish_reason: null
48732
- }]
48733
- })}
48734
-
48735
- `,
48736
- "utf8"
48737
- );
48738
- yield Buffer.from(buildFinishSse(makeBase(), "stop", null), "utf8");
48739
- yield Buffer.from("data: [DONE]\n\n", "utf8");
48740
- return;
48741
- }
48742
- upstream = resp.body;
48743
- if (activeClearTimer) activeClearTimer();
48744
- activeClearTimer = clearTimer;
48745
- }
48746
- } finally {
48747
- if (activeClearTimer) {
48748
- activeClearTimer();
48749
- activeClearTimer = null;
48750
- }
48751
- }
48752
- }
48753
48470
 
48754
- // src/compress-loop-anthropic.ts
48755
- function executeProxyTool2(toolName, args, ctx) {
48471
+ // src/loop/core.ts
48472
+ var MAX_LOOP_ROUNDS = 10;
48473
+ function executeProxyTool(toolName, args, ctx, callId) {
48756
48474
  if (toolName === "compress") {
48757
- return applyRanges(parseCompressInput(args), ctx);
48475
+ return applyRanges(parseCompressInput(args, callId), ctx);
48758
48476
  }
48759
48477
  if (toolName === "decompress") {
48760
48478
  return resolveDecompress(args, ctx);
@@ -48776,941 +48494,62 @@ function executeProxyTool2(toolName, args, ctx) {
48776
48494
  ${lines.join("\n\n")}`;
48777
48495
  }
48778
48496
  if (toolName === "acp_status") {
48779
- return buildStatusReport(ctx.session.state, ctx.messages, estimateTokensFast);
48497
+ return handleAcpStatus(args, ctx);
48780
48498
  }
48781
48499
  return `[Unknown proxy tool: ${toolName}]`;
48782
48500
  }
48783
- function parseAnthropicSse(eventStr) {
48784
- const lines = eventStr.split("\n");
48785
- let type = "";
48786
- const dataLines = [];
48787
- for (const l of lines) {
48788
- if (l.startsWith("event:")) {
48789
- type = l.slice(6).trim();
48790
- } else if (l.startsWith("data:")) {
48791
- dataLines.push(l.slice(5).replace(/^ /, ""));
48792
- }
48793
- }
48794
- if (!type) return null;
48795
- const jsonStr = dataLines.join("\n").trim();
48796
- if (!jsonStr) return { type, data: {} };
48797
- try {
48798
- return { type, data: JSON.parse(jsonStr) };
48799
- } catch {
48800
- return { type, data: {} };
48501
+ function handleAcpStatus(args, ctx) {
48502
+ const scope = typeof args.scope === "string" ? args.scope : void 0;
48503
+ const view = typeof args.view === "string" ? args.view : void 0;
48504
+ const tool = typeof args.tool === "string" ? args.tool : void 0;
48505
+ const sort = typeof args.sort === "string" ? args.sort : void 0;
48506
+ const limit = typeof args.limit === "number" ? args.limit : void 0;
48507
+ const base = buildStatusReport(ctx.session.state, ctx.messages, estimateTokensFast, { scope, view, tool, sort, limit });
48508
+ if (scope) return base;
48509
+ const nudge = ctx.nudge;
48510
+ const ranges = nudge?.compressibleRanges ?? [];
48511
+ const protectedRanges = nudge?.protectedRanges ?? [];
48512
+ const extra = [];
48513
+ if (nudge) {
48514
+ extra.push("");
48515
+ extra.push(nudge.shouldInject ? `Nudge: ACTIVE \u2014 ${nudge.reason}` : `Nudge: idle \u2014 ${nudge.reason}`);
48516
+ }
48517
+ if (ranges.length > 0 || protectedRanges.length > 0) {
48518
+ extra.push("");
48519
+ extra.push(formatRanges(ranges, protectedRanges));
48520
+ }
48521
+ return extra.length > 0 ? `${base}
48522
+ ${extra.join("\n")}` : base;
48523
+ }
48524
+ function recordUsage(ctx, usage, round) {
48525
+ const prompt = usage.inputTokens;
48526
+ const cached = usage.cachedTokens;
48527
+ const out = usage.outputTokens;
48528
+ if (typeof prompt === "number") ctx.session.stats.inputTokens += prompt;
48529
+ ctx.session.stats.lastInputTokens = (typeof prompt === "number" ? prompt : 0) + (typeof cached === "number" ? cached : 0);
48530
+ if (typeof cached === "number") {
48531
+ ctx.session.stats.cachedTokens += cached;
48532
+ ctx.session.stats.cacheSamples += 1;
48801
48533
  }
48534
+ if (typeof out === "number") ctx.session.stats.outputTokens += out;
48535
+ const hitPct = typeof prompt === "number" && typeof cached === "number" && prompt + cached > 0 ? Math.round(cached / (prompt + cached) * 100) : 0;
48536
+ ctx.log(
48537
+ `[acp-usage] round ${round} input=${ctx.session.stats.lastInputTokens} cached=${cached ?? 0} (cache hit ${hitPct}%)`
48538
+ );
48802
48539
  }
48803
- function buildTextBlockSse(index, text) {
48804
- return `event: content_block_start
48805
- data: ${JSON.stringify({ type: "content_block_start", index, content_block: { type: "text", text: "" } })}
48806
-
48807
- event: content_block_delta
48808
- data: ${JSON.stringify({ type: "content_block_delta", index, delta: { type: "text_delta", text } })}
48809
-
48810
- event: content_block_stop
48811
- data: ${JSON.stringify({ type: "content_block_stop", index })}
48812
-
48813
- `;
48814
- }
48815
- function buildTerminalSse(stopReason, outputTokens, inputTokens, cachedTokens, messageId, model) {
48816
- const usage = {
48817
- input_tokens: inputTokens,
48818
- output_tokens: outputTokens,
48819
- cache_read_input_tokens: cachedTokens
48820
- };
48821
- const extra = {};
48822
- if (messageId) extra.id = messageId;
48823
- if (model) extra.model = model;
48824
- return `event: message_delta
48825
- data: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: stopReason, stop_sequence: null }, usage, ...extra })}
48826
-
48827
- event: message_stop
48828
- data: ${JSON.stringify({ type: "message_stop" })}
48829
-
48830
- `;
48831
- }
48832
- function remapIndex(json, oldIndex, newIndex) {
48833
- return json.replaceAll(`"index":${oldIndex}`, `"index":${newIndex}`).replaceAll(`"index": ${oldIndex}`, `"index": ${newIndex}`);
48834
- }
48835
- function safeParse2(s3) {
48836
- try {
48837
- const v2 = JSON.parse(s3);
48838
- return typeof v2 === "object" && v2 ? v2 : {};
48839
- } catch {
48840
- return {};
48841
- }
48842
- }
48843
- async function* compressLoopAnthropicStream(initialUpstream, ctx, requestBody, requestOptions) {
48844
- let upstream = initialUpstream;
48845
- let activeClearTimer = null;
48846
- try {
48847
- const model = requestBody.model ?? void 0;
48848
- let messageId;
48849
- let clientIndex = 0;
48850
- let totalOutputTokens = 0;
48851
- let totalInputTokens = 0;
48852
- let totalCachedTokens = 0;
48853
- for (let loopCount = 1; ; loopCount++) {
48854
- if (loopCount > 10) {
48855
- ctx.log("[acp-proxy: anthropic compress loop limit (10) reached, finishing]");
48856
- yield Buffer.from(buildTerminalSse("end_turn", totalOutputTokens, totalInputTokens, totalCachedTokens, messageId, model), "utf8");
48857
- return;
48858
- }
48859
- const isFirstRound = loopCount === 1;
48860
- const state = { clientIndex, toolBlocks: /* @__PURE__ */ new Map(), indexMap: /* @__PURE__ */ new Map() };
48861
- let hasRealToolUse = false;
48862
- let roundText = "";
48863
- let roundStopReason;
48864
- const reader = upstream.getReader();
48865
- const decoder = new TextDecoder("utf-8");
48866
- let sseBuffer = "";
48867
- const cbs = {
48868
- onRealToolUse: () => {
48869
- hasRealToolUse = true;
48870
- },
48871
- onText: (t) => {
48872
- roundText += t;
48873
- },
48874
- onOutputTokens: (n) => {
48875
- totalOutputTokens += n;
48876
- },
48877
- onMessageId: (id) => {
48878
- if (!messageId) messageId = id;
48879
- },
48880
- onStopReason: (r) => {
48881
- roundStopReason = r;
48882
- },
48883
- onCacheUsage: (input, cached) => {
48884
- if (typeof input === "number") {
48885
- ctx.session.stats.inputTokens += input;
48886
- ctx.session.stats.lastInputTokens = input + (typeof cached === "number" ? cached : 0);
48887
- totalInputTokens += input;
48888
- }
48889
- if (typeof cached === "number") {
48890
- ctx.session.stats.cachedTokens += cached;
48891
- ctx.session.stats.cacheSamples += 1;
48892
- totalCachedTokens += cached;
48893
- }
48894
- }
48895
- };
48896
- try {
48897
- for (; ; ) {
48898
- const { done, value } = await reader.read();
48899
- if (done) break;
48900
- sseBuffer += decoder.decode(value, { stream: true });
48901
- sseBuffer = normalizeSseLineEndings(sseBuffer);
48902
- let sep;
48903
- while ((sep = sseBuffer.indexOf("\n\n")) >= 0) {
48904
- const eventStr = sseBuffer.slice(0, sep);
48905
- sseBuffer = sseBuffer.slice(sep + 2);
48906
- if (!eventStr.trim()) continue;
48907
- for (const b2 of routeAnthropicEvent(eventStr, isFirstRound, state, cbs)) {
48908
- yield b2;
48909
- }
48910
- }
48911
- }
48912
- sseBuffer += decoder.decode();
48913
- sseBuffer = normalizeSseLineEndings(sseBuffer);
48914
- let resSep;
48915
- while ((resSep = sseBuffer.indexOf("\n\n")) >= 0) {
48916
- const eventStr = sseBuffer.slice(0, resSep);
48917
- sseBuffer = sseBuffer.slice(resSep + 2);
48918
- if (!eventStr.trim()) continue;
48919
- for (const b2 of routeAnthropicEvent(eventStr, isFirstRound, state, cbs)) {
48920
- yield b2;
48921
- }
48922
- }
48923
- } finally {
48924
- reader.releaseLock();
48925
- }
48926
- clientIndex = state.clientIndex;
48927
- const proxyCalls = [...state.toolBlocks.values()].filter((b2) => PROXY_TOOL_NAMES.has(b2.name));
48928
- const mutatingProxy = proxyCalls.filter((b2) => MUTATING_PROXY_TOOLS.has(b2.name));
48929
- const readonlyProxy = proxyCalls.filter((b2) => READONLY_PROXY_TOOLS.has(b2.name));
48930
- const hasMutatingOnly = mutatingProxy.length > 0 && !hasRealToolUse;
48931
- if (!hasMutatingOnly) {
48932
- for (const tc of readonlyProxy) {
48933
- const args = safeParse2(tc.json);
48934
- let result;
48935
- try {
48936
- result = executeProxyTool2(tc.name, args, ctx);
48937
- } catch (e) {
48938
- result = `[${tc.name} FAILED: ${e instanceof Error ? e.message : String(e)}]`;
48939
- }
48940
- const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
48941
- ctx.log(`[acp-proxy: ${tc.name} (${tc.id}) \u2192 ${preview.replace(/\n/g, " ")}]`);
48942
- yield Buffer.from(buildTextBlockSse(clientIndex, buildVisibilityMarker(tc.name, result)), "utf8");
48943
- clientIndex++;
48944
- }
48945
- const stop = hasRealToolUse ? "tool_use" : roundStopReason ?? "end_turn";
48946
- yield Buffer.from(buildTerminalSse(stop, totalOutputTokens, totalInputTokens, totalCachedTokens, messageId, model), "utf8");
48947
- return;
48948
- }
48949
- const names = proxyCalls.map((c) => c.name).join(", ");
48950
- ctx.log(`[acp-proxy: anthropic round ${loopCount} \u2014 ${proxyCalls.length} proxy call(s): ${names}]`);
48951
- const messages = requestBody.messages ?? [];
48952
- const assistantContent = [];
48953
- if (roundText.length > 0) {
48954
- assistantContent.push({ type: "text", text: roundText });
48955
- }
48956
- for (const tc of proxyCalls) {
48957
- assistantContent.push({ type: "tool_use", id: tc.id, name: tc.name, input: safeParse2(tc.json) });
48958
- }
48959
- messages.push({ role: "assistant", content: assistantContent });
48960
- for (const tc of proxyCalls) {
48961
- const args = safeParse2(tc.json);
48962
- const result = executeProxyTool2(tc.name, args, ctx);
48963
- const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
48964
- ctx.log(`[acp-proxy: ${tc.name} (${tc.id}) \u2192 ${preview.replace(/\n/g, " ")}]`);
48965
- yield Buffer.from(buildTextBlockSse(clientIndex, buildVisibilityMarker(tc.name, result)), "utf8");
48966
- clientIndex++;
48967
- messages.push({
48968
- role: "user",
48969
- content: [{ type: "tool_result", tool_use_id: tc.id, content: result }]
48970
- });
48971
- }
48972
- requestBody.messages = messages;
48973
- const { response: resp, clearTimer } = await fetchWithTimeout(requestOptions.url, {
48974
- method: "POST",
48975
- headers: requestOptions.headers,
48976
- body: JSON.stringify(requestBody),
48977
- ...ctx.proxyUrl ? { dispatcher: proxyDispatcher(ctx.proxyUrl) } : {}
48978
- });
48979
- if (!resp.ok || !resp.body) {
48980
- const errText = await resp.text().catch(() => "upstream error");
48981
- ctx.log(`[acp-proxy: anthropic compress loop upstream error ${resp.status}: ${errText.slice(0, 200)}]`);
48982
- yield Buffer.from(buildTextBlockSse(clientIndex, `
48983
- [acp-proxy: upstream error ${resp.status}: ${errText.slice(0, 200)}]
48984
- `), "utf8");
48985
- yield Buffer.from(buildTerminalSse("end_turn", totalOutputTokens, totalInputTokens, totalCachedTokens, messageId, model), "utf8");
48986
- return;
48987
- }
48988
- upstream = resp.body;
48989
- if (activeClearTimer) activeClearTimer();
48990
- activeClearTimer = clearTimer;
48991
- }
48992
- } finally {
48993
- if (activeClearTimer) {
48994
- activeClearTimer();
48995
- activeClearTimer = null;
48996
- }
48997
- }
48998
- }
48999
- function routeAnthropicEvent(eventStr, isFirstRound, state, cb) {
49000
- const parsed = parseAnthropicSse(eventStr);
49001
- if (!parsed) return [];
49002
- const { type, data } = parsed;
49003
- if (type === "message_start") {
49004
- const msg2 = data.message ?? {};
49005
- if (typeof msg2.id === "string") cb.onMessageId(msg2.id);
49006
- const u2 = msg2.usage ?? {};
49007
- cb.onCacheUsage(u2.input_tokens, u2.cache_read_input_tokens);
49008
- return isFirstRound ? [Buffer.from(eventStr + "\n\n", "utf8")] : [];
49009
- }
49010
- if (type === "ping") {
49011
- return [Buffer.from(eventStr + "\n\n", "utf8")];
49012
- }
49013
- if (type === "content_block_start") {
49014
- const upstreamIndex = data.index ?? 0;
49015
- const block = data.content_block ?? {};
49016
- if (block.type === "tool_use") {
49017
- const name = typeof block.name === "string" ? block.name : "";
49018
- const id = typeof block.id === "string" ? block.id : `toolu_${upstreamIndex}`;
49019
- if (PROXY_TOOL_NAMES.has(name)) {
49020
- state.toolBlocks.set(upstreamIndex, { id, name, json: "" });
49021
- return [];
49022
- }
49023
- cb.onRealToolUse();
49024
- }
49025
- const ci2 = state.clientIndex++;
49026
- state.indexMap.set(upstreamIndex, ci2);
49027
- if (isFirstRound) return [Buffer.from(eventStr + "\n\n", "utf8")];
49028
- return [Buffer.from(remapIndex(eventStr + "\n\n", upstreamIndex, ci2), "utf8")];
49029
- }
49030
- if (type === "content_block_delta") {
49031
- const upstreamIndex = data.index ?? 0;
49032
- const delta = data.delta ?? {};
49033
- if (state.toolBlocks.has(upstreamIndex)) {
49034
- if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
49035
- state.toolBlocks.get(upstreamIndex).json += delta.partial_json;
49036
- }
49037
- return [];
49038
- }
49039
- if (delta.type === "text_delta" && typeof delta.text === "string") cb.onText(delta.text);
49040
- if (isFirstRound) return [Buffer.from(eventStr + "\n\n", "utf8")];
49041
- const ci2 = state.indexMap.get(upstreamIndex) ?? upstreamIndex;
49042
- return [Buffer.from(remapIndex(eventStr + "\n\n", upstreamIndex, ci2), "utf8")];
49043
- }
49044
- if (type === "content_block_stop") {
49045
- const upstreamIndex = data.index ?? 0;
49046
- if (state.toolBlocks.has(upstreamIndex)) return [];
49047
- if (isFirstRound) return [Buffer.from(eventStr + "\n\n", "utf8")];
49048
- const ci2 = state.indexMap.get(upstreamIndex) ?? upstreamIndex;
49049
- return [Buffer.from(remapIndex(eventStr + "\n\n", upstreamIndex, ci2), "utf8")];
49050
- }
49051
- if (type === "message_delta") {
49052
- const u2 = data.usage ?? {};
49053
- const out = u2.output_tokens;
49054
- if (typeof out === "number") cb.onOutputTokens(out);
49055
- cb.onCacheUsage(
49056
- u2.input_tokens,
49057
- u2.cache_read_input_tokens
49058
- );
49059
- const d = data.delta ?? {};
49060
- if (typeof d.stop_reason === "string") cb.onStopReason(d.stop_reason);
49061
- return [];
49062
- }
49063
- if (type === "message_stop") {
49064
- return [];
49065
- }
49066
- return isFirstRound ? [Buffer.from(eventStr + "\n\n", "utf8")] : [];
49067
- }
49068
-
49069
- // src/compress-loop-responses.ts
49070
- var TEXT_PROTOCOL = process.env.ACP_COMPRESS_PROTOCOL === "text";
49071
- function extractTextTriggers(text) {
49072
- const calls = [];
49073
- let clean = "";
49074
- let i = 0;
49075
- let n = 0;
49076
- while (i < text.length) {
49077
- const open = text.indexOf(ACP_TEXT_OPEN, i);
49078
- if (open === -1) {
49079
- clean += text.slice(i);
49080
- break;
49081
- }
49082
- clean += text.slice(i, open);
49083
- const after = open + ACP_TEXT_OPEN.length;
49084
- const close = text.indexOf(ACP_TEXT_CLOSE, after);
49085
- if (close === -1) {
49086
- clean += text.slice(open);
49087
- break;
49088
- }
49089
- const payload = text.slice(after, close).trim();
49090
- if (payload) {
49091
- const stamp = `${Date.now()}_${n++}`;
49092
- calls.push({
49093
- itemId: `fc_text_${stamp}`,
49094
- callId: `call_text_${stamp}`,
49095
- name: COMPRESS_TOOL_NAME,
49096
- arguments: payload
49097
- });
49098
- }
49099
- i = close + ACP_TEXT_CLOSE.length;
49100
- }
49101
- return { clean, calls };
49102
- }
49103
- function executeProxyTool3(toolName, args, ctx) {
49104
- if (toolName === "compress") {
49105
- return applyRanges(parseCompressInput(args), ctx);
49106
- }
49107
- if (toolName === "decompress") {
49108
- return resolveDecompress(args, ctx);
49109
- }
49110
- if (toolName === "search_context") {
49111
- const query = typeof args.query === "string" ? args.query : "";
49112
- if (query.length === 0) return "[search_context FAILED: query is required]";
49113
- const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 5;
49114
- const blocks = ctx.core.search(query, ctx.session.state).slice(0, limit);
49115
- if (blocks.length === 0) return `[No blocks matched "${query}"]`;
49116
- const lines = blocks.map((b2) => {
49117
- const topic = b2.topic ?? "(no topic)";
49118
- const preview = b2.summary.length > 200 ? b2.summary.slice(0, 200) + "..." : b2.summary;
49119
- return `${b2.blockId} (T${b2.tier}) "${topic}"
49120
- ${preview}`;
49121
- });
49122
- return `Found ${blocks.length} block(s) for "${query}":
49123
-
49124
- ${lines.join("\n\n")}`;
49125
- }
49126
- if (toolName === "acp_status") {
49127
- return buildStatusReport(ctx.session.state, ctx.messages, estimateTokensFast);
49128
- }
49129
- return `[Unknown proxy tool: ${toolName}]`;
49130
- }
49131
- function extractEventType(rawEvent) {
49132
- for (const l of rawEvent.split("\n")) {
49133
- if (l.startsWith("event:")) return l.slice(6).trim();
49134
- }
49135
- return null;
49136
- }
49137
- function extractDataLine(rawEvent) {
49138
- const parts = [];
49139
- for (const l of rawEvent.split("\n")) {
49140
- if (l.startsWith("data:")) {
49141
- let v2 = l.slice(5);
49142
- if (v2.startsWith(" ")) v2 = v2.slice(1);
49143
- parts.push(v2);
49144
- }
49145
- }
49146
- return parts.length ? parts.join("\n") : null;
49147
- }
49148
- function classifyResponsesSseEvent(eventStr) {
49149
- const type = extractEventType(eventStr);
49150
- const dataLine = extractDataLine(eventStr);
49151
- if (!type || !dataLine) return {};
49152
- let obj;
49153
- try {
49154
- obj = JSON.parse(dataLine);
49155
- } catch {
49156
- return {};
49157
- }
49158
- const out = {};
49159
- switch (type) {
49160
- case "response.created":
49161
- case "response.in_progress":
49162
- out.isMeta = true;
49163
- out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
49164
- return out;
49165
- case "response.output_item.added": {
49166
- const item = obj.item;
49167
- if (item?.type === "function_call") {
49168
- const name = typeof item.name === "string" ? item.name : "";
49169
- out.fcStart = {
49170
- itemId: typeof item.id === "string" ? item.id : "",
49171
- callId: typeof item.call_id === "string" ? item.call_id : "",
49172
- name
49173
- };
49174
- return out;
49175
- }
49176
- if (item?.type === "custom_tool_call") out.noBuffer = true;
49177
- out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
49178
- return out;
49179
- }
49180
- case "response.content_part.added":
49181
- case "response.content_part.done":
49182
- case "response.output_text.done":
49183
- out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
49184
- return out;
49185
- case "response.output_text.delta": {
49186
- const delta = typeof obj.delta === "string" ? obj.delta : "";
49187
- if (delta) {
49188
- out.contentDelta = delta;
49189
- out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
49190
- }
49191
- return out;
49192
- }
49193
- case "response.function_call_arguments.delta": {
49194
- const itemId = typeof obj.item_id === "string" ? obj.item_id : "";
49195
- const delta = typeof obj.delta === "string" ? obj.delta : "";
49196
- out.fcArgs = { itemId, delta };
49197
- return out;
49198
- }
49199
- case "response.output_item.done": {
49200
- const item = obj.item;
49201
- if (item?.type === "function_call") {
49202
- out.fcDone = { itemId: typeof item.id === "string" ? item.id : "" };
49203
- return out;
49204
- }
49205
- if (item?.type === "custom_tool_call") {
49206
- out.noBuffer = true;
49207
- out.customToolCallDone = true;
49208
- }
49209
- out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
49210
- return out;
49211
- }
49212
- case "response.completed":
49213
- out.isMeta = true;
49214
- out.terminal = true;
49215
- out.terminalKind = "completed";
49216
- out.responseObj = obj.response ?? null;
49217
- return out;
49218
- case "response.incomplete":
49219
- out.isMeta = true;
49220
- out.terminal = true;
49221
- out.terminalKind = "incomplete";
49222
- out.terminalRaw = eventStr;
49223
- return out;
49224
- case "response.failed":
49225
- case "response.error":
49226
- out.isMeta = true;
49227
- out.terminal = true;
49228
- out.terminalKind = "failed";
49229
- out.terminalRaw = eventStr;
49230
- return out;
49231
- default:
49232
- if (type.startsWith("response.custom_tool_call.")) out.noBuffer = true;
49233
- out.yieldChunk = Buffer.from(eventStr + "\n\n", "utf8");
49234
- return out;
49235
- }
49236
- }
49237
- function buildMessageItemSequence(itemId, outputIndex, text) {
49238
- const item = { type: "message", id: itemId, role: "assistant", content: [] };
49239
- const part = { type: "output_text", text: "" };
49240
- const doneItem = { type: "message", id: itemId, role: "assistant", content: [{ type: "output_text", text }] };
49241
- return [
49242
- `event: response.output_item.added
49243
- data: ${JSON.stringify({ type: "response.output_item.added", output_index: outputIndex, item })}
49244
-
49245
- `,
49246
- `event: response.content_part.added
49247
- data: ${JSON.stringify({ type: "response.content_part.added", item_id: itemId, output_index: outputIndex, part })}
49248
-
49249
- `,
49250
- `event: response.output_text.delta
49251
- data: ${JSON.stringify({ type: "response.output_text.delta", item_id: itemId, output_index: outputIndex, delta: text })}
49252
-
49253
- `,
49254
- `event: response.output_text.done
49255
- data: ${JSON.stringify({ type: "response.output_text.done", item_id: itemId, output_index: outputIndex, text })}
49256
-
49257
- `,
49258
- `event: response.content_part.done
49259
- data: ${JSON.stringify({ type: "response.content_part.done", item_id: itemId, output_index: outputIndex, part: { type: "output_text", text } })}
49260
-
49261
- `,
49262
- `event: response.output_item.done
49263
- data: ${JSON.stringify({ type: "response.output_item.done", output_index: outputIndex, item: doneItem })}
49264
-
49265
- `
49266
- ].join("");
49267
- }
49268
- function buildFunctionCallEvents(fc, outputIndex) {
49269
- return [
49270
- `event: response.output_item.added
49271
- data: ${JSON.stringify({
49272
- type: "response.output_item.added",
49273
- output_index: outputIndex,
49274
- item: { type: "function_call", id: fc.itemId, call_id: fc.callId, name: fc.name, arguments: "" }
49275
- })}
49276
-
49277
- `,
49278
- `event: response.function_call_arguments.delta
49279
- data: ${JSON.stringify({
49280
- type: "response.function_call_arguments.delta",
49281
- item_id: fc.itemId,
49282
- delta: fc.arguments
49283
- })}
49284
-
49285
- `,
49286
- `event: response.function_call_arguments.done
49287
- data: ${JSON.stringify({
49288
- type: "response.function_call_arguments.done",
49289
- item_id: fc.itemId,
49290
- arguments: fc.arguments
49291
- })}
49292
-
49293
- `,
49294
- `event: response.output_item.done
49295
- data: ${JSON.stringify({
49296
- type: "response.output_item.done",
49297
- output_index: outputIndex,
49298
- item: { type: "function_call", id: fc.itemId, call_id: fc.callId, name: fc.name, arguments: fc.arguments }
49299
- })}
49300
-
49301
- `
49302
- ].join("");
49303
- }
49304
- function buildCompleted(responseObj) {
49305
- const resp = responseObj ?? { id: `resp-proxy-${Date.now()}`, status: "completed", output: [] };
49306
- return `event: response.completed
49307
- data: ${JSON.stringify({
49308
- type: "response.completed",
49309
- response: resp
49310
- })}
49311
-
49312
- `;
49313
- }
49314
- function buildFailed(responseObj) {
49315
- const id = responseObj?.id ?? `resp-failed-${Date.now()}`;
49316
- const resp = { ...responseObj ?? {}, id, status: "failed", error: { code: "server_error", message: "upstream returned empty response" } };
49317
- return `event: response.failed
49318
- data: ${JSON.stringify({
49319
- type: "response.failed",
49320
- response: resp
49321
- })}
49322
-
49323
- `;
49324
- }
49325
- function responsesJsonOutput(response) {
49326
- const textParts = [];
49327
- const calls = [];
49328
- for (const item of Array.isArray(response.output) ? response.output : []) {
49329
- if (!item || typeof item !== "object") continue;
49330
- const value = item;
49331
- if (value.type === "message") {
49332
- for (const part of Array.isArray(value.content) ? value.content : []) {
49333
- if (part && typeof part === "object" && part.type === "output_text") {
49334
- textParts.push(part);
49335
- }
49336
- }
49337
- } else if (value.type === "function_call") {
49338
- calls.push({
49339
- itemId: typeof value.id === "string" ? value.id : "",
49340
- callId: typeof value.call_id === "string" ? value.call_id : "",
49341
- name: typeof value.name === "string" ? value.name : "",
49342
- arguments: typeof value.arguments === "string" ? value.arguments : ""
49343
- });
49344
- }
49345
- }
49346
- return {
49347
- text: textParts.map((part) => typeof part.text === "string" ? part.text : "").join(""),
49348
- textParts,
49349
- calls
49350
- };
49351
- }
49352
- function replaceResponsesJsonText(parts, text) {
49353
- parts.forEach((part, index) => {
49354
- part.text = index === 0 ? text : "";
49355
- });
49356
- }
49357
- function surfaceReadonlyJson(current, proxyCalls, ctx) {
49358
- const markers = [];
49359
- for (const call of proxyCalls) {
49360
- if (MUTATING_PROXY_TOOLS.has(call.name)) continue;
49361
- let args = {};
49362
- try {
49363
- args = JSON.parse(call.arguments);
49364
- } catch {
49365
- args = {};
49366
- }
49367
- let result;
49368
- try {
49369
- result = executeProxyTool3(call.name, args, ctx);
49370
- ctx.log(`[acp-proxy: responses JSON ${call.name} (read-only) \u2192 ${result.slice(0, 120).replace(/\n/g, " ")}]`);
49371
- } catch (e) {
49372
- result = `\u274C [ACP] ${call.name} FAILED: ${String(e)}`;
49373
- ctx.log(`[acp-proxy: responses JSON ${call.name} (read-only) FAILED: ${String(e)}]`);
49374
- }
49375
- markers.push(buildVisibilityMarker(call.name, result));
49376
- }
49377
- if (markers.length === 0) return current;
49378
- const out = Array.isArray(current.output) ? [...current.output] : [];
49379
- out.push({ type: "message", id: `msg_acp_ro_${Date.now()}_${markers.length}`, role: "assistant", content: [{ type: "output_text", text: markers.join("\n") }] });
49380
- return { ...current, output: out };
49381
- }
49382
- async function compressLoopResponsesJson(initialResponse, ctx, requestBody, requestOptions) {
49383
- let current = initialResponse;
49384
- for (let loopCount = 1; loopCount <= 5; loopCount++) {
49385
- const output = responsesJsonOutput(current);
49386
- const extracted = extractTextTriggers(output.text);
49387
- const allCalls = [...output.calls, ...extracted.calls].filter((call) => call.name.length > 0);
49388
- const proxyCalls = allCalls.filter((call) => PROXY_TOOL_NAMES.has(call.name));
49389
- const realCalls = allCalls.filter((call) => !PROXY_TOOL_NAMES.has(call.name));
49390
- const mutatingProxy = proxyCalls.filter((call) => MUTATING_PROXY_TOOLS.has(call.name));
49391
- if (mutatingProxy.length === 0 || realCalls.length > 0) {
49392
- if (proxyCalls.length > 0) {
49393
- replaceResponsesJsonText(output.textParts, extracted.clean);
49394
- current = surfaceReadonlyJson(current, proxyCalls, ctx);
49395
- }
49396
- return current;
49397
- }
49398
- const inputItems = Array.isArray(requestBody.input) ? [...requestBody.input] : [];
49399
- if (extracted.clean.trim()) {
49400
- inputItems.push({ type: "message", role: "assistant", content: [{ type: "output_text", text: extracted.clean }] });
49401
- }
49402
- for (const call of proxyCalls) {
49403
- let args = {};
49404
- try {
49405
- args = JSON.parse(call.arguments);
49406
- } catch (error) {
49407
- log("warn", `[acp-compress-args] ${call.name} JSON.parse failed: ${String(error)}`);
49408
- }
49409
- const result = executeProxyTool3(call.name, args, ctx);
49410
- ctx.log(`[acp-proxy: responses JSON ${call.name} \u2192 ${result.slice(0, 120).replace(/\n/g, " ")}]`);
49411
- inputItems.push({ type: "message", role: "user", content: buildVisibilityMarker(call.name, result) });
49412
- }
49413
- requestBody.input = inputItems;
49414
- const { response, clearTimer } = await fetchWithTimeout(requestOptions.url, {
49415
- method: "POST",
49416
- headers: requestOptions.headers,
49417
- body: JSON.stringify(requestBody),
49418
- ...ctx.proxyUrl ? { dispatcher: proxyDispatcher(ctx.proxyUrl) } : {}
49419
- });
49420
- try {
49421
- if (!response.ok) {
49422
- const detail = await response.text().catch(() => "upstream error");
49423
- throw new Error(`responses compress loop upstream error ${response.status}: ${detail.slice(0, 200)}`);
49424
- }
49425
- current = await response.json();
49426
- } finally {
49427
- clearTimer();
49428
- }
49429
- }
49430
- ctx.log("[acp-proxy: responses JSON compress loop limit (5) reached]");
49431
- return current;
49432
- }
49433
- async function* compressLoopResponsesStream(initialUpstream, ctx, requestBody, requestOptions) {
49434
- const textProtocol = ctx.textProtocol ?? TEXT_PROTOCOL;
49435
- let upstream = initialUpstream;
49436
- let loopCount = 0;
49437
- let responseObj = null;
49438
- let activeClearTimer = null;
49439
- let nextOutputIndex = 0;
49440
- for (; ; ) {
49441
- loopCount++;
49442
- if (loopCount > 5) {
49443
- ctx.log("[acp-proxy: responses compress loop limit (5) reached, forwarding completion as-is]");
49444
- const limItemId = `msg_acp_limit_${Date.now()}`;
49445
- yield Buffer.from(buildMessageItemSequence(limItemId, nextOutputIndex++, "\n[acp-proxy: compress loop limit reached]\n"), "utf8");
49446
- yield Buffer.from(buildCompleted(responseObj), "utf8");
49447
- return;
49448
- }
49449
- const fcByItemId = /* @__PURE__ */ new Map();
49450
- let contentText = "";
49451
- let customToolCalls = 0;
49452
- let completed = false;
49453
- let terminalKind = null;
49454
- let terminalRaw = null;
49455
- const isFirstRound = loopCount === 1;
49456
- const reader = upstream.getReader();
49457
- const decoder = new TextDecoder("utf-8");
49458
- let sseBuffer = "";
49459
- try {
49460
- for (; ; ) {
49461
- const { done, value } = await reader.read();
49462
- if (done) break;
49463
- sseBuffer += decoder.decode(value, { stream: true });
49464
- if (sseBuffer.indexOf("\r") !== -1) sseBuffer = sseBuffer.replace(/\r\n|\r/g, "\n");
49465
- let sep;
49466
- while ((sep = sseBuffer.indexOf("\n\n")) >= 0) {
49467
- const eventStr = sseBuffer.slice(0, sep);
49468
- sseBuffer = sseBuffer.slice(sep + 2);
49469
- if (!eventStr.trim()) continue;
49470
- const d = classifyResponsesSseEvent(eventStr);
49471
- if (d.yieldChunk && (isFirstRound || !d.isMeta) && !(textProtocol && !d.isMeta && !d.noBuffer)) {
49472
- yield d.yieldChunk;
49473
- }
49474
- if (d.contentDelta) contentText += d.contentDelta;
49475
- if (d.fcStart) {
49476
- fcByItemId.set(d.fcStart.itemId, {
49477
- itemId: d.fcStart.itemId,
49478
- callId: d.fcStart.callId,
49479
- name: d.fcStart.name,
49480
- arguments: ""
49481
- });
49482
- }
49483
- if (d.fcArgs) {
49484
- const existing = fcByItemId.get(d.fcArgs.itemId);
49485
- if (existing) existing.arguments += d.fcArgs.delta;
49486
- }
49487
- if (d.fcDone) {
49488
- const existing = fcByItemId.get(d.fcDone.itemId);
49489
- if (existing && !existing.arguments) {
49490
- const item = JSON.parse(extractDataLine(eventStr) ?? "{}").item;
49491
- const args = typeof item?.arguments === "string" ? item.arguments : "";
49492
- existing.arguments = args;
49493
- }
49494
- }
49495
- if (d.customToolCallDone) customToolCalls++;
49496
- if (d.terminal) {
49497
- completed = true;
49498
- terminalKind = d.terminalKind ?? null;
49499
- terminalRaw = d.terminalRaw ?? null;
49500
- responseObj = d.responseObj ?? responseObj;
49501
- const resp2 = d.responseObj ?? {};
49502
- const usage = resp2.usage;
49503
- if (usage && d.terminalKind === "completed") {
49504
- const prompt = usage.input_tokens ?? usage.prompt_tokens ?? "?";
49505
- const inDet = usage.input_tokens_details;
49506
- const prDet = usage.prompt_tokens_details;
49507
- const cached = inDet?.cached_tokens ?? prDet?.cached_tokens ?? "?";
49508
- const out = usage.output_tokens ?? "?";
49509
- log("info", `[acp-usage] round ${loopCount} input=${prompt} cached=${cached} output=${out}${cached !== "?" && cached !== 0 && prompt !== "?" ? ` (cache hit ${Math.round(Number(cached) / Number(prompt) * 100)}%)` : ""}`);
49510
- if (typeof prompt === "number") {
49511
- ctx.session.stats.inputTokens += prompt;
49512
- ctx.session.stats.lastInputTokens = prompt + (typeof cached === "number" ? cached : 0);
49513
- if (typeof cached === "number") ctx.session.stats.cachedTokens += cached;
49514
- if (typeof out === "number") ctx.session.stats.outputTokens += out;
49515
- ctx.session.stats.cacheSamples += 1;
49516
- }
49517
- }
49518
- }
49519
- }
49520
- }
49521
- } finally {
49522
- reader.releaseLock();
49523
- if (activeClearTimer) {
49524
- activeClearTimer();
49525
- activeClearTimer = null;
49526
- }
49527
- }
49528
- if (textProtocol) {
49529
- const extracted = extractTextTriggers(contentText);
49530
- contentText = extracted.clean;
49531
- for (const c of extracted.calls) {
49532
- fcByItemId.set(c.itemId, c);
49533
- }
49534
- if (contentText.trim()) {
49535
- const textItemId = `msg_acp_text_r${loopCount}_${Date.now()}`;
49536
- yield Buffer.from(buildMessageItemSequence(textItemId, nextOutputIndex++, contentText), "utf8");
49537
- }
49538
- }
49539
- const allCalls = [...fcByItemId.values()].filter((c) => c.name.length > 0);
49540
- const proxyCalls = allCalls.filter((c) => PROXY_TOOL_NAMES.has(c.name));
49541
- const realCalls = allCalls.filter((c) => !PROXY_TOOL_NAMES.has(c.name));
49542
- const readonlyProxy = proxyCalls.filter((c) => READONLY_PROXY_TOOLS.has(c.name));
49543
- log("debug", `[acp-diag] round ${loopCount} allCalls=[${allCalls.map((c) => c.name).join(",")}] realCalls=[${realCalls.map((c) => c.name).join(",")}] customToolCalls=${customToolCalls} text=${JSON.stringify(contentText.slice(0, 120))}`);
49544
- const hasMutatingOnly = proxyCalls.some((c) => MUTATING_PROXY_TOOLS.has(c.name)) && realCalls.length === 0;
49545
- if (!hasMutatingOnly) {
49546
- for (const fc of readonlyProxy) {
49547
- let args = {};
49548
- try {
49549
- args = JSON.parse(fc.arguments);
49550
- } catch (e) {
49551
- log("warn", `[acp-compress-args] ${fc.name} JSON.parse failed: ${String(e)}`);
49552
- args = {};
49553
- }
49554
- let result;
49555
- try {
49556
- result = executeProxyTool3(fc.name, args, ctx);
49557
- const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
49558
- ctx.log(`[acp-proxy: responses ${fc.name} (read-only) (${fc.callId}) \u2192 ${preview.replace(/\n/g, " ")}]`);
49559
- } catch (e) {
49560
- result = `\u274C [ACP] ${fc.name} FAILED: ${String(e)}`;
49561
- ctx.log(`[acp-proxy: responses ${fc.name} (read-only) (${fc.callId}) FAILED: ${String(e)}]`);
49562
- }
49563
- const markerItemId = `msg_acp_ro_${Date.now()}_${nextOutputIndex}`;
49564
- yield Buffer.from(buildMessageItemSequence(markerItemId, nextOutputIndex++, buildVisibilityMarker(fc.name, result)), "utf8");
49565
- }
49566
- let oi2 = nextOutputIndex;
49567
- for (const fc of realCalls) {
49568
- yield Buffer.from(buildFunctionCallEvents(fc, oi2), "utf8");
49569
- oi2++;
49570
- }
49571
- nextOutputIndex = oi2;
49572
- if (terminalKind && terminalKind !== "completed" && terminalRaw) {
49573
- yield Buffer.from(terminalRaw + "\n\n", "utf8");
49574
- return;
49575
- }
49576
- const hasUsage = !!responseObj?.usage;
49577
- const emittedReadonly = readonlyProxy.length > 0;
49578
- if (contentText.length === 0 && realCalls.length === 0 && customToolCalls === 0 && !emittedReadonly && !hasUsage) {
49579
- ctx.log("[acp-proxy: empty upstream response (no content/usage) \u2014 injecting response.failed for client retry]");
49580
- yield Buffer.from(buildFailed(responseObj), "utf8");
49581
- return;
49582
- }
49583
- if (!completed) {
49584
- ctx.log("[acp-proxy: responses stream ended without completion]");
49585
- }
49586
- yield Buffer.from(buildCompleted(responseObj), "utf8");
49587
- return;
49588
- }
49589
- const names = proxyCalls.map((c) => c.name).join(", ");
49590
- ctx.log(`[acp-proxy: responses round ${loopCount} \u2014 ${proxyCalls.length} proxy call(s): ${names}]`);
49591
- const inputItems = Array.isArray(requestBody.input) ? [...requestBody.input] : [];
49592
- if (contentText) {
49593
- inputItems.push({
49594
- type: "message",
49595
- role: "assistant",
49596
- content: [{ type: "output_text", text: contentText }]
49597
- });
49598
- }
49599
- if (!textProtocol) {
49600
- for (const fc of proxyCalls) {
49601
- inputItems.push({
49602
- type: "function_call",
49603
- id: fc.itemId || `fc_${Date.now()}`,
49604
- call_id: fc.callId || `call_${Date.now()}`,
49605
- name: fc.name,
49606
- arguments: fc.arguments
49607
- });
49608
- }
49609
- }
49610
- for (const fc of proxyCalls) {
49611
- let args = {};
49612
- try {
49613
- args = JSON.parse(fc.arguments);
49614
- } catch (e) {
49615
- log("warn", `[acp-compress-args] ${fc.name} JSON.parse failed: ${String(e)}. raw arguments (len=${fc.arguments.length}): ${fc.arguments.slice(0, 300)}`);
49616
- args = {};
49617
- }
49618
- if (fc.name === "compress") {
49619
- log("debug", `[acp-compress-args] compress args parsed: ${JSON.stringify(args).slice(0, 400)}`);
49620
- }
49621
- const result = executeProxyTool3(fc.name, args, ctx);
49622
- const preview = result.length > 120 ? result.slice(0, 120) + "..." : result;
49623
- ctx.log(`[acp-proxy: responses ${fc.name} (${fc.callId}) \u2192 ${preview.replace(/\n/g, " ")}]`);
49624
- const markerItemId = `msg_acp_${Date.now()}_${nextOutputIndex}`;
49625
- yield Buffer.from(
49626
- buildMessageItemSequence(markerItemId, nextOutputIndex++, buildVisibilityMarker(fc.name, result)),
49627
- "utf8"
49628
- );
49629
- inputItems.push(textProtocol ? { type: "message", role: "user", content: buildVisibilityMarker(fc.name, result) } : { type: "function_call_output", call_id: fc.callId || `call_${Date.now()}`, output: result });
49630
- }
49631
- requestBody.input = inputItems;
49632
- if (!("stream" in requestBody)) requestBody.stream = true;
49633
- const { response: resp, clearTimer } = await fetchWithTimeout(requestOptions.url, {
49634
- method: "POST",
49635
- headers: requestOptions.headers,
49636
- body: JSON.stringify(requestBody),
49637
- ...ctx.proxyUrl ? { dispatcher: proxyDispatcher(ctx.proxyUrl) } : {}
49638
- });
49639
- if (!resp.ok || !resp.body) {
49640
- clearTimer();
49641
- const errText = await resp.text().catch(() => "upstream error");
49642
- ctx.log(`[acp-proxy: responses compress loop upstream error ${resp.status}: ${errText.slice(0, 200)}]`);
49643
- const errItemId = `msg_acp_err_${Date.now()}`;
49644
- yield Buffer.from(
49645
- buildMessageItemSequence(errItemId, nextOutputIndex++, `
49646
- [acp-proxy: upstream error ${resp.status}: ${errText.slice(0, 200)}]
49647
- `),
49648
- "utf8"
49649
- );
49650
- yield Buffer.from(buildCompleted(responseObj), "utf8");
49651
- return;
49652
- }
49653
- upstream = resp.body;
49654
- if (activeClearTimer) activeClearTimer();
49655
- activeClearTimer = clearTimer;
49656
- }
49657
- }
49658
-
49659
- // src/loop/core.ts
49660
- var MAX_LOOP_ROUNDS = 10;
49661
- function executeProxyTool4(toolName, args, ctx, callId) {
49662
- if (toolName === "compress") {
49663
- return applyRanges(parseCompressInput(args, callId), ctx);
49664
- }
49665
- if (toolName === "decompress") {
49666
- return resolveDecompress(args, ctx);
49667
- }
49668
- if (toolName === "search_context") {
49669
- const query = typeof args.query === "string" ? args.query : "";
49670
- if (query.length === 0) return "[search_context FAILED: query is required]";
49671
- const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 5;
49672
- const blocks = ctx.core.search(query, ctx.session.state).slice(0, limit);
49673
- if (blocks.length === 0) return `[No blocks matched "${query}"]`;
49674
- const lines = blocks.map((b2) => {
49675
- const topic = b2.topic ?? "(no topic)";
49676
- const preview = b2.summary.length > 200 ? b2.summary.slice(0, 200) + "..." : b2.summary;
49677
- return `${b2.blockId} (T${b2.tier}) "${topic}"
49678
- ${preview}`;
49679
- });
49680
- return `Found ${blocks.length} block(s) for "${query}":
49681
-
49682
- ${lines.join("\n\n")}`;
49683
- }
49684
- if (toolName === "acp_status") {
49685
- return buildStatusReport(ctx.session.state, ctx.messages, estimateTokensFast);
49686
- }
49687
- return `[Unknown proxy tool: ${toolName}]`;
49688
- }
49689
- function recordUsage(ctx, usage, round) {
49690
- const prompt = usage.inputTokens;
49691
- const cached = usage.cachedTokens;
49692
- const out = usage.outputTokens;
49693
- if (typeof prompt === "number") ctx.session.stats.inputTokens += prompt;
49694
- ctx.session.stats.lastInputTokens = (typeof prompt === "number" ? prompt : 0) + (typeof cached === "number" ? cached : 0);
49695
- if (typeof cached === "number") ctx.session.stats.cachedTokens += cached;
49696
- if (typeof out === "number") ctx.session.stats.outputTokens += out;
49697
- ctx.session.stats.cacheSamples += 1;
49698
- const hitPct = typeof prompt === "number" && typeof cached === "number" && prompt + cached > 0 ? Math.round(cached / (prompt + cached) * 100) : 0;
49699
- ctx.log(
49700
- `[acp-usage] round ${round} input=${ctx.session.stats.lastInputTokens} cached=${cached ?? 0} (cache hit ${hitPct}%)`
49701
- );
49702
- }
49703
- async function* runCompressLoop(upstream, ctx, requestBody, requestOptions, adapter, systemPrompt) {
48540
+ async function* runCompressLoop(upstream, ctx, requestBody, requestOptions, adapter, systemPrompt, signal) {
49704
48541
  let activeClearTimer = null;
49705
48542
  let currentUpstream = upstream;
49706
48543
  const coreMessages = [...ctx.messages];
49707
48544
  try {
49708
48545
  for (let round = 1; round <= MAX_LOOP_ROUNDS; round++) {
48546
+ if (signal?.aborted) break;
49709
48547
  let assistantText = "";
49710
48548
  const calls = [];
49711
48549
  let usage = {};
49712
48550
  let finishReason;
49713
48551
  for await (const ev of adapter.parseStream(currentUpstream, round)) {
48552
+ if (signal?.aborted) break;
49714
48553
  if (ev.kind === "text") {
49715
48554
  assistantText += ev.delta;
49716
48555
  if (!ctx.textProtocol && round === 1 && ev.raw) {
@@ -49758,7 +48597,7 @@ async function* runCompressLoop(upstream, ctx, requestBody, requestOptions, adap
49758
48597
  } catch {
49759
48598
  parsedArgs = {};
49760
48599
  }
49761
- const result = executeProxyTool4(call.name, parsedArgs, ctx, call.callId);
48600
+ const result = executeProxyTool(call.name, parsedArgs, ctx, call.callId);
49762
48601
  proxyResults.push({ name: call.name, callId: call.callId, result, arguments: call.arguments });
49763
48602
  yield adapter.emitMarker(call.name, result);
49764
48603
  } else {
@@ -49791,7 +48630,7 @@ async function* runCompressLoop(upstream, ctx, requestBody, requestOptions, adap
49791
48630
  if (ctx.textProtocol) {
49792
48631
  coreMessages.push({
49793
48632
  id: `acp_loop_r${round}_marker_${pr2.callId}`,
49794
- role: "user",
48633
+ role: "system",
49795
48634
  contentType: "text",
49796
48635
  text: buildVisibilityMarker(pr2.name, pr2.result)
49797
48636
  });
@@ -49837,6 +48676,7 @@ async function* runCompressLoop(upstream, ctx, requestBody, requestOptions, adap
49837
48676
  return;
49838
48677
  }
49839
48678
  ctx.log(`[acp-loop] round ${round} saw mutating proxy tool; re-requesting`);
48679
+ if (signal?.aborted) break;
49840
48680
  const newBody = adapter.buildRequest(coreMessages, systemPrompt, requestBody);
49841
48681
  if (process.env.ACP_DUMP_REQ !== "0" && ctx.debug) {
49842
48682
  try {
@@ -49848,12 +48688,17 @@ async function* runCompressLoop(upstream, ctx, requestBody, requestOptions, adap
49848
48688
  } catch {
49849
48689
  }
49850
48690
  }
49851
- const { response: resp, clearTimer } = await fetchWithTimeout(requestOptions.url, {
49852
- method: "POST",
49853
- headers: requestOptions.headers,
49854
- body: JSON.stringify(newBody),
49855
- ...ctx.proxyUrl ? { dispatcher: proxyDispatcher(ctx.proxyUrl) } : {}
49856
- });
48691
+ const { response: resp, clearTimer } = await fetchWithTimeout(
48692
+ requestOptions.url,
48693
+ {
48694
+ method: "POST",
48695
+ headers: requestOptions.headers,
48696
+ body: JSON.stringify(newBody),
48697
+ ...ctx.proxyUrl ? { dispatcher: proxyDispatcher(ctx.proxyUrl) } : {}
48698
+ },
48699
+ void 0,
48700
+ signal
48701
+ );
49857
48702
  if (!resp.ok || !resp.body) {
49858
48703
  clearTimer();
49859
48704
  const errText = await resp.text().catch(() => "upstream error");
@@ -49902,13 +48747,13 @@ async function* iterSseEvents(stream2) {
49902
48747
  reader.releaseLock();
49903
48748
  }
49904
48749
  }
49905
- function extractEventType2(rawEvent) {
48750
+ function extractEventType(rawEvent) {
49906
48751
  for (const l of rawEvent.split("\n")) {
49907
48752
  if (l.startsWith("event:")) return l.slice(6).trim();
49908
48753
  }
49909
48754
  return null;
49910
48755
  }
49911
- function extractDataLine2(rawEvent) {
48756
+ function extractDataLine(rawEvent) {
49912
48757
  const parts = [];
49913
48758
  for (const l of rawEvent.split("\n")) {
49914
48759
  if (l.startsWith("data:")) {
@@ -49919,7 +48764,7 @@ function extractDataLine2(rawEvent) {
49919
48764
  }
49920
48765
  return parts.length ? parts.join("\n") : null;
49921
48766
  }
49922
- function buildMessageItemSequence2(itemId, outputIndex, text) {
48767
+ function buildMessageItemSequence(itemId, outputIndex, text) {
49923
48768
  const item = { type: "message", id: itemId, role: "assistant", content: [] };
49924
48769
  const part = { type: "output_text", text: "" };
49925
48770
  const doneItem = {
@@ -49958,7 +48803,7 @@ data: ${JSON.stringify({ type: "response.output_item.done", output_index: output
49958
48803
  "utf8"
49959
48804
  );
49960
48805
  }
49961
- function buildFunctionCallEvents2(fc, itemId, outputIndex) {
48806
+ function buildFunctionCallEvents(fc, itemId, outputIndex) {
49962
48807
  return Buffer.from(
49963
48808
  [
49964
48809
  `event: response.output_item.added
@@ -49997,7 +48842,7 @@ data: ${JSON.stringify({
49997
48842
  "utf8"
49998
48843
  );
49999
48844
  }
50000
- function buildCompleted2(responseObj) {
48845
+ function buildCompleted(responseObj) {
50001
48846
  const resp = responseObj ?? { id: `resp-proxy-${Date.now()}`, status: "completed", output: [] };
50002
48847
  return Buffer.from(
50003
48848
  `event: response.completed
@@ -50041,8 +48886,8 @@ function createResponsesAdapter(textProtocol, projection) {
50041
48886
  async *parseStream(upstream, round) {
50042
48887
  const pending = /* @__PURE__ */ new Map();
50043
48888
  for await (const eventStr of iterSseEvents(upstream)) {
50044
- const type = extractEventType2(eventStr);
50045
- const dataLine = extractDataLine2(eventStr);
48889
+ const type = extractEventType(eventStr);
48890
+ const dataLine = extractDataLine(eventStr);
50046
48891
  if (!type || !dataLine) continue;
50047
48892
  let obj;
50048
48893
  try {
@@ -50140,15 +48985,15 @@ function createResponsesAdapter(textProtocol, projection) {
50140
48985
  }
50141
48986
  },
50142
48987
  emitText(delta) {
50143
- return buildMessageItemSequence2(`msg-proxy-${Date.now()}-${outputIndex}`, outputIndex++, delta);
48988
+ return buildMessageItemSequence(`msg-proxy-${Date.now()}-${outputIndex}`, outputIndex++, delta);
50144
48989
  },
50145
48990
  emitToolCall(call) {
50146
- const buf = buildFunctionCallEvents2(call, `fc-proxy-${Date.now()}-${outputIndex}`, outputIndex);
48991
+ const buf = buildFunctionCallEvents(call, `fc-proxy-${Date.now()}-${outputIndex}`, outputIndex);
50147
48992
  outputIndex += 1;
50148
48993
  return buf;
50149
48994
  },
50150
48995
  emitMarker(toolName, result) {
50151
- return buildMessageItemSequence2(
48996
+ return buildMessageItemSequence(
50152
48997
  `marker-${Date.now()}-${outputIndex}`,
50153
48998
  outputIndex++,
50154
48999
  buildVisibilityMarker(toolName, result)
@@ -50187,7 +49032,7 @@ data: ${JSON.stringify({ type: "response.failed", response: failed })}
50187
49032
  }
50188
49033
  resp = { ...resp, usage };
50189
49034
  }
50190
- return buildCompleted2(resp);
49035
+ return buildCompleted(resp);
50191
49036
  },
50192
49037
  emitError(message) {
50193
49038
  const resp = {
@@ -50482,7 +49327,7 @@ async function* iterSseEvents2(stream2) {
50482
49327
  reader.releaseLock();
50483
49328
  }
50484
49329
  }
50485
- function parseAnthropicSse2(eventStr) {
49330
+ function parseAnthropicSse(eventStr) {
50486
49331
  const lines = eventStr.split("\n");
50487
49332
  let type = "";
50488
49333
  const dataLines = [];
@@ -50592,7 +49437,7 @@ ${systemPrompt}` : systemPrompt;
50592
49437
  let usageYielded = false;
50593
49438
  const indexMap = /* @__PURE__ */ new Map();
50594
49439
  for await (const eventStr of iterSseEvents2(upstream)) {
50595
- const parsed = parseAnthropicSse2(eventStr);
49440
+ const parsed = parseAnthropicSse(eventStr);
50596
49441
  if (!parsed) continue;
50597
49442
  const { type, data } = parsed;
50598
49443
  const rawBuf = Buffer.from(eventStr + "\n\n", "utf8");
@@ -50720,6 +49565,184 @@ function pickAdapter(protocol, requestBody, textProtocol, responsesProjection, a
50720
49565
  throw new Error(`[acp-loop] unknown protocol: ${protocol}`);
50721
49566
  }
50722
49567
 
49568
+ // src/compress-loop-responses.ts
49569
+ function extractTextTriggers(text) {
49570
+ const calls = [];
49571
+ let clean = "";
49572
+ let i = 0;
49573
+ let n = 0;
49574
+ while (i < text.length) {
49575
+ const open = text.indexOf(ACP_TEXT_OPEN, i);
49576
+ if (open === -1) {
49577
+ clean += text.slice(i);
49578
+ break;
49579
+ }
49580
+ clean += text.slice(i, open);
49581
+ const after = open + ACP_TEXT_OPEN.length;
49582
+ const close = text.indexOf(ACP_TEXT_CLOSE, after);
49583
+ if (close === -1) {
49584
+ clean += text.slice(open);
49585
+ break;
49586
+ }
49587
+ const payload = text.slice(after, close).trim();
49588
+ if (payload) {
49589
+ const stamp = `${Date.now()}_${n++}`;
49590
+ calls.push({
49591
+ itemId: `fc_text_${stamp}`,
49592
+ callId: `call_text_${stamp}`,
49593
+ name: COMPRESS_TOOL_NAME,
49594
+ arguments: payload
49595
+ });
49596
+ }
49597
+ i = close + ACP_TEXT_CLOSE.length;
49598
+ }
49599
+ return { clean, calls };
49600
+ }
49601
+ function executeProxyTool2(toolName, args, ctx) {
49602
+ if (toolName === "compress") {
49603
+ return applyRanges(parseCompressInput(args), ctx);
49604
+ }
49605
+ if (toolName === "decompress") {
49606
+ return resolveDecompress(args, ctx);
49607
+ }
49608
+ if (toolName === "search_context") {
49609
+ const query = typeof args.query === "string" ? args.query : "";
49610
+ if (query.length === 0) return "[search_context FAILED: query is required]";
49611
+ const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 5;
49612
+ const blocks = ctx.core.search(query, ctx.session.state).slice(0, limit);
49613
+ if (blocks.length === 0) return `[No blocks matched "${query}"]`;
49614
+ const lines = blocks.map((b2) => {
49615
+ const topic = b2.topic ?? "(no topic)";
49616
+ const preview = b2.summary.length > 200 ? b2.summary.slice(0, 200) + "..." : b2.summary;
49617
+ return `${b2.blockId} (T${b2.tier}) "${topic}"
49618
+ ${preview}`;
49619
+ });
49620
+ return `Found ${blocks.length} block(s) for "${query}":
49621
+
49622
+ ${lines.join("\n\n")}`;
49623
+ }
49624
+ if (toolName === "acp_status") {
49625
+ return buildStatusReport(ctx.session.state, ctx.messages, estimateTokensFast);
49626
+ }
49627
+ return `[Unknown proxy tool: ${toolName}]`;
49628
+ }
49629
+ function responsesJsonOutput(response) {
49630
+ const textParts = [];
49631
+ const calls = [];
49632
+ for (const item of Array.isArray(response.output) ? response.output : []) {
49633
+ if (!item || typeof item !== "object") continue;
49634
+ const value = item;
49635
+ if (value.type === "message") {
49636
+ for (const part of Array.isArray(value.content) ? value.content : []) {
49637
+ if (part && typeof part === "object" && part.type === "output_text") {
49638
+ textParts.push(part);
49639
+ }
49640
+ }
49641
+ } else if (value.type === "function_call") {
49642
+ calls.push({
49643
+ itemId: typeof value.id === "string" ? value.id : "",
49644
+ callId: typeof value.call_id === "string" ? value.call_id : "",
49645
+ name: typeof value.name === "string" ? value.name : "",
49646
+ arguments: typeof value.arguments === "string" ? value.arguments : ""
49647
+ });
49648
+ }
49649
+ }
49650
+ return {
49651
+ text: textParts.map((part) => typeof part.text === "string" ? part.text : "").join(""),
49652
+ textParts,
49653
+ calls
49654
+ };
49655
+ }
49656
+ function replaceResponsesJsonText(parts, text) {
49657
+ parts.forEach((part, index) => {
49658
+ part.text = index === 0 ? text : "";
49659
+ });
49660
+ }
49661
+ function surfaceReadonlyJson(current, proxyCalls, ctx) {
49662
+ const markers = [];
49663
+ for (const call of proxyCalls) {
49664
+ if (MUTATING_PROXY_TOOLS.has(call.name)) continue;
49665
+ let args = {};
49666
+ try {
49667
+ args = JSON.parse(call.arguments);
49668
+ } catch {
49669
+ args = {};
49670
+ }
49671
+ let result;
49672
+ try {
49673
+ result = executeProxyTool2(call.name, args, ctx);
49674
+ ctx.log(`[acp-proxy: responses JSON ${call.name} (read-only) \u2192 ${result.slice(0, 120).replace(/\n/g, " ")}]`);
49675
+ } catch (e) {
49676
+ result = `\u274C [ACP] ${call.name} FAILED: ${String(e)}`;
49677
+ ctx.log(`[acp-proxy: responses JSON ${call.name} (read-only) FAILED: ${String(e)}]`);
49678
+ }
49679
+ markers.push(buildVisibilityMarker(call.name, result));
49680
+ }
49681
+ if (markers.length === 0) return current;
49682
+ const out = Array.isArray(current.output) ? [...current.output] : [];
49683
+ out.push({ type: "message", id: `msg_acp_ro_${Date.now()}_${markers.length}`, role: "assistant", content: [{ type: "output_text", text: markers.join("\n") }] });
49684
+ return { ...current, output: out };
49685
+ }
49686
+ async function compressLoopResponsesJson(initialResponse, ctx, requestBody, requestOptions) {
49687
+ let current = initialResponse;
49688
+ for (let loopCount = 1; loopCount <= MAX_LOOP_ROUNDS; loopCount++) {
49689
+ const output = responsesJsonOutput(current);
49690
+ const extracted = extractTextTriggers(output.text);
49691
+ const allCalls = [...output.calls, ...extracted.calls].filter((call) => call.name.length > 0);
49692
+ const proxyCalls = allCalls.filter((call) => PROXY_TOOL_NAMES.has(call.name));
49693
+ const realCalls = allCalls.filter((call) => !PROXY_TOOL_NAMES.has(call.name));
49694
+ const mutatingProxy = proxyCalls.filter((call) => MUTATING_PROXY_TOOLS.has(call.name));
49695
+ if (mutatingProxy.length === 0 || realCalls.length > 0) {
49696
+ if (proxyCalls.length > 0) {
49697
+ replaceResponsesJsonText(output.textParts, extracted.clean);
49698
+ current = surfaceReadonlyJson(current, proxyCalls, ctx);
49699
+ }
49700
+ return current;
49701
+ }
49702
+ const inputItems = Array.isArray(requestBody.input) ? [...requestBody.input] : [];
49703
+ if (extracted.clean.trim()) {
49704
+ inputItems.push({ type: "message", role: "assistant", content: [{ type: "output_text", text: extracted.clean }] });
49705
+ }
49706
+ let mutatedThisTurn = false;
49707
+ for (const call of proxyCalls) {
49708
+ let args = {};
49709
+ try {
49710
+ args = JSON.parse(call.arguments);
49711
+ } catch (error) {
49712
+ log("warn", `[acp-compress-args] ${call.name} JSON.parse failed: ${String(error)}`);
49713
+ }
49714
+ let result;
49715
+ if (MUTATING_PROXY_TOOLS.has(call.name) && mutatedThisTurn) {
49716
+ result = `Already ${call.name}ed once this turn. Do not ${call.name} again; generate your normal response now.`;
49717
+ ctx.log(`[acp-proxy: responses JSON ${call.name} skipped (state already mutated this turn)]`);
49718
+ } else {
49719
+ result = executeProxyTool2(call.name, args, ctx);
49720
+ if (MUTATING_PROXY_TOOLS.has(call.name)) mutatedThisTurn = true;
49721
+ ctx.log(`[acp-proxy: responses JSON ${call.name} \u2192 ${result.slice(0, 120).replace(/\n/g, " ")}]`);
49722
+ }
49723
+ inputItems.push({ type: "message", role: "developer", content: buildVisibilityMarker(call.name, result) });
49724
+ }
49725
+ requestBody.input = inputItems;
49726
+ const { response, clearTimer } = await fetchWithTimeout(requestOptions.url, {
49727
+ method: "POST",
49728
+ headers: requestOptions.headers,
49729
+ body: JSON.stringify(requestBody),
49730
+ ...ctx.proxyUrl ? { dispatcher: proxyDispatcher(ctx.proxyUrl) } : {}
49731
+ });
49732
+ try {
49733
+ if (!response.ok) {
49734
+ const detail = await response.text().catch(() => "upstream error");
49735
+ throw new Error(`responses compress loop upstream error ${response.status}: ${detail.slice(0, 200)}`);
49736
+ }
49737
+ current = await response.json();
49738
+ } finally {
49739
+ clearTimer();
49740
+ }
49741
+ }
49742
+ ctx.log(`[acp-proxy: responses JSON compress loop limit (${MAX_LOOP_ROUNDS}) reached]`);
49743
+ return current;
49744
+ }
49745
+
50723
49746
  // src/stream-openai.ts
50724
49747
  function rewriteOpenaiJsonResponse(body, ctx) {
50725
49748
  if (!body || typeof body !== "object") return body;
@@ -50763,7 +49786,7 @@ ${note}` : note;
50763
49786
  }
50764
49787
 
50765
49788
  // src/stream-responses.ts
50766
- var TEXT_PROTOCOL2 = process.env.ACP_COMPRESS_PROTOCOL === "text";
49789
+ var TEXT_PROTOCOL = process.env.ACP_COMPRESS_PROTOCOL === "text";
50767
49790
  function rewriteResponsesJsonResponse(body, ctx) {
50768
49791
  if (!body || typeof body !== "object") return body;
50769
49792
  const b2 = body;
@@ -51795,6 +50818,15 @@ var UPSTREAM_HOP_HEADERS = /* @__PURE__ */ new Set([
51795
50818
  // streamed from fetch, otherwise clients try to decompress plain bytes.
51796
50819
  "content-encoding"
51797
50820
  ]);
50821
+ function buildForwardHeaders(headers) {
50822
+ const out = {};
50823
+ for (const [k2, v2] of Object.entries(headers)) {
50824
+ if (k2.toLowerCase() === "content-length" || k2.toLowerCase() === "host") continue;
50825
+ out[k2] = v2;
50826
+ }
50827
+ out["content-type"] = "application/json";
50828
+ return out;
50829
+ }
51798
50830
  function resolveUpstream(_opts, reqUrl, req) {
51799
50831
  const mitmUpstream = readMitmUpstream(req?.socket);
51800
50832
  if (mitmUpstream) {
@@ -52018,11 +51050,24 @@ async function handle(req, res, opts, core, config, log2) {
52018
51050
  return;
52019
51051
  }
52020
51052
  let bodyBuffer;
51053
+ let urlPath;
51054
+ let responsesCompact;
51055
+ let route;
51056
+ let upstreamOrigin;
51057
+ let protocol;
52021
51058
  try {
52022
51059
  bodyBuffer = await readBody(req);
52023
- const decoded = await decodeRequestBody(headerValue(req, "content-encoding"), bodyBuffer, MAX_REQUEST_BYTES);
52024
- bodyBuffer = decoded.body;
52025
- if (decoded.decoded) delete req.headers["content-encoding"];
51060
+ const url = req.url ?? "";
51061
+ urlPath = url.split("?", 2)[0];
51062
+ responsesCompact = urlPath.endsWith("/responses/compact");
51063
+ route = resolveUpstream(opts, req.url ?? "", req);
51064
+ upstreamOrigin = route ? route.upstream : opts.upstream;
51065
+ protocol = route?.explicitProtocol ?? (req.method === "POST" && bodyBuffer.length > 0 ? urlPath.endsWith("/chat/completions") ? "openai" : urlPath.endsWith("/v1/messages") || urlPath.endsWith("/messages") ? "anthropic" : urlPath.endsWith("/responses") || responsesCompact ? "responses" : null : null);
51066
+ if (protocol !== null && bodyBuffer.length > 0) {
51067
+ const decoded = await decodeRequestBody(headerValue(req, "content-encoding"), bodyBuffer, MAX_REQUEST_BYTES);
51068
+ bodyBuffer = decoded.body;
51069
+ if (decoded.decoded) delete req.headers["content-encoding"];
51070
+ }
52026
51071
  } catch (err2) {
52027
51072
  if (err2 instanceof BodyTooLargeError) {
52028
51073
  log2("warn", `413: request body exceeds ${err2.limit} bytes`);
@@ -52035,13 +51080,7 @@ async function handle(req, res, opts, core, config, log2) {
52035
51080
  res.end(JSON.stringify({ error: { type: "invalid_request", message: String(err2) } }));
52036
51081
  return;
52037
51082
  }
52038
- const url = req.url ?? "";
52039
- const urlPath = url.split("?", 2)[0];
52040
- const responsesCompact = urlPath.endsWith("/responses/compact");
52041
51083
  const countTokens = isCountTokensRequest(req.method ?? "GET", urlPath, bodyBuffer.length > 0);
52042
- const route = resolveUpstream(opts, req.url ?? "", req);
52043
- const upstreamOrigin = route ? route.upstream : opts.upstream;
52044
- const protocol = route?.explicitProtocol ?? (req.method === "POST" && bodyBuffer.length > 0 ? urlPath.endsWith("/chat/completions") ? "openai" : urlPath.endsWith("/v1/messages") || urlPath.endsWith("/messages") ? "anthropic" : urlPath.endsWith("/responses") || responsesCompact ? "responses" : null : null);
52045
51084
  let parsed = null;
52046
51085
  if (protocol && bodyBuffer.length > 0) {
52047
51086
  try {
@@ -52086,19 +51125,19 @@ async function handle(req, res, opts, core, config, log2) {
52086
51125
  });
52087
51126
  const clientLabel = responsesIdentity?.clientProvided ? responsesIdentity.value : clientConversationHeader(req.headers);
52088
51127
  const session = getSession(sessionId, { protocol, upstreamOrigin, label: clientLabel ?? void 0 });
52089
- await withSessionLock(session, async () => {
52090
- prepared = countTokens ? prepareCountTokens(parsed, core, reqConfig, log2, session) : protocol === "anthropic" ? prepareAnthropic(parsed, req, opts, core, reqConfig, log2, session) : protocol === "openai" ? prepareOpenai(parsed, req, opts, core, reqConfig, log2, session) : responsesCompact ? prepareResponsesCompact(bodyBuffer, parsed, session) : prepareResponses(parsed, req, opts, core, reqConfig, log2, session, responsesIdentity);
52091
- acquireInFlight(session);
52092
- try {
51128
+ acquireInFlight(session);
51129
+ try {
51130
+ await withSessionLock(session, async () => {
51131
+ prepared = countTokens ? prepareCountTokens(parsed, core, reqConfig, log2, session) : protocol === "anthropic" ? prepareAnthropic(parsed, req, opts, core, reqConfig, log2, session) : protocol === "openai" ? prepareOpenai(parsed, req, opts, core, reqConfig, log2, session) : responsesCompact ? prepareResponsesCompact(bodyBuffer, parsed, session) : prepareResponses(parsed, req, opts, core, reqConfig, log2, session, responsesIdentity);
52093
51132
  await forward(req, res, opts, prepared.body, prepared, core, reqConfig, log2, route, affinity);
52094
- } finally {
52095
- releaseInFlight(session);
52096
- }
52097
- });
51133
+ });
51134
+ } finally {
51135
+ releaseInFlight(session);
51136
+ }
52098
51137
  }
52099
51138
  if (!prepared) {
52100
51139
  if (protocol === null && !opts.passthrough) {
52101
- log2("warn", `unrecognized path ${url} \u2014 not a known protocol (/chat/completions, /v1/messages, /responses, /responses/compact); forwarding unchanged`);
51140
+ log2("warn", `unrecognized path ${req.url ?? ""} \u2014 not a known protocol (/chat/completions, /v1/messages, /responses, /responses/compact); forwarding unchanged`);
52102
51141
  }
52103
51142
  await forward(req, res, opts, bodyBuffer, null, core, reqConfig, log2, route, void 0);
52104
51143
  }
@@ -52134,6 +51173,7 @@ function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
52134
51173
  ++session.stats.requests;
52135
51174
  let processedMessages = [];
52136
51175
  let originalMessages = [];
51176
+ let nudge;
52137
51177
  let rebuiltMessages = parsed.messages;
52138
51178
  let systemOut = parsed.system;
52139
51179
  let toolsOut = parsed.tools;
@@ -52144,6 +51184,7 @@ function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
52144
51184
  const tokenCount = session.stats.lastInputTokens;
52145
51185
  const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
52146
51186
  session.state = turn.state;
51187
+ nudge = turn.nudge;
52147
51188
  session.stats.contextTokens = tokenCount;
52148
51189
  if (!session.meta.title) {
52149
51190
  const t = deriveTitle(msgs);
@@ -52173,7 +51214,7 @@ function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
52173
51214
  }
52174
51215
  const rebuilt = { ...parsed, messages: rebuiltMessages, system: systemOut, tools: toolsOut };
52175
51216
  markDirty(session);
52176
- return { body: JSON.stringify(rebuilt), session, processedMessages, originalMessages, anthropicSystem: parsed.system, protocol: "anthropic", stream: stream2, compressInjected: opts.compress.injectTool };
51217
+ return { body: JSON.stringify(rebuilt), session, processedMessages, originalMessages, anthropicSystem: parsed.system, protocol: "anthropic", stream: stream2, compressInjected: opts.compress.injectTool, nudge };
52177
51218
  }
52178
51219
  function prepareOpenai(parsed, req, opts, core, config, log2, session) {
52179
51220
  const sessionId = session.id;
@@ -52181,6 +51222,7 @@ function prepareOpenai(parsed, req, opts, core, config, log2, session) {
52181
51222
  ++session.stats.requests;
52182
51223
  let processedMessages = [];
52183
51224
  let originalMessages = [];
51225
+ let nudge;
52184
51226
  let rebuiltMessages = parsed.messages;
52185
51227
  let toolsOut = parsed.tools;
52186
51228
  const maxTokens = typeof parsed.max_tokens === "number" ? parsed.max_tokens : 8192;
@@ -52192,6 +51234,7 @@ function prepareOpenai(parsed, req, opts, core, config, log2, session) {
52192
51234
  const tokenCount = session.stats.lastInputTokens;
52193
51235
  const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
52194
51236
  session.state = turn.state;
51237
+ nudge = turn.nudge;
52195
51238
  session.stats.contextTokens = tokenCount;
52196
51239
  if (!session.meta.title) {
52197
51240
  const t = deriveTitle(msgs);
@@ -52226,7 +51269,7 @@ function prepareOpenai(parsed, req, opts, core, config, log2, session) {
52226
51269
  rebuilt.stream_options = { include_usage: true };
52227
51270
  }
52228
51271
  markDirty(session);
52229
- return { body: JSON.stringify(rebuilt), session, processedMessages, originalMessages, protocol: "openai", stream: stream2, compressInjected: shouldInject };
51272
+ return { body: JSON.stringify(rebuilt), session, processedMessages, originalMessages, protocol: "openai", stream: stream2, compressInjected: shouldInject, nudge };
52230
51273
  }
52231
51274
  function prepareResponses(parsed, req, opts, core, config, log2, session, identity) {
52232
51275
  const sessionId = session.id;
@@ -52237,6 +51280,7 @@ function prepareResponses(parsed, req, opts, core, config, log2, session, identi
52237
51280
  }
52238
51281
  let processedMessages = [];
52239
51282
  let originalMessages = [];
51283
+ let nudge;
52240
51284
  let responsesProjection;
52241
51285
  let rebuiltInput = parsed.input;
52242
51286
  let toolsOut = parsed.tools;
@@ -52253,6 +51297,7 @@ function prepareResponses(parsed, req, opts, core, config, log2, session, identi
52253
51297
  const tokenCount = session.stats.lastInputTokens;
52254
51298
  const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: process.env.ACP_RENDER_NONE ? "none" : "text-only" });
52255
51299
  session.state = turn.state;
51300
+ nudge = turn.nudge;
52256
51301
  session.stats.contextTokens = tokenCount;
52257
51302
  if (!session.meta.title) {
52258
51303
  const t = deriveTitle(msgs);
@@ -52314,7 +51359,8 @@ function prepareResponses(parsed, req, opts, core, config, log2, session, identi
52314
51359
  protocol: "responses",
52315
51360
  stream: stream2,
52316
51361
  compressInjected: shouldInject,
52317
- responsesTextProtocol
51362
+ responsesTextProtocol,
51363
+ nudge
52318
51364
  };
52319
51365
  }
52320
51366
  function isCountTokensRequest(method, urlPath, hasBody) {
@@ -52557,105 +51603,39 @@ async function forward(req, res, opts, body, prepared, core, config, log2, route
52557
51603
  dumpRaw = dumpStreamToFile(b2, opts.dumpSse, `${Date.now()}-${prepared.session.id}-raw.sse`);
52558
51604
  }
52559
51605
  try {
52560
- if (process.env.ACP_LOOP_V2 !== "0") {
52561
- const parsedReq = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
52562
- const reqHeaders = {};
52563
- for (const [k2, v2] of Object.entries(headers)) {
52564
- if (k2.toLowerCase() === "content-length" || k2.toLowerCase() === "host") continue;
52565
- reqHeaders[k2] = v2;
52566
- }
52567
- reqHeaders["content-type"] = "application/json";
52568
- const textProtocol = prepared.protocol === "responses" && !!prepared.responsesTextProtocol;
52569
- const systemPrompt = textProtocol ? buildCompressTextSystemPrompt() : buildCompressSystemPrompt();
52570
- const adapter = pickAdapter(prepared.protocol, parsedReq, textProtocol, prepared.responsesProjection, prepared.anthropicSystem);
52571
- const loop = runCompressLoop(
52572
- streamToRead,
52573
- { core, config, messages: prepared.processedMessages.length > 0 ? prepared.processedMessages : prepared.originalMessages, session: prepared.session, log: ctx.log, proxyUrl, textProtocol, debug: opts.debug },
52574
- parsedReq,
52575
- { url: upstreamUrl, headers: reqHeaders },
52576
- adapter,
52577
- systemPrompt
52578
- );
52579
- for await (const chunk of loop) {
52580
- {
52581
- const s3 = chunk.toString("utf8");
52582
- if (s3.includes("<acp ") || s3.includes("</acp")) {
52583
- log2("warn", `[${prepared.session.id}] tag echo: ${prepared.protocol} response stream contains <acp tag`);
52584
- }
52585
- }
52586
- res.write(chunk);
52587
- if (res.writableNeedDrain) await new Promise((r) => res.once("drain", () => r()));
52588
- }
52589
- res.end();
52590
- } else if (prepared.protocol === "openai") {
52591
- const parsedReq = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
52592
- const reqHeaders = {};
52593
- for (const [k2, v2] of Object.entries(headers)) {
52594
- if (k2.toLowerCase() === "content-length" || k2.toLowerCase() === "host") continue;
52595
- reqHeaders[k2] = v2;
52596
- }
52597
- reqHeaders["content-type"] = "application/json";
52598
- const loop = compressLoopStream(
52599
- streamToRead,
52600
- { core, config, messages: prepared.originalMessages, session: prepared.session, log: ctx.log, proxyUrl },
52601
- parsedReq,
52602
- { url: upstreamUrl, headers: reqHeaders }
52603
- );
52604
- for await (const chunk of loop) {
52605
- {
52606
- const s3 = chunk.toString("utf8");
52607
- if (s3.includes("<acp ") || s3.includes("</acp")) {
52608
- log2("warn", `[${prepared.session.id}] tag echo: openai response stream contains <acp tag`);
52609
- }
52610
- }
52611
- if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
52612
- }
52613
- } else if (prepared.protocol === "responses") {
52614
- const parsedReq = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
52615
- const reqHeaders = {};
52616
- for (const [k2, v2] of Object.entries(headers)) {
52617
- if (k2.toLowerCase() === "content-length" || k2.toLowerCase() === "host") continue;
52618
- reqHeaders[k2] = v2;
52619
- }
52620
- reqHeaders["content-type"] = "application/json";
52621
- const loop = compressLoopResponsesStream(
52622
- streamToRead,
52623
- { core, config, messages: prepared.originalMessages, session: prepared.session, log: ctx.log, proxyUrl, textProtocol: prepared.responsesTextProtocol },
52624
- parsedReq,
52625
- { url: upstreamUrl, headers: reqHeaders }
52626
- );
52627
- for await (const chunk of loop) {
52628
- {
52629
- const s3 = chunk.toString("utf8");
52630
- if (s3.includes("<acp ") || s3.includes("</acp")) {
52631
- log2("warn", `[${prepared.session.id}] tag echo: responses response stream contains <acp tag`);
52632
- }
51606
+ const parsedReq = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
51607
+ const reqHeaders = buildForwardHeaders(headers);
51608
+ const textProtocol = prepared.protocol === "responses" && !!prepared.responsesTextProtocol;
51609
+ const systemPrompt = textProtocol ? buildCompressTextSystemPrompt() : buildCompressSystemPrompt();
51610
+ const adapter = pickAdapter(prepared.protocol, parsedReq, textProtocol, prepared.responsesProjection, prepared.anthropicSystem);
51611
+ const abortCtrl = new AbortController();
51612
+ req.on("close", () => {
51613
+ if (!res.writableEnded) abortCtrl.abort();
51614
+ });
51615
+ const loop = runCompressLoop(
51616
+ streamToRead,
51617
+ { core, config, messages: prepared.processedMessages.length > 0 ? prepared.processedMessages : prepared.originalMessages, session: prepared.session, log: ctx.log, proxyUrl, textProtocol, debug: opts.debug, nudge: prepared.nudge },
51618
+ parsedReq,
51619
+ { url: upstreamUrl, headers: reqHeaders },
51620
+ adapter,
51621
+ systemPrompt,
51622
+ abortCtrl.signal
51623
+ );
51624
+ for await (const chunk of loop) {
51625
+ {
51626
+ const s3 = chunk.toString("utf8");
51627
+ if (s3.includes("<acp ") || s3.includes("</acp")) {
51628
+ log2("warn", `[${prepared.session.id}] tag echo: ${prepared.protocol} response stream contains <acp tag`);
52633
51629
  }
52634
- if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
52635
51630
  }
52636
- } else {
52637
- const parsedReq = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
52638
- const reqHeaders = {};
52639
- for (const [k2, v2] of Object.entries(headers)) {
52640
- if (k2.toLowerCase() === "content-length" || k2.toLowerCase() === "host") continue;
52641
- reqHeaders[k2] = v2;
52642
- }
52643
- reqHeaders["content-type"] = "application/json";
52644
- const loop = compressLoopAnthropicStream(
52645
- streamToRead,
52646
- { core, config, messages: prepared.originalMessages, session: prepared.session, log: ctx.log, proxyUrl },
52647
- parsedReq,
52648
- { url: upstreamUrl, headers: reqHeaders }
52649
- );
52650
- for await (const chunk of loop) {
52651
- {
52652
- const s3 = chunk.toString("utf8");
52653
- if (s3.includes("<acp ") || s3.includes("</acp")) {
52654
- log2("warn", `[${prepared.session.id}] tag echo: anthropic response stream contains <acp tag`);
52655
- }
52656
- }
52657
- if (!res.write(chunk)) await new Promise((r) => res.once("drain", () => r()));
51631
+ res.write(chunk);
51632
+ if (res.writableNeedDrain) {
51633
+ await Promise.race([
51634
+ new Promise((r) => res.once("drain", () => r())),
51635
+ new Promise((r) => res.once("close", () => r()))
51636
+ ]);
52658
51637
  }
51638
+ if (res.destroyed || res.writableEnded) break;
52659
51639
  }
52660
51640
  res.end();
52661
51641
  } catch (e) {
@@ -52672,12 +51652,7 @@ async function forward(req, res, opts, body, prepared, core, config, log2, route
52672
51652
  let json = JSON.parse(text);
52673
51653
  if (prepared.protocol === "responses" && prepared.responsesTextProtocol) {
52674
51654
  const requestBody = JSON.parse(typeof body === "string" ? body : body.toString("utf8"));
52675
- const requestHeaders = {};
52676
- for (const [key, value] of Object.entries(headers)) {
52677
- if (key.toLowerCase() === "content-length" || key.toLowerCase() === "host") continue;
52678
- requestHeaders[key] = value;
52679
- }
52680
- requestHeaders["content-type"] = "application/json";
51655
+ const requestHeaders = buildForwardHeaders(headers);
52681
51656
  json = await compressLoopResponsesJson(
52682
51657
  json,
52683
51658
  { core, config, messages: prepared.originalMessages, session: prepared.session, log: ctx.log, proxyUrl, textProtocol: true },
@@ -52814,6 +51789,7 @@ function readBody(req) {
52814
51789
  size += c.length;
52815
51790
  if (size > MAX_REQUEST_BYTES) {
52816
51791
  aborted = true;
51792
+ req.destroy();
52817
51793
  reject(new BodyTooLargeError(MAX_REQUEST_BYTES));
52818
51794
  return;
52819
51795
  }