Skip to content

Use kernel renderTags:"text-only" + forbid tag echoing in prompt - #3

Merged
ranxianglei merged 2 commits into
masterfrom
2026-08-07_render-tags-and-no-echo
Aug 7, 2026
Merged

ranxianglei merged 2 commits into
masterfrom
2026-08-07_render-tags-and-no-echo

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Two changes keeping tags out of model output.

1. server.ts — kernel-driven tag strategy replaces proxy hack

Pass renderTags: "text-only" to processTurn (both Anthropic + OpenAI paths). Requires acp-kernel PR#38 (RenderStrategy three-state). The render-refs node now leaves tool messages (tool-call args / tool-result content) pristine — no tag injected — so structured JSON is never corrupted. Refs are still assigned to every message (assignRefsNode runs unconditionally), so compress ranges still work.

Removes the stripToolTags hack: ACP_TAG_RE constant + stripToolTags() function + 2 call sites. Tag stripping was proxy-side post-processing doing what the kernel should do. Now the kernel does it cleanly via renderTags.

2. compress-tool.ts — forbid tag echoing in prompt

The ACP TAGS prompt section previously said 'Use these annotations to assess which messages are consuming the most context' — this encouraged the model to focus on the tags, and it started reproducing the tag syntax in its output (observed as </acip> typos in replies).

New wording: tags are system metadata injected by the proxy; NEVER echo, repeat, or reference these XML tags in responses; use only the ref ID inside compress calls, never the XML wrapper.

Tags never reach the client

Tags exist only on the proxy→provider request path. Provider responses stream back to the client untouched. This PR ensures they also don't leak into the model's generated text.

Depends on

acp-kernel PR#38 (RenderStrategy). Node_modules overlay used locally for testing; npm bump pending.

Two related changes that keep tags out of model output and client history:

1. server.ts: pass renderTags:"text-only" to processTurn (both Anthropic
   and OpenAI paths). The acp-kernel render-refs node now skips tool
   messages (tool-call args / tool-result content) so structured JSON is
   never polluted by an <acp> tag. This replaces the stripToolTags hack
   (proxy-side post-processing) — the responsibility moves into the kernel
   where ref assignment and tag rendering are properly separated.
   - delete ACP_TAG_RE constant and stripToolTags function
   - delete the two stripToolTags(turn.messages) call sites

2. compress-tool.ts: ACP TAGS prompt section now states tags are system
   metadata injected by the proxy and must NEVER be echoed in responses;
   use only the ref ID inside compress calls. The old wording encouraged
   the model to 'use these annotations', which led it to reproduce the
   tag syntax in its output (observed as </acip> typos in replies).

Tags never reach the client regardless — they exist only between proxy
and provider on the request path. This change ensures they also don't
leak into the model's own generated text.
Request-side (server.ts): after processTurn, log renderTags strategy
and tag-injection counts — confirms text-only keeps tool messages
pristine while text messages stay tagged for the LLM.

Response-side (esbuild-safe): detect <acp tags leaking into model
output. Plain-function ctx.log survives tree-shaking, so detection
lives in server.ts response-forwarding loops (Buffer.toString) and
the two non-stream rewrite functions, NOT inside async generators
(esbuild DCEs ctx.log calls reachable only from generators).
Anthropic routeEvent keeps a fine-grained per-delta check since its
ctx.log survives.
@ranxianglei
ranxianglei merged commit 66feb67 into master Aug 7, 2026
0 of 2 checks passed
ranxianglei added a commit that referenced this pull request Aug 8, 2026
Concurrent requests sharing a session id could interleave processTurn /
stream-rewriter mutations on session.state, corrupting it. Add
withSessionLock(session, fn) — a per-session promise chain that serializes
the prepare → forward critical section.

Also parse the request body once in handle() and pass the parsed object to
prepare*, eliminating duplicate JSON.parse on large payloads (review #3).
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).
antclt pushed a commit to antclt/billion-context that referenced this pull request Oct 16, 2026
The nightly concurrency groups ('schedule' -> shared 'nightly' suffix)
carry cancel-in-progress: true, and GitHub applies that WITHIN the group:
two scheduled runs of the same workflow kill each other. The per-file
comments ('keeps scheduled runs out of that cancel scope', ranxianglei#2637 ranxianglei#3)
describe the intent — pushes must not cancel the nightly run — but the
flag as written also lets the 30-min cron kill its own predecessor if a
run ever outlives the interval: tokens spent, no green recorded, and the
smart gate (ranxianglei#3295) can never learn the head is covered.

Empirically no lane has hit this yet (current scheduled runs finish in
minutes, 0 cancellations in recent history) — this is latent hardening,
not a live-fire fix. cancel-in-progress is now scoped to non-schedule
events, so same-ref supersede semantics are unchanged while scheduled
runs queue behind each other (at most one pending; a superseded PENDING
run costs nothing).
This was referenced Oct 17, 2026
ranxianglei pushed a commit that referenced this pull request Oct 21, 2026
The new breaker (a failed-only mutating round completes the turn
instead of re-requesting) invalidated 24 tests across 15 files that
used a failing compress as their re-request vehicle or pinned the old
internal-continuation behavior. Three adaptation classes:

- contract rewrites (old behavior gone by design): issue1502 salvage
  (no re-request after unsalvageable garbage args), issue3661 forms
  1/2 (exercised on salvage-SUCCESSFUL rounds so the placeholder rides
  a real re-request; raw-malformed-skeleton absence pinned), issue1195
  #2777 (the accepted overflow fold is now asserted to survive into
  the NEXT turn's rebuild), ccr-v2 unarmed opt-in (refusal receipt
  asserted on client SSE), loop-compress #422b (no fold refresh, no
  re-request).
- vehicle swaps to acp_status (the test pins a re-request property,
  not compress itself): loop-core #5, fix-stream abort tests,
  loop-compress #3/#6/#10, strict-echo-model-criterion,
  anthropic-thinking-replay x3, upstream-truncation,
  loop-strict-echo (+responses), visibility-markers streaming pair.
- made genuinely successful (the test needs a landed fold): e2e
  anthropic / responses-chat-relay / proxy-smoke round trips (unique
  session ids + protected-zone tail mass), subagent-namespace
  (positional refs + tail mass + honest usage claims — its mock had
  always failed via hash-boundary refs and only passed on master
  through retry-after-failure).
z0d0dOr pushed a commit to z0d0dOr/billion-context that referenced this pull request Oct 24, 2026
…end contract

The new breaker (a failed-only mutating round completes the turn
instead of re-requesting) invalidated 24 tests across 15 files that
used a failing compress as their re-request vehicle or pinned the old
internal-continuation behavior. Three adaptation classes:

- contract rewrites (old behavior gone by design): issue1502 salvage
  (no re-request after unsalvageable garbage args), issue3661 forms
  1/2 (exercised on salvage-SUCCESSFUL rounds so the placeholder rides
  a real re-request; raw-malformed-skeleton absence pinned), issue1195
  ranxianglei#2777 (the accepted overflow fold is now asserted to survive into
  the NEXT turn's rebuild), ccr-v2 unarmed opt-in (refusal receipt
  asserted on client SSE), loop-compress #422b (no fold refresh, no
  re-request).
- vehicle swaps to acp_status (the test pins a re-request property,
  not compress itself): loop-core ranxianglei#5, fix-stream abort tests,
  loop-compress ranxianglei#3/ranxianglei#6/ranxianglei#10, strict-echo-model-criterion,
  anthropic-thinking-replay x3, upstream-truncation,
  loop-strict-echo (+responses), visibility-markers streaming pair.
- made genuinely successful (the test needs a landed fold): e2e
  anthropic / responses-chat-relay / proxy-smoke round trips (unique
  session ids + protected-zone tail mass), subagent-namespace
  (positional refs + tail mass + honest usage claims — its mock had
  always failed via hash-boundary refs and only passed on master
  through retry-after-failure).

(cherry picked from commit d2c1086)
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