billion-context 0.1.68-pr.428.255 → 0.1.68

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,66 +50,6 @@ 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
-
69
- **Why the carriers differ.** In plugin mode the agent owns compression: the
70
- `compress` call + result live in the agent's own history and are re-sent every
71
- turn, so the summary rides on the tool call and the agent's view never renders
72
- the kernel's `acp_summary` fallback (`billion-context-pi` `src/messages.ts`
73
- skips `acp_summary_*`). In proxy mode the client is not ACP-native, so the
74
- proxy executes `compress` server-side; the tool call never enters the client's
75
- history, and preflight blocks have no tool call at all — so the kernel's
76
- `acp_summary` message is the only carrier. The kernel renders it as role
77
- `system`, but strict OpenAI-compatible backends (SGLang) require exactly one
78
- system message at index 0, so `systemToUser` (`src/util.ts`) re-voices it as a
79
- `user` message, leaving it at its anchor position. This keeps the head system
80
- message (the prefix-cache anchor) byte-stable across compress turns, so a new
81
- block does not invalidate the whole-conversation prefix.
82
-
83
- **Why `user`, not `system` or a forged tool call.** A mid-stream `system`
84
- message is what SGLang rejects (#377). A forged `compress` tool call would be
85
- the "pure" carrier, but in proxy mode it requires fabricating an
86
- assistant `tool_calls` + `user` `tool_result` pair by id, declaring the tool in
87
- the request, and handling preflight blocks that have no authentic call — far
88
- more invasive than re-voicing a standalone note. A `user` message is allowed
89
- anywhere in the conversation, so it is the minimal change that satisfies both
90
- SGLang's one-system rule and prefix-cache stability. The accepted trade-off:
91
- a summary is a stand-in for the folded history, and re-voicing it as a user
92
- turn is a semantic mismatch the model tolerates (it is clearly marked
93
- `[Compressed conversation section]`).
94
-
95
- **Do the two modes coexist?**
96
-
97
- - **Same proxy instance: yes, by design.** One proxy serves plugin and plain
98
- clients at once; `pluginMode` is decided per request (`x-bili-plugin` header)
99
- and bound per session (`session.metadata.pluginAgent`). The launcher reuses a
100
- running proxy.
101
- - **Same session: the mode is sticky.** A session created in plugin mode stays
102
- plugin mode (metadata inheritance); a plain session can only be *upgraded* to
103
- plugin mode if a plugin request arrives with a matching conversation id (the
104
- header outranks) — and never downgraded. In practice a plain→plugin upgrade
105
- requires the plugin client's conversation id to match an existing plain
106
- session id, which doesn't happen (each client generates its own id).
107
- - **Cross-mode block hazard: theoretical only.** It would require the same
108
- conversation id to span a mode switch. plugin→proxy is safe (the tool call is
109
- in the shared history); proxy→plugin could orphan proxy-created block
110
- summaries (their tool call isn't in the agent's history and the agent's view
111
- skips `acp_summary`) — but that needs the id match above, which doesn't occur.
112
-
113
53
  ## Install
114
54
 
115
55
  ```bash
package/dist/index.js CHANGED
@@ -51718,18 +51718,25 @@ function reserveOutputHeadroom(window2, maxOutput) {
51718
51718
  }
51719
51719
  return window2;
51720
51720
  }
51721
- function systemToUser(messages) {
51722
- let hasSys = false;
51723
- for (const m2 of messages) {
51724
- if (m2.role === "system" || m2.role === "developer") {
51725
- hasSys = true;
51726
- break;
51727
- }
51728
- }
51729
- if (!hasSys) return messages;
51730
- return messages.map(
51731
- (m2) => m2.role === "system" || m2.role === "developer" ? { ...m2, role: "user" } : m2
51732
- );
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];
51733
51740
  }
51734
51741
  function shouldReserveOutputHeadroom(protocol) {
51735
51742
  return protocol !== "anthropic";
@@ -52392,7 +52399,7 @@ function createOpenaiAdapter(requestBody, clientSystem) {
52392
52399
  );
52393
52400
  return {
52394
52401
  buildRequest(coreMessages, systemPrompt, body) {
52395
- const messages = systemToUser(coreToOpenai(coreMessages));
52402
+ const messages = hoistMidSystemMessages(coreToOpenai(coreMessages));
52396
52403
  const withSys = injectOpenaiSystem(messages, [clientSystem, systemPrompt].filter((p2) => typeof p2 === "string" && p2.length > 0));
52397
52404
  return { ...body, messages: withSys };
52398
52405
  },
@@ -56638,7 +56645,7 @@ function prepareOpenai(parsed, req, opts, core, config, prompts, log2, session,
56638
56645
  log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit, parsed.model));
56639
56646
  processedMessages = stripKernelSummaries(turn.messages, turn.state);
56640
56647
  reapOrphanBlocks(session, msgs, deactivateBlock);
56641
- rebuiltMessages = systemToUser(coreToOpenai(processedMessages));
56648
+ rebuiltMessages = hoistMidSystemMessages(coreToOpenai(processedMessages));
56642
56649
  const sysParts = [];
56643
56650
  if (systemText) sysParts.push(systemText);
56644
56651
  if (shouldInject) sysParts.push(buildCompressSystemPrompt(prompts));