Skip to content

feat: OpenAI Responses API support (/v1/responses) - #4

Merged
ranxianglei merged 16 commits into
masterfrom
2026-08-07_responses-api
Aug 8, 2026
Merged

ranxianglei merged 16 commits into
masterfrom
2026-08-07_responses-api

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Aug 7, 2026 •

Copy link
Copy Markdown
Owner

Third protocol alongside anthropic + openai chat completions.

Why

Codex 0.147 dropped `wire_api="chat"` and forces the Responses API. Without this, billion-context can only transparently-proxy Codex (no ACP compression) — defeating the value prop for token-billed official OpenAI users.

What

Core Responses API support

  • src/responses.ts: `ResponsesRequestBody` (`input` items) ↔ `CoreMessage`. Responses input items are typed (`message`/`function_call`/`function_call_output`/`additional_tools`) and map closer to core messages than chat completions. Preserves opaque preamble items (e.g. Codex `additional_tools`) verbatim so host directives survive the round-trip.
  • src/stream-responses.ts: Responses SSE rewriter. Key difference from chat: Responses carries the event type in the SSE `event:` line, NOT in the JSON data body. Routes compress `function_call` events, multi-round compress loop emits complete item sequences (added → deltas → done).
  • src/compress-loop-responses.ts: Multi-round compress loop for Responses (mirrors chat `compressLoopStream`).
  • server.ts: `detectProtocol` matches `/responses` → `responses` protocol; `prepareResponses` runs processTurn(text-only) + injects instructions + nudge + `injectResponsesTool` (flat Responses tool format, all 4 ACP tools); forward uses `rewriteResponsesSseStream` / `rewriteResponsesJsonResponse`.
  • compress-tool.ts: `COMPRESS_TOOL_RESPONSES` and friends — flat `{type:"function", name, parameters}` (not nested `{function:{name,...}}`).

Codex code_mode integration fixes

  • additional_tools preservation: `responses.ts` default branch dropped unknown input items — this silently stripped Codex `additional_tools`, which disabled code_mode (client-side tools) entirely. Fixed with an opaque-item-type set that preserves preamble items verbatim.
  • developer message contract: compress prompt must go in a `developer` message, NOT the top-level `instructions` field — non-empty `instructions` breaks `responses_lite` tool exposure.
  • custom_tool_call / output round-trip: results were dropped → Codex death loop. Added core-message conversion for these item types.

Robustness fixes (3 rounds of codex review)

  • Tool-type preservation (`get_weather` stays `function_call`, not rewritten to `custom_tool_call`).
  • Failed/incomplete/error response terminal handling — verbatim passthrough, no synthetic `completed`.
  • CRLF SSE normalization (shared `sse-util.ts` `normalizeSseLineEndings`) across all 5 stream splitters + residual buffer drain + multi-line `data:` join.
  • Query-string protocol detection (`url.split("?")[0]` before `endsWith`).
  • `fetchWithTimeout` stream-stall abort (active timer cleared only after stream fully consumed).

Message id stability (critical correctness fix)

  • Problem: all three wire converters assigned `id: `raw-${idx}`` — a position index, not an identity. None of the three protocols attach a stable id to request-side message items. When a client deletes/reorders messages, downstream ids shifted → `covered` sets silently swallowed live content under stale summaries.
  • Fix: content-fingerprint ids (`h_${sha256(role|contentType|toolCallId|toolName|text)}`, src/message-id.ts) with a `ClusterCounter` that disambiguates duplicate content (`h_xxx` vs `h_xxx_1`). tool_call/tool_result bind to the protocol's `call_id`.
  • Orphan block GC (src/orphan-gc.ts): fingerprint ids mean a block whose messages the client deleted becomes an orphan (summary floats to top). After each `processTurn`, blocks with zero id hits for `ORPHAN_THRESHOLD=3` consecutive turns are deactivated + their cache cleared. Partial orphans (some ids still present) are left alone.

Decompress correctness across rounds

  • Problem: `collectBlockContent` scans `ctx.messages`, but proxy only holds the post-fold view per round — original messages from earlier rounds are gone → decompress returned empty + deactivated the block (data loss).
  • Fix: `session.blockContents` caches block originals at compress time (`{one, full}` two views). `src/decompress-shared.ts` (`resolveDecompress`) shared by both compress-loop files honors the `full` flag against the cache and deletes the cache entry on successful decompress.

Reverse-proxy fixes

  • content-encoding stripping (server.ts:42): Node fetch (undici) transparently decodes gzip/br, but forwarded response headers still carried `content-encoding: gzip` → clients tried to decompress plaintext → JSON/SSE parse failures. Added `content-encoding` to `UPSTREAM_HOP_HEADERS` (filters both request + response copy-back). Found by codex review.

Verification

  • typecheck ✅ / 105 test ✅ / build ✅ (88.86 KB)
  • Live: Codex 0.147 → proxy → comfly relay → OpenAI works end-to-end; compression triggers correctly; cache-hit rate 91-98% (prefix cache preserved).
  • Code reviewed by codex (4 rounds: initial + 3 follow-up bug batches).

Known limitations (follow-up PRs)

  • Persistence (next priority): sessions live in process memory — a proxy restart drops all compression state. For long sessions (millions of tokens of accumulated history) this is a showstopper: without the saved block summaries the client re-sends full raw history which exceeds model context limits, hanging the session permanently. Persistence will follow the proven `SessionStateStore` pattern from billion-context-pi (atomic temp+rename write, `mergeInitialState` forward-compat) adapted for proxy (debounced async writes, `blockContents` in a separate file, session id via `x-acp-session` header).
  • Single-round compress path superseded by the multi-round loop; no remaining single-round code path in use.

acp-kernel pinned to exact 0.0.17 (for `renderTags: "text-only"`).

Third protocol alongside anthropic + openai chat completions. This lets
billion-context serve clients that only speak the Responses API (notably
Codex 0.147, which dropped wire_api='chat' and forces 'responses').

New files:
- src/responses.ts: ResponsesRequestBody (input/items) <-> CoreMessage.
  input items are typed (message/function_call/function_call_output) and
  map closer to core messages than chat completions does.
- src/stream-responses.ts: Responses SSE rewriter. Responses carries the
  event type in the SSE event: line (not in JSON data, unlike chat).
  Suppresses compress function_call arguments, emits result as a text
  delta + clean response.completed at the end.
- tests/proxy-responses-stream.test.ts: passthrough + compress-suppress.

server.ts wiring:
- detectProtocol: /responses -> 'responses'
- prepareResponses: responsesToCore -> processTurn(text-only) ->
  coreToResponses -> inject instructions (system) + nudge (trailing
  user item) + injectResponsesTool (flat Responses tool format).
- forward: responses branch uses rewriteResponsesSseStream (single-round;
  multi-round compress loop is chat-completions-only for now).
- non-stream: rewriteResponsesJsonResponse.

compress-tool.ts: COMPRESS_TOOL_RESPONSES (flat {type:function,name,
parameters} form, not nested function:{...}).
compressLoopResponsesStream mirrors compressLoopStream but for the
Responses API: accumulates function_call items (output_item.added +
function_call_arguments.delta), executes proxy-only calls, appends
function_call + function_call_output items to input[], and re-requests
(up to 5 rounds). Real function calls and plain text pass through.

This fixes the single-turn bug where a compress call ended the Codex
turn, leaving the model unable to continue its work.

- src/compress-loop-responses.ts (new): the loop + SSE event classifier
- src/compress-loop.ts: export buildVisibilityMarker (reused)
- src/server.ts: Responses branch now uses the loop instead of the
  single-turn rewriter
- tests/proxy-responses-stream.test.ts: multi-round test with mocked
  fetch (compress -> re-request -> continue)
…t.delta)

Fixes the 'OutputTextDelta without active item' error from Codex. The
Responses API requires a message item to be declared via
response.output_item.added + response.content_part.added BEFORE any
response.output_text.delta for that item. Previously the proxy emitted
bare output_text.delta with an undeclared fixed item id.

Changes:
- classifyResponsesSseEvent: mark response.created/in_progress/completed
  as isMeta so round 2+ suppresses them (merged stream = single open)
- buildMessageItemSequence replaces buildOutputTextDelta: emits the full
  added+content_part.added+delta+done+content_part.done+item.done sequence
- Unified passthrough: round 2+ now passes upstream content events as-is
  (preserving their own output_item.added declarations) instead of
  re-wrapping deltas with a synthetic item id
- nextOutputIndex counter tracks emitted items across rounds
…esponses_lite)

Three layered bugs prevented Codex code_mode from working through the proxy.
All verified with real Codex CLI + comfly relay (gpt-5.6-sol, responses_lite).

1. additional_tools dropped (src/responses.ts)
   responsesToCore's default branch discarded all unknown item types,
   including `additional_tools` which carries the code_mode exec/wait tool
   definitions and must stay at input[0] (openai/codex client.rs splice(0..0)).
   Fix: OPAQUE_ITEM_TYPES set + preamble capture; prepareResponses re-prepends
   them verbatim so code_mode tool exposure is preserved.

2. responses_lite contract broken (src/server.ts)
   injectResponsesInstructions wrote the compress prompt into the top-level
   `instructions` field, but responses_lite requires instructions="" (the
   real prompt lives in input[1] developer message). A non-empty instructions
   field took the response off the lite processing path.
   Fix: inject compress prompt + base system as a developer message inside
   input (after additional_tools, before conversation); top-level instructions
   is never touched.

3. exec results dropped -> infinite loop (src/responses.ts)
   custom_tool_call / custom_tool_call_output (code_mode exec call + result)
   hit the default branch and were discarded. The model never saw its own
   tool results, so it re-issued the same exec call every round.
   Fix: add explicit cases in responsesToCore (-> assistant tool-call /
   tool tool-result) and coreToResponses (-> custom_tool_call /
   custom_tool_call_output), so exec history round-trips through compression.

Also:
- ACP_DUMP_REQ=1 gates request body dumps (avoid concurrent file corruption)
- ACP_DEBUG logs input item types + rebuilt conversion stats
- ACP_NO_COMPRESS_PROMPT / ACP_RENDER_NONE are debug toggles
- Text-protocol scaffolding (stream-responses.ts, buildCompressTextSystemPrompt)
  remains in tree but is inert under the default function protocol.
Addresses review findings on the HTTP layer before public/long-running use:
- readBody: cap at 100 MB (reject oversized bodies with 413) to avoid
  memory exhaustion; previously unbounded.
- All three upstream fetch sites (server.ts forward, compress-loop,
  compress-loop-responses) now use fetchWithTimeout (10 min AbortController)
  so a stuck upstream cannot hold a client connection open forever.
- config.ts safeReadJson no longer silently swallows parse errors (only
  ENOENT is quiet); a malformed providers file now logs a warning.
- server.ts catch returns 413 for body-size rejections (vs generic 502).
- session.ts: documents the known limitation that state is in-process only
  (restart drops it; multi-instance does not share) and that session id
  defaults to a hash of the first user message (multi-tenant must send
  x-acp-session). Persistence tracked as TODO.

New file: src/fetch-util.ts (fetchWithTimeout + MAX_REQUEST_BYTES).
…ests

Addresses four protocol-correctness bugs found by reviewing the Responses
implementation. All covered by new tests (tests/proxy-responses-review.test.ts).

1. Standard function tools were rewritten as custom tools (responses.ts)
   coreToResponses used PROXY_TOOL_NAMES.has(name) to pick function_call vs
   custom_tool_call. A host's normal function tool (e.g. get_weather) hit the
   else branch and was emitted as custom_tool_call — changing Responses API
   semantics and risking upstream rejection.
   Fix: track call_ids that entered as custom_tool_call/custom_tool_call_output
   in a Set, and emit the original item type by call_id membership.

2. Fabricated success after a failed terminal (compress-loop-responses.ts)
   classify only treated response.completed as terminal. response.failed and
   response.incomplete passed through (default), then buildCompleted emitted a
   synthetic response.completed — giving the client a contradictory state
   machine (failed AND completed).
   Fix: classify failed/incomplete/error as terminal (isMeta, no auto-yield),
   capture the raw event, and at stream end replay it verbatim instead of
   synthesizing completion.

3. Valid CRLF SSE streams were swallowed (compress-loop-responses.ts)
   The SSE loop split on indexOf("\n\n"). A service emitting \r\n\r\n event
   separators never matched, so events (including response.created/completed)
   vanished and the proxy synthesized a bogus response id.
   Fix: normalize CRLF / lone CR to LF after each decode; join multi-line
   data: fields per SSE spec (previously only the first data: line was kept).

4. Unknown Responses input items silently dropped (responses.ts)
   The default switch branch dropped anything outside the fixed type list, so
   OpenAI adding new item types (or provider extension types) would lose data.
   Fix: preserve unknown items verbatim in the preamble (forward-compat).

Also:
- ACP_DUMP_REQ=1 gates request body dumps (avoid concurrent file corruption)
- ACP_DEBUG logs input item types + rebuilt conversion stats
- [acp-usage] logs per-round cache hit ratio from response.completed usage
- [acp-compress-args]/[acp-compress-input] diagnostics for compress parse failures
…ss/acp_status)

injectResponsesTool only injected COMPRESS_TOOL_RESPONSES, but the compress loop
(compress-loop-responses.ts executeProxyTool) dispatches all 4 tool names in
PROXY_TOOL_NAMES, and the injected compress prompt documents search_context /
decompress usage. The model could read the instructions, try to call
search_context to locate a block, then decompress it — but those tools were
absent from the tools array, so the model reported 'cannot call search_context'.

This was a declaration/implementation mismatch: the loop was ready to execute
decompress/search_context/acp_status, but server.ts never declared them.

Fix: add DECOMPRESS_TOOL_RESPONSES, SEARCH_CONTEXT_TOOL_RESPONSES,
ACP_STATUS_TOOL_RESPONSES (Responses flat format), group them as
ACP_TOOLS_RESPONSES, and injectResponsesTool now appends all four (idempotent
on tool name, matching the OpenAI chat path which already injects all four via
ACP_TOOLS_OPENAI).
Four protocol-correctness issues found by review.

1. Query string broke protocol detection (server.ts)
   detectProtocol used url.endsWith("/responses") etc. A request like
   /v1/responses?foo=1 did not match, so compression/injection silently
   disabled. Strip the query string (url.split("?")[0]) before matching.

2. CRLF SSE events lost across ALL paths (not just responses)
   The SSE splitter only matched \n\n. CRLF-emitting upstreams (\r\n\r\n)
   dropped every event and forced synthesized response ids. Previously only
   the responses path was fixed; now a shared normalizeSseLineEndings helper
   (sse-util.ts) is applied at every split site: compress-loop.ts (chat),
   compress-loop-responses.ts, stream.ts, stream-openai.ts, stream-responses.ts.
   Also drains the residual SSE buffer at stream end in compress-loop.ts
   (previously the last event was dropped if the stream lacked a trailing
   blank line).

3. 10-min timeout did not cover the response body (fetch-util.ts)
   fetchWithTimeout cleared the timer when fetch() resolved (headers only), so
   a stalled SSE body was never aborted — contradicting the docstring.
   Changed the API to return { response, clearTimer }; callers clear the
   timer only after the body stream is fully consumed, so the timeout now
   guards the full headers+body lifecycle. All three call sites updated
   (compress-loop.ts, compress-loop-responses.ts multi-round loops use an
   activeClearTimer cleared in the reader finally; server.ts passthrough
   clears at every stream-consumed exit point).
…ess time)

decompress used collectBlockContent(state, block, ctx.messages) which matches
block.effectiveMessageIds against CoreMessage.id in ctx.messages. But
ctx.messages is the POST-FOLD view (prepared.processedMessages): messages
already compressed in an earlier round are replaced by their summary and are
absent, so collectBlockContent returns 0 items. The block was then
deactivated unconditionally, discarding it and making later retries fail.

A second, deeper issue: message ids are raw-${idx}, regenerated every round
by responsesToCore, so ids are not stable across rounds — even keeping the
pre-fold messages would not guarantee a match.

Fix: cache the original content at compress time, when the source messages
are still present in this round's ctx.messages (a freshly-compressed block
has not been folded yet — folding happens in the next processTurn). The
cache lives on the session (session.blockContents: Map<blockId, {text,count}>).
decompress reads from the cache first and only falls back to
collectBlockContent. deactivateBlock now runs only when content was actually
recovered (count>0 or a cache hit), so a 0-result no longer poisons the block.

Applied to both the chat path (compress-loop.ts) and the responses path
(compress-loop-responses.ts). Originals are captured in stream.ts applyRanges
right after applyCompression, by diffing block ids before/after and calling
collectBlockContent on the new blocks with full:true.
Codex review found two issues with the decompress cache added in d75326d:

1. full:false semantics broken — cache was populated with full:true
   unconditionally, so a decompress hit returned the complete original
   even when the caller did not pass full:true (default is one-level:
   direct messages + nested child summaries).

2. cache never cleared — successful decompress deactivated the block in
   state but left the cached originals in session.blockContents, causing
   unbounded memory growth and retained sensitive content over a long
   session.

Fix:
- session.blockContents now stores {one, full} views; applyRanges caches
  both at compress time.
- resolveDecompress (extracted to src/decompress-shared.ts, shared by
  both compress-loop files) selects the view by args.full, deletes the
  cache after a successful decompress, and only deactivates the block
  when content was actually recovered.

Tests: 4 new (proxy-decompress-cache.test.ts) covering one vs full views,
cache deletion, and missing-cache fallback. 89 pass.
PROBLEM: the three wire converters (anthropic.ts/openai.ts/responses.ts)
assigned message ids as `raw-${idx}` — a *position* index, not an
identity. None of the three protocols attach a stable id to request-side
message items, so ids were derived from array position. As soon as a
client (or another plugin) deletes/reorders messages:

  Turn1 [A,B,C,D,E,F] → compress B,C → effectiveMessageIds=[raw-1,raw-2]
  Turn2 client drops B,C → sends [A,D,E,F] → D shifts to raw-1, E to raw-2
    covered={raw-1,raw-2} now matches D and E → they get swallowed under
    the B+C summary. Live content silently disappears.

FIX 1 — content fingerprints (src/message-id.ts):
  id = h_${sha256(role|contentType|toolCallId|toolName|text)}
  Two identical messages collide; a ClusterCounter appends _${N} so the
  Nth duplicate gets a distinct id (prevents `covered` sets from
  collapsing two unrelated turns onto one id and losing recent content).

  Trade-off vs a real client id: deleting a duplicated message only
  disturbs the cluster of that identity (local damage), never the whole
  downstream (global damage like position). Fully distinct messages are
  completely immune to reordering/deletion.

  tool_call/tool_result bind to the protocol's call_id (tool_use.id /
  tool_call_id / call_id), so two same-name same-args calls stay distinct.

FIX 2 — orphan block GC (src/orphan-gc.ts):
  With fingerprints, a block whose effective ids the client has deleted
  becomes an orphan: no anchor in the visible context, so its summary
  floats to the top (insertAt ?? 0), polluting the model's view. After
  each processTurn we count consecutive turns a block has had zero id
  hits; at ORPHAN_THRESHOLD=3 it is deactivated and its blockContents
  cache entry is dropped. Partial orphans (some ids present) are left
  alone — deactivating on partial matches would destroy blocks during
  normal incremental edits.

Tests (10 new, 99 pass):
- proxy-message-id: stability, reordering immunity, duplicate distinctness,
  call_id binding, prefix-vs-mNNNNN non-collision (anthropic/openai/responses).
- proxy-orphan-gc: hit-keeps-active, threshold-reap, streak-reset, cache-clear.
- basic/proxy-basic: updated h_* prefix assertions.
1. reapOrphanBlocks now checks the client's raw input (msgs) instead of
   the kernel's folded output (turn.messages). The folded output never
   contains the original ids of compressed messages, so the old param
   would eventually count every block as orphaned. (In practice h_msg0
   survives in the fold so it didn't reap, but msgs is the correct
   semantic — it reflects what the client actually sent.)

2. prepareResponses logs the final tools[] forwarded upstream when
   ACP_DEBUG is set, showing each tool's type:name (function vs
   namespace vs custom) so we can confirm ACP tools are present.

3. forward() tool-name diagnostic fixed: Responses API uses flat
   {name} not nested {function:{name}}, so it always showed '?'.
@ranxianglei
ranxianglei force-pushed the 2026-08-07_responses-api branch from d0d9b31 to 95354e9 Compare August 8, 2026 01:02
Node's fetch (undici) transparently decodes gzip/br responses, so
response.body is plaintext. But response.headers still carries the
upstream 'content-encoding: gzip' marker. The proxy copied upstream
headers verbatim to the client (server.ts:520), so clients received
'content-encoding: gzip' over plaintext bytes and tried to decompress
them a second time — corrupting JSON/SSE parsing.

Add 'content-encoding' to UPSTREAM_HOP_HEADERS so it is dropped on both
sides: the request forward (client→upstream, line 508) and the response
copy-back (upstream→client, line 520). This is the standard reverse-
proxy fix for the fetch-auto-decompress trap.

Found by codex code review.
The proxy injects <acp tokens=".." type="..">mNNNNN</acp> tags into INPUT
messages so the model can reference them. When the model echoes a tag in its
OUTPUT (in-context learning), the metadata leaks to the client as visible
noise — most visible in Codex, which renders raw output text.

