Skip to content

fix: carry per-conversation id in an ephemeral trailing note, not the static system (#1611) - #1620

Closed
ranxianglei wants to merge 3 commits into
masterfrom
2026-09-29_conv-id-trailing-prefix-cache
Closed

ranxianglei wants to merge 3 commits into
masterfrom
2026-09-29_conv-id-trailing-prefix-cache

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

What changed

withConversationIdNote() (src/compress-tool.ts) embedded the live per-conversation id in the static system prompt:

[Your bili conversation id: pfa-…. When calling the bili compression tools, pass this value as the conversation_id argument so a shared MCP process can route the call to THIS session.]

A fork / resume / session-switch continues under a new canonical id while the message history stays byte-identical (#1486). That one system line was then the only cross-session divergence, so the upstream prompt cache missed entirely (cached_tokens=0) even though the whole history was identical — nullifying #1486's byte-exact resume inheritance.

Fix

  • src/compress-tool.ts — withConversationIdNote(text) now emits a constant, byte-stable pointer (CONVERSATION_ID_POINTER_NOTE, no id value); new conversationIdTrailingNote(id) returns [Your bili conversation id: <id>].
  • src/server.ts — each of the four client lanes appends that trailing note as an ephemeral user message, under the same gate the old id-in-system used: Anthropic opts.compress.injectTool, OpenAI/Google shouldInject, Responses shouldInject && !isCompactionTrigger && !ACP_NO_COMPRESS_PROMPT. The compress-loop re-request keeps the constant system pointer (no trailing note — it's proxy-driven; tools execute server-side).

This mirrors the existing nudge / imgNote / retrieveNote pattern ("not persisted, never enters the agent's re-sent history, safe for the prefix-cache anchor"). MCP routing is unchanged: the model still reads the value from context and echoes it as the conversation_id argument (src/mcp.ts routes on the argument).

Behavior change (flagged per AGENTS.md §4)

  • System prompt text changed. Old: the full [Your bili conversation id: <id>. When calling…THIS session.] line sat in the static system. New: the static system carries only the constant pointer ("Your bili conversation id is provided in a short ephemeral message near the end of this conversation…") and the live value appears as a trailing user message [Your bili conversation id: <id>].
    • Why: the id value is per-conversation, not stable across forks/resumes; embedding it in system made every cross-session continuation diverge at exactly one line → 0% cache. Moving it to the ephemeral tail keeps the system — and therefore the whole-history prefix — byte-identical across sessions.
  • Wire shape: +1 trailing user message on every tool-injecting request (same class of change as the already-shipped nudge/imgNote/retrieveNote tails). Within-session cache hits are unchanged (the note sits past the divergence point); cross-session / fork / resume now hit through the entire history instead of 0%.

Both compression modes were reasoned: plugin mode (agent owns compression) and proxy mode (proxy executes compress) both keep the note present under the identical gate, so no mode-specific regression.

Pre-flight

  • npm run typecheck — clean
  • npm test — 3123 tests, 0 fail (two consecutive runs)
  • npm run build — success
  • E2E preflight (E2E_CHECK=1 tests/e2e/e2e-codex.test.ts) — green (codex-cli 0.147.0 + dist OK). The full token-costing e2e needs a live upstream not reachable from this sandbox; the authoritative e2e runs in CI (ci-e2e.yml) since this touches the request pipeline (src/server.ts).

Test updates reflect the intended +1 trailing message: count/shape assertions bumped by one, and the #1548 append-stability check now excludes the convnote tail slot alongside the chain tag (the byte-level cacheable history prefix is unchanged by this fix).

Closes #1611

Refs: #760 (per-call conversation_id), #1486 (byte-exact resume inheritance), #1541/#1542 (chain stamps), #1592 (0% signature quantified).

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-09-29_conv-id-trailing-prefix-cache (3586673)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.1562-pr.1620.c6af033

This exact version is immutable and reproducible. The rolling pr tag tracks the newest test build across all PRs.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr1620.tgz
npm install -g package

Install instructions are refreshed on each push.

ework-agent added 3 commits September 29, 2026 02:18
… static system

#1611: withConversationIdNote() embedded the live conversation id in the
static system prompt. A fork/resume/session-switch continues under a NEW
canonical id while the history stays byte-identical (#1486), so that single
system line was the only cross-session divergence and it collapsed the
upstream prefix cache to 0% (cached_tokens=0 despite identical history).

The system now carries only a byte-stable pointer (CONVERSATION_ID_POINTER_NOTE,
constant across every session); the live value travels in an ephemeral trailing
user message on each wire lane (conversationIdTrailingNote), mirroring the
nudge/imgNote/retrieveNote pattern. Per-lane gates are unchanged from the old
id-in-system gate. MCP routing is unaffected: the model still reads the value
from context and echoes it as the conversation_id argument (src/mcp.ts routes
on the argument).

Test updates reflect the intended +1 trailing user message per tool-injecting
request: count/shape assertions bumped by one, and the #1548 append-stability
check now excludes the convnote tail slot alongside the chain tag (the byte-level
cacheable history prefix is unchanged).
… note

The #1611 fix appends the per-conversation id as an ephemeral trailing
USER message after the client history on every tool-injecting request.
The native-lane fake scripts the model off the LAST user message (the
directive queue source) and reads lastUserRef from it — so the new tail
masked the real prompt: the scripted compress never fired (pi lane) and
the filler ref oracle read null (opencode lane). Skip convnote messages
in both filters, same transport-metadata treatment as the #1421 chain
carrier. Reproduced locally: pi suite 3/4 -> 4/4, opencode (V1) 4/4.
Rebased onto master, which added tests/fold-round2-shape.test.ts
(febbc16) after this branch point. That test's divergence model counts
the trailing volatile tail as three slots (nudge / chain stamp / new
user turn). #1611 appends one more ephemeral tail — the conversation-id
note — so steady bodies end [..., prompt, convnote, chain] and the old
'-3' boundary classified the first growth pair as a mid-history rewrite
('pair 0->1 diverges at element #3/7'). Bumped to '-4' with the slot
list updated; verified all pairs pass incl. both fold round-2s.
@ranxianglei
ranxianglei force-pushed the 2026-09-29_conv-id-trailing-prefix-cache branch from b8e0a57 to 3586673 Compare September 28, 2026 18:31
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Review of PR #1620 (fix for #1611)

① Issue triage — valid, root cause correctly targeted. #1611's premise checks out: the live per-conversation id sat in the static system line, so any fork/resume/session-switch (new canonical id, byte-identical history, #1486) diverged at exactly one system line → provider prefix cache missed entirely (cached_tokens=0, quantified in #1592's S2 fork evidence). The fix removes the producer of the divergence rather than masking it: system now carries a byte-stable pointer const (CONVERSATION_ID_POINTER_NOTE, src/compress-tool.ts), and the live value rides an ephemeral trailing user message under the same gate the id-in-system used. MCP routing is untouched — the model still reads the value from context and echoes it as conversation_id (src/mcp.ts). I also verified the one subtle path: the compress-loop re-request deliberately drops the trailing note (keeps only the constant pointer) — safe, because conversation_id is consumed exclusively by the MCP shim, never by the server-side loop.

② Regression ledger (existing behaviors on touched paths):

③ CI red on the original head — root-caused and fixed (2 commits pushed).

  • real pi package-native lane failed with compress must have been called and its result re-sent; real opencode native lane with filler message must carry its ACP ref tag, got null. All ~9 prior runs of both workflows (other branches off current master) were green → caused by this PR, not flake. Root cause: tests/e2e/fake-upstream-chat.mjs scripts the model off the last user message (directive queue source) and reads lastUserRef from it — the new convnote tail lands after the client's real prompt and masked it: the scripted compress directive never fired (pi) and the filler-ref oracle read null (opencode). Fix (commit efdca14d): skip convnote messages in both filters, same transport-metadata treatment as the [design] chain checkpoint step 3: carrier stamp shape on role-alternation-strict wires (anthropic/google) #1421 chain carrier. Reproduced locally first: pi suite 3/4 → 4/4, opencode (V1 lane, host 1.14.46) 4/4.
  • The branch was 13 commits behind master; I rebased onto current master. That surfaced a semantic collision the pre-rebase run couldn't see: master added tests/fold-round2-shape.test.ts (dsh 缓存失效 #1592, febbc162) after this branch point, whose divergence model hardcodes a 3-slot volatile tail (nudge / chain stamp / new turn). conversation-id line in the static system prompt breaks the upstream prefix cache on fork/resume/session-switch (cached≈0 despite byte-identical history) #1611 adds a 4th, so steady bodies end […, prompt, convnote, chain] and the first growth pair was misclassified as a mid-history rewrite (pair 0->1 diverges at element #3/7 — verified against raw bodies on both trees: master passes at exactly the idxB >= count-3 boundary; the branch needs -4). Fix (commit 35866736): constant widened to 4 with the slot list documented; all pairs pass incl. both fold round-2s.

Pre-flight on the final head 35866736: typecheck clean · 3139 unit tests / 0 fail · build OK · pi e2e 4/4 · opencode e2e 4/4 (V1 locally; V2 2.0.3 lane runs in CI). Local dsh lane not executed here (sandbox dsh 0.1.1-rc.2 vs CI pin 0.1.5-rc.3 — profile-boot mismatch unrelated to this diff); the dsh run on the previous head was green and the fake change only restores pre-#1611 last-user semantics for it. CI re-runs were triggered by the push.

Verdict: direction correct, root cause (not symptom), wire-shape change properly flagged, tests have teeth. Per §7.4 this stays a human-merge (wire/message-shape change affecting both modes). Ready to merge once the triggered CI suite is green.

中文摘要:修复了 #1611「会话 id 写进静态 system 导致 fork/resume 后前缀缓存全 miss」的问题(id 值改为随临时尾部消息下发,system 只留常量指针);我定位并修掉了它引起的两处 CI 红(e2e fake 的"最后一条用户消息"语义被新尾注遮蔽、以及 rebase 后与 master 新增 #1592 折叠形状测试的 3→4 尾槽冲突),已补两个测试修复 commit 并 rebase 到最新 master,本地全套预检通过,等 CI 绿后可合并。

@ranxianglei

Copy link
Copy Markdown
Owner Author

Superseded by #1685 / #1692 (zero-injection: the tail note this PR adds is exactly what the approved design rules out). Suggest closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant