billion-context 0.1.68 → 0.1.69-pr.443.269

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/README.md CHANGED
@@ -50,6 +50,67 @@ Agent (Claude Code / Codex / Cursor / Aider ...)
50
50
 
51
51
  The proxy injects four context-management tools (`compress`, `decompress`, `search_context`, `acp_status`) into the conversation. The model calls `compress` when the conversation grows, and the proxy executes it server-side — the compressed ranges are folded into the conversation history before the next turn.
52
52
 
53
+ ### Two compression modes — who executes `compress`
54
+
55
+ The proxy runs in one of two modes, and **the mode decides who executes
56
+ `compress`, which in turn decides how the summary travels to the model** (the
57
+ "carrier"). This distinction is the root of #377.
58
+
59
+ | | **Launcher / plugin mode** (`bili pi`, `bili codex`, …) | **Proxy mode** (plain client → `/bili/`) |
60
+ |---|---|---|
61
+ | Client | ACP-native agent with the bili extension (pi/omp) | Any OpenAI/Anthropic client, no extension |
62
+ | Who executes `compress` | **The agent** (pi runs it locally) | **The proxy** (server-side compress loop) |
63
+ | `compress` tool call in the re-sent history? | Yes — part of the agent's own conversation | No — ephemeral proxy-loop traffic |
64
+ | Preflight blocks (no tool call)? | No — overflow forces the model to call `compress` | Yes — `src/preflight.ts` compresses behind the client's back |
65
+ | **Summary carrier on the wire** | **the `compress` tool call** | **an `acp_summary` user message** |
66
+ | System messages on the wire | always exactly 1 (client + prompt) | always exactly 1 (client + prompt) — summaries ride on user messages |
67
+ | SGLang "single system" 400 (#377) | cannot happen | cannot happen (summaries are user messages, not system) |
68
+ | Proxy-injected tools + nudge | none (agent owns compression) | the 4 context tools + nudge (when enabled) |
69
+
70
+ **Why the carriers differ.** In plugin mode the agent owns compression: the
71
+ `compress` call + result live in the agent's own history and are re-sent every
72
+ turn, so the summary rides on the tool call and the agent's view never renders
73
+ the kernel's `acp_summary` fallback (`billion-context-pi` `src/messages.ts`
74
+ skips `acp_summary_*`). In proxy mode the client is not ACP-native, so the
75
+ proxy executes `compress` server-side; the tool call never enters the client's
76
+ history, and preflight blocks have no tool call at all — so the kernel's
77
+ `acp_summary` message is the only carrier. The kernel renders it as role
78
+ `system`, but strict OpenAI-compatible backends (SGLang) require exactly one
79
+ system message at index 0, so `systemToUser` (`src/util.ts`) re-voices it as a
80
+ `user` message, leaving it at its anchor position. This keeps the head system
81
+ message (the prefix-cache anchor) byte-stable across compress turns, so a new
82
+ block does not invalidate the whole-conversation prefix.
83
+
84
+ **Why `user`, not `system` or a forged tool call.** A mid-stream `system`
85
+ message is what SGLang rejects (#377). A forged `compress` tool call would be
86
+ the "pure" carrier, but in proxy mode it requires fabricating an
87
+ assistant `tool_calls` + `user` `tool_result` pair by id, declaring the tool in
88
+ the request, and handling preflight blocks that have no authentic call — far
89
+ more invasive than re-voicing a standalone note. A `user` message is allowed
90
+ anywhere in the conversation, so it is the minimal change that satisfies both
91
+ SGLang's one-system rule and prefix-cache stability. The accepted trade-off:
92
+ a summary is a stand-in for the folded history, and re-voicing it as a user
93
+ turn is a semantic mismatch the model tolerates (it is clearly marked
94
+ `[Compressed conversation section]`).
95
+
96
+ **Do the two modes coexist?**
97
+
98
+ - **Same proxy instance: yes, by design.** One proxy serves plugin and plain
99
+ clients at once; `pluginMode` is decided per request (`x-bili-plugin` header)
100
+ and bound per session (`session.metadata.pluginAgent`). The launcher reuses a
101
+ running proxy.
102
+ - **Same session: the mode is sticky.** A session created in plugin mode stays
103
+ plugin mode (metadata inheritance); a plain session can only be *upgraded* to
104
+ plugin mode if a plugin request arrives with a matching conversation id (the
105
+ header outranks) — and never downgraded. In practice a plain→plugin upgrade
106
+ requires the plugin client's conversation id to match an existing plain
107
+ session id, which doesn't happen (each client generates its own id).
108
+ - **Cross-mode block hazard: theoretical only.** It would require the same
109
+ conversation id to span a mode switch. plugin→proxy is safe (the tool call is
110
+ in the shared history); proxy→plugin could orphan proxy-created block
111
+ summaries (their tool call isn't in the agent's history and the agent's view
112
+ skips `acp_summary`) — but that needs the id match above, which doesn't occur.
113
+
53
114
  ## Install
54
115
 
55
116
  ```bash
package/dist/index.js CHANGED
@@ -19050,7 +19050,7 @@ var require_response = __commonJS({
19050
19050
  var assert = __require("assert");
19051
19051
  var { isomorphicEncode, serializeJavascriptValueToJSONString } = require_infra();
19052
19052
  var textEncoder = new TextEncoder("utf-8");
19053
- var Response = class _Response {
19053
+ var Response2 = class _Response {
19054
19054
  /** @type {Headers} */
19055
19055
  #headers;
19056
19056
  #state;
@@ -19222,13 +19222,13 @@ var require_response = __commonJS({
19222
19222
  response.#state = newState;
19223
19223
  }
19224
19224
  };
19225
- var { getResponseHeaders, setResponseHeaders, getResponseState, setResponseState } = Response;
19226
- Reflect.deleteProperty(Response, "getResponseHeaders");
19227
- Reflect.deleteProperty(Response, "setResponseHeaders");
19228
- Reflect.deleteProperty(Response, "getResponseState");
19229
- Reflect.deleteProperty(Response, "setResponseState");
19230
- mixinBody(Response, getResponseState);
19231
- Object.defineProperties(Response.prototype, {
19225
+ var { getResponseHeaders, setResponseHeaders, getResponseState, setResponseState } = Response2;
19226
+ Reflect.deleteProperty(Response2, "getResponseHeaders");
19227
+ Reflect.deleteProperty(Response2, "setResponseHeaders");
19228
+ Reflect.deleteProperty(Response2, "getResponseState");
19229
+ Reflect.deleteProperty(Response2, "setResponseState");
19230
+ mixinBody(Response2, getResponseState);
19231
+ Object.defineProperties(Response2.prototype, {
19232
19232
  type: kEnumerableProperty,
19233
19233
  url: kEnumerableProperty,
19234
19234
  status: kEnumerableProperty,
@@ -19244,7 +19244,7 @@ var require_response = __commonJS({
19244
19244
  configurable: true
19245
19245
  }
19246
19246
  });
19247
- Object.defineProperties(Response, {
19247
+ Object.defineProperties(Response2, {
19248
19248
  json: kEnumerableProperty,
19249
19249
  redirect: kEnumerableProperty,
19250
19250
  error: kEnumerableProperty
@@ -19377,7 +19377,7 @@ var require_response = __commonJS({
19377
19377
  }
19378
19378
  }
19379
19379
  function fromInnerResponse(innerResponse, guard) {
19380
- const response = new Response(kConstruct);
19380
+ const response = new Response2(kConstruct);
19381
19381
  setResponseState(response, innerResponse);
19382
19382
  const headers = new Headers(kConstruct);
19383
19383
  setResponseHeaders(response, headers);
@@ -19431,14 +19431,14 @@ var require_response = __commonJS({
19431
19431
  converter: webidl.converters.HeadersInit
19432
19432
  }
19433
19433
  ]);
19434
- webidl.is.Response = webidl.util.MakeTypeAssertion(Response);
19434
+ webidl.is.Response = webidl.util.MakeTypeAssertion(Response2);
19435
19435
  module.exports = {
19436
19436
  isNetworkError,
19437
19437
  makeNetworkError,
19438
19438
  makeResponse,
19439
19439
  makeAppropriateNetworkError,
19440
19440
  filterResponse,
19441
- Response,
19441
+ Response: Response2,
19442
19442
  cloneResponse,
19443
19443
  fromInnerResponse,
19444
19444
  getResponseState
@@ -48010,7 +48010,11 @@ var MAX_REQUEST_BYTES = 100 * 1024 * 1024;
48010
48010
  var UPSTREAM_TIMEOUT_MS = 10 * 60 * 1e3;
48011
48011
  async function fetchWithTimeout(url, opts, timeoutMs = UPSTREAM_TIMEOUT_MS, externalSignal) {
48012
48012
  const controller = new AbortController();
48013
- const timer2 = setTimeout(() => controller.abort(), timeoutMs);
48013
+ let timer2 = setTimeout(() => controller.abort(), timeoutMs);
48014
+ const rearm = () => {
48015
+ clearTimeout(timer2);
48016
+ timer2 = setTimeout(() => controller.abort(), timeoutMs);
48017
+ };
48014
48018
  let onExternalAbort = null;
48015
48019
  if (externalSignal) {
48016
48020
  if (externalSignal.aborted) controller.abort();
@@ -48019,22 +48023,54 @@ async function fetchWithTimeout(url, opts, timeoutMs = UPSTREAM_TIMEOUT_MS, exte
48019
48023
  externalSignal.addEventListener("abort", onExternalAbort, { once: true });
48020
48024
  }
48021
48025
  }
48026
+ const cleanup = () => {
48027
+ clearTimeout(timer2);
48028
+ if (onExternalAbort && externalSignal) externalSignal.removeEventListener("abort", onExternalAbort);
48029
+ };
48022
48030
  try {
48023
48031
  const finalOpts = { ...opts, signal: controller.signal };
48024
- const response = await fetch(url, finalOpts);
48025
- return {
48026
- response,
48027
- clearTimer: () => {
48028
- clearTimeout(timer2);
48029
- if (onExternalAbort && externalSignal) externalSignal.removeEventListener("abort", onExternalAbort);
48030
- }
48031
- };
48032
+ const raw = await fetch(url, finalOpts);
48033
+ if (raw.body) {
48034
+ const wrapped = armIdleBody(raw.body, rearm);
48035
+ return {
48036
+ response: new Response(wrapped, {
48037
+ status: raw.status,
48038
+ statusText: raw.statusText,
48039
+ headers: raw.headers
48040
+ }),
48041
+ clearTimer: cleanup
48042
+ };
48043
+ }
48044
+ return { response: raw, clearTimer: cleanup };
48032
48045
  } catch (e) {
48033
- clearTimeout(timer2);
48034
- if (onExternalAbort && externalSignal) externalSignal.removeEventListener("abort", onExternalAbort);
48046
+ cleanup();
48035
48047
  throw e;
48036
48048
  }
48037
48049
  }
48050
+ function armIdleBody(body, rearm) {
48051
+ const reader = body.getReader();
48052
+ return new ReadableStream({
48053
+ async pull(controller) {
48054
+ try {
48055
+ const result = await reader.read();
48056
+ if (result.done) {
48057
+ controller.close();
48058
+ return;
48059
+ }
48060
+ rearm();
48061
+ controller.enqueue(result.value);
48062
+ } catch (e) {
48063
+ controller.error(e);
48064
+ }
48065
+ },
48066
+ async cancel(reason) {
48067
+ try {
48068
+ await reader.cancel(reason);
48069
+ } catch {
48070
+ }
48071
+ }
48072
+ });
48073
+ }
48038
48074
  var UpstreamHttpError = class extends Error {
48039
48075
  status;
48040
48076
  body;
@@ -51718,25 +51754,18 @@ function reserveOutputHeadroom(window2, maxOutput) {
51718
51754
  }
51719
51755
  return window2;
51720
51756
  }
51721
- function hoistMidSystemMessages(messages) {
51722
- let lead = 0;
51723
- while (lead < messages.length && (messages[lead].role === "system" || messages[lead].role === "developer")) lead++;
51724
- if (lead === messages.length) return messages;
51725
- let midCount = 0;
51726
- for (let i = lead; i < messages.length; i++) {
51727
- const r = messages[i].role;
51728
- if (r === "system" || r === "developer") midCount++;
51729
- }
51730
- if (midCount === 0) return messages;
51731
- const head = messages.slice(0, lead);
51732
- const mid = [];
51733
- const rest = [];
51734
- for (let i = lead; i < messages.length; i++) {
51735
- const r = messages[i].role;
51736
- if (r === "system" || r === "developer") mid.push(messages[i]);
51737
- else rest.push(messages[i]);
51738
- }
51739
- return [...head, ...mid, ...rest];
51757
+ function systemToUser(messages) {
51758
+ let hasSys = false;
51759
+ for (const m2 of messages) {
51760
+ if (m2.role === "system" || m2.role === "developer") {
51761
+ hasSys = true;
51762
+ break;
51763
+ }
51764
+ }
51765
+ if (!hasSys) return messages;
51766
+ return messages.map(
51767
+ (m2) => m2.role === "system" || m2.role === "developer" ? { ...m2, role: "user" } : m2
51768
+ );
51740
51769
  }
51741
51770
  function shouldReserveOutputHeadroom(protocol) {
51742
51771
  return protocol !== "anthropic";
@@ -52330,6 +52359,20 @@ function rewriteContentChunk(parsed, content) {
52330
52359
 
52331
52360
  `, "utf8");
52332
52361
  }
52362
+ function stripFinishReasonChunk(buf) {
52363
+ const m2 = /^data: (.*)\n\n$/s.exec(buf.toString("utf8"));
52364
+ if (!m2) return buf;
52365
+ try {
52366
+ const parsed = JSON.parse(m2[1]);
52367
+ if (!Array.isArray(parsed.choices)) return buf;
52368
+ parsed.choices = parsed.choices.map((c) => ({ ...c, finish_reason: null }));
52369
+ return Buffer.from(`data: ${JSON.stringify(parsed)}
52370
+
52371
+ `, "utf8");
52372
+ } catch {
52373
+ return buf;
52374
+ }
52375
+ }
52333
52376
  function createOpenaiAdapter(requestBody, clientSystem) {
52334
52377
  const model = requestBody.model ?? "unknown";
52335
52378
  let responseId = `chatcmpl-proxy-${Date.now()}`;
@@ -52399,7 +52442,7 @@ function createOpenaiAdapter(requestBody, clientSystem) {
52399
52442
  );
52400
52443
  return {
52401
52444
  buildRequest(coreMessages, systemPrompt, body) {
52402
- const messages = hoistMidSystemMessages(coreToOpenai(coreMessages));
52445
+ const messages = systemToUser(coreToOpenai(coreMessages));
52403
52446
  const withSys = injectOpenaiSystem(messages, [clientSystem, systemPrompt].filter((p2) => typeof p2 === "string" && p2.length > 0));
52404
52447
  return { ...body, messages: withSys };
52405
52448
  },
@@ -52526,6 +52569,7 @@ function createOpenaiAdapter(requestBody, clientSystem) {
52526
52569
  if (sawRealToolCall) {
52527
52570
  yield { kind: "meta", chunk: rawBuf };
52528
52571
  yield { kind: "done", finishReason, suppressCompletion: true };
52572
+ continue;
52529
52573
  } else {
52530
52574
  yield {
52531
52575
  kind: "done",
@@ -52535,7 +52579,7 @@ function createOpenaiAdapter(requestBody, clientSystem) {
52535
52579
  }
52536
52580
  if (!delta) continue;
52537
52581
  if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
52538
- yield { kind: "reasoning", delta: delta.reasoning_content, raw: rawBuf };
52582
+ yield { kind: "reasoning", delta: delta.reasoning_content, raw: finishReason ? stripFinishReasonChunk(rawBuf) : rawBuf };
52539
52583
  }
52540
52584
  if (delta.tool_calls) {
52541
52585
  const tcs = delta.tool_calls;
@@ -52570,10 +52614,10 @@ function createOpenaiAdapter(requestBody, clientSystem) {
52570
52614
  const clean = tagFilter.push(delta.content);
52571
52615
  if (clean.length > 0) {
52572
52616
  const raw = clean === delta.content ? rawBuf : rewriteContentChunk(parsed, clean);
52573
- yield { kind: "text", delta: clean, ...hasReasoning ? {} : { raw } };
52617
+ yield { kind: "text", delta: clean, ...hasReasoning ? {} : { raw: finishReason ? stripFinishReasonChunk(raw) : raw } };
52574
52618
  }
52575
52619
  } else if (!hasReasoning && (delta.role || Object.keys(delta).length === 0 && !finishReason)) {
52576
- yield { kind: "meta", chunk: rawBuf, firstRoundOnly: true };
52620
+ yield { kind: "meta", chunk: finishReason ? stripFinishReasonChunk(rawBuf) : rawBuf, firstRoundOnly: true };
52577
52621
  }
52578
52622
  }
52579
52623
  },
@@ -56593,7 +56637,7 @@ function prepareAnthropic(parsed, req, opts, core, config, prompts, log2, sessio
56593
56637
  if (injectTools) {
56594
56638
  toolsOut = injectTool(parsed.tools);
56595
56639
  }
56596
- if (opts.compress.injectNudge && turn.nudge?.shouldInject) {
56640
+ if (opts.compress.injectNudge && !pluginMode && turn.nudge?.shouldInject) {
56597
56641
  try {
56598
56642
  const rendered = renderNudgeText(turn.nudge, prompts);
56599
56643
  if (rendered.text) {
@@ -56645,7 +56689,7 @@ function prepareOpenai(parsed, req, opts, core, config, prompts, log2, session,
56645
56689
  log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit, parsed.model));
56646
56690
  processedMessages = stripKernelSummaries(turn.messages, turn.state);
56647
56691
  reapOrphanBlocks(session, msgs, deactivateBlock);
56648
- rebuiltMessages = hoistMidSystemMessages(coreToOpenai(processedMessages));
56692
+ rebuiltMessages = systemToUser(coreToOpenai(processedMessages));
56649
56693
  const sysParts = [];
56650
56694
  if (systemText) sysParts.push(systemText);
56651
56695
  if (shouldInject) sysParts.push(buildCompressSystemPrompt(prompts));
@@ -56653,7 +56697,7 @@ function prepareOpenai(parsed, req, opts, core, config, prompts, log2, session,
56653
56697
  if (injectTools) {
56654
56698
  toolsOut = injectOpenaiTool(parsed.tools);
56655
56699
  }
56656
- if (opts.compress.injectNudge && turn.nudge?.shouldInject && shouldInject) {
56700
+ if (opts.compress.injectNudge && !pluginMode && turn.nudge?.shouldInject && shouldInject) {
56657
56701
  try {
56658
56702
  const rendered = renderNudgeText(turn.nudge, prompts);
56659
56703
  if (rendered.text) {
@@ -56747,7 +56791,7 @@ function prepareResponses(parsed, req, opts, core, config, prompts, log2, sessio
56747
56791
  const devContent = [...projection.systemParts, ...forgedSummaries].join("\n\n---\n\n");
56748
56792
  rebuiltInput = injectResponsesDeveloperMessage(rebuiltInput, devContent);
56749
56793
  }
56750
- if (opts.compress.injectNudge && turn.nudge?.shouldInject && shouldInject && !isCompactionTrigger) {
56794
+ if (opts.compress.injectNudge && !pluginMode && turn.nudge?.shouldInject && shouldInject && !isCompactionTrigger) {
56751
56795
  try {
56752
56796
  const rendered = renderNudgeText(turn.nudge, prompts);
56753
56797
  if (rendered.text) {