All three protocols had tag-echo DETECTION (warn logs) but none STRIPPED.
This adds a streaming-safe stripper to the responses path (stream + non-stream):

Stream (stream-responses.ts):
- stripInjectedTagsFeed/Flush: incremental buffer that drops complete echoed
  tags, holds back a potential partial open marker at delta boundaries, and
  drops unclosed tag fragments at stream end.
- routeResponsesEvent intercepts response.output_text.delta (both modes),
  runs the delta through the stripper, emits the safe portion (or suppresses).
- Generator flushes the stripper buffer before the compress tail.
- Open marker <acp  (space) and close </acp> are distinct from the text-
  protocol trigger <acp_compress>/</acp_compress>, so they never interfere.

Non-stream (rewriteResponsesJsonResponse):
- Regex strip of <acp [^>]*>...</acp> from all output_text parts, regardless
  of whether compress ran.

Tests (6 new, 105 pass): single-delta strip, split-delta strip, partial-open
hold+release, <acp_compress> not stripped, non-stream strip, no-tag unchanged.
@ranxianglei
ranxianglei merged commit 50ad00f into master Aug 8, 2026
2 checks passed
ranxianglei added a commit that referenced this pull request Aug 8, 2026
Two bugs from the latest proxy review:

1. readBody() called req.destroy() after rejecting — the client connection
   was cut off with no HTTP response, leaving handle() unable to write 413.
   Fix: reject with a typed BodyTooLargeError WITHOUT destroying; handle()
   catches it and writes a clean 413 JSON response.

2. TEXT_PROTOCOL suppressed ALL output_text.delta/done and forged a
   response.completed with output:[] even when NO compress trigger was
   present, corrupting normal Responses output. Fix: only rebuild when a
   trigger was actually caught; otherwise pass every event through verbatim.

Added 4 regression tests (3 readBody + 1 responses no-trigger passthrough).
LX666-666 pushed a commit to LX666-666/billion-context that referenced this pull request Aug 9, 2026
…ianglei#2,ranxianglei#3,ranxianglei#4,ranxianglei#6)

ranxianglei#1 (CRITICAL) protocol conversion was lossy: image->[image] placeholder,
  thinking signature dropped (Anthropic rejects unsigned thinking), tool_result
  .is_error dropped, OpenAI developer->system, image_url/input_image deleted.
  Fix: BiliMessage sidecar (src/bili-message.ts) extends CoreMessage with
  optional raw*/signature/isError/originalRole/image* fields. The kernel only
  does {...msg} spread copies, so sidecar survives the pipeline to coreToX,
  which replays the original block losslessly. 8 round-trip tests added.

#2b (MAJOR) session-id extractKey() lowercased the API key before hashing —
  two distinct valid keys differing only in case collided into one hash bucket.
  Remove toLowerCase; keep raw (trim only).

ranxianglei#3 (MAJOR) auto-update stale-lock takeover never deleted the stale lock file,
  so writeFile({flag:'wx'}) always failed EEXIST -> catch returned null ->
  a single crash during update permanently blocked all future auto-updates.
  Add unlink(LOCK_FILE) before the wx write, with ENOENT tolerated.

ranxianglei#4 (MAJOR, security) /__bili/ management endpoints (config read/write, reload,
  session stats) had no auth. With --host 0.0.0.0 a remote caller could rewrite
  upstream routing to exfiltrate API keys. Gate all /__bili/ + /__acp/ paths to
  loopback connections only (127.x / ::1); model-traffic endpoints stay open.

ranxianglei#6 (MINOR) build artifact was not self-contained: acp-kernel stayed as a
  runtime dependency (dist had 'from "acp-kernel"'). tsup now sets
  noExternal:[acp-kernel,node-forge] and acp-kernel moves to devDependencies,
  keeping the exact pin (0.0.17). dist verified: 0 'from acp-kernel' refs.
ranxianglei added a commit to LX666-666/billion-context that referenced this pull request Aug 10, 2026
…eload

Review feedback from ranxianglei (PR#73 comments ranxianglei#3-ranxianglei#4):

1. [ranxianglei#9 critical] Restore Responses session-identity fallback chain
   (responses.ts): header → body.session_id → metadata.session_id →
   previous_response_id → content-fingerprint(hashId). The previous
   generated-randomUUID() broke compression for headerless clients
   (Pi, plain OpenAI SDK) — every request got a new session so
   compression state never accumulated.

2. [ranxianglei#1+ranxianglei#2 critical] parseState robustness (codex-takeover.ts): corrupted
   state file now returns undefined (treat as stale/recoverable) instead
   of throwing, which would leave Codex CLI unusable. Added
   sectionPrefixLength type validation (non-negative number) to prevent
   NaN slicing that could wipe the entire config.toml.

3. [Comment#4 ranxianglei#2] PUT /__bili/config onChanged now also refreshes routes
   (server.ts): previously only proxy fields were refreshed, so pure API
   callers (non-Web-UI) had to also POST /reload for provider config to
   take effect. Now PUT guarantees save-then-apply in one call.

4. Fix misleading context-window hint on routing page (page.ts).
ranxianglei pushed a commit that referenced this pull request Oct 14, 2026
…mum (#3071)

#3071: on a model with NO declared output ceiling, the proxy's starved-budget
restore used the client's own last non-starved high-water as its only lower
bound. That estimate decays unbounded from the client's raw-history size
(..., 3071, 2061, 252, <=200), so once starved the session pins at a death
rattle and every turn truncates at max-tokens (finish=length, 0 visible chars);
a fresh fork child inherits the rattle and forwards max_tokens=1 verbatim. On
v0.1.195 this blanked 9 turns across 3 DSH sessions in one day.

Fix (minimal — matches the issue's suggestion #1 literally): when no source
knows a ceiling, the STARVED / born-starved / fork-inherited restore target is
floored at OUTPUT_RESTORE_ABSOLUTE_FLOOR (8192). A non-starved value is still
trusted verbatim (client's explicit choice; far smaller blast radius), and a
known-ceiling model is untouched. The no-ceiling WARN dedup moves from
once-per-model (global) to once-per-session+model so each affected session logs
its own WARN instead of staying silent behind another session's first hit.

Paths fixed: 1 (no-ceiling restore pinned at poisoned high-water), 2a (killed
session got zero warns), 2b (fork child forwarded the starved budget verbatim).
Path 3 (headroom reserving the shrunken budget) needs no change: the restore
runs before the headroom reservation reads max_tokens, so the floored value
flows in automatically.

Deferred to owner sign-off (product decisions / pinned fail-closed behavior,
flagged in-thread): #4 invalidating the #2490 dsh planning floor after a refused
replay (contradicts its sticky-by-design overflow guarantee), and #5 retrying a
starved finish=length turn (reverses #2756's deliberate exclusion; the root-cause
floor fix should remove most of those empty turns first).

Closes #3071
@ranxianglei
ranxianglei deleted the 2026-08-07_responses-api branch October 15, 2026 05:31
This was referenced Oct 15, 2026
ranxianglei pushed a commit that referenced this pull request Oct 21, 2026
… fold-reconcile fully re-anchors (#4131)

The #1001 unannounced-rewrite detector judged history rewrites on raw-id
hash identity alone, outside the order-centric reconcile ladder. A bili
upgrade across v0.1.201 re-mints non-text message ids by design (#3642 D1
accepted this one-time churn and promised the reconcile ladder would absorb
it), so on the first turn after a cross-version restart the detector saw
the collapse, marked a compaction boundary anyway, and applyCompactionArchive
pruned the ref map + tombstoned hundreds of still-cited refs — destroying
fold coverage for a pure transform (incident: 212 covered refs re-anchored
with 0 unmatched, then pruned to 392 live ids).

detectUnannouncedHistoryRewrite now takes this pass's fold-reconcile verdict:
when kind is reanchored/resend with unmatched === 0, the collapse is
explained churn — the destructive boundary is suppressed and an
id-churn-adoption conflict event + info log are recorded instead. Off/noop
verdicts carry no evidence and keep legacy behavior; partial claims
(unmatched > 0) keep the legacy escalation path unchanged.

Second facet (issue expectation #4): applyCompactionArchive now names in
its log line how many refs the prune drops and which block coverages they
sit under, so any remaining destructive prune is auditable instead of silent.

Tests: tests/issue4131-cross-version-churn-gate.test.ts pins both directions
(explained churn suppressed; genuine rewrite still escalates; off/noop
verdicts do not suppress; legacy callers unchanged) plus the log facet.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant