Skip to content

[PR] merge: tier-2 consolidation train — 15 PRs + repair of #4525's sidScope compile break - #4551

Merged
ranxianglei merged 26 commits into
masterfrom
2026-10-24_tier2-consolidation-train
Oct 24, 2026
Merged

ranxianglei merged 26 commits into
masterfrom
2026-10-24_tier2-consolidation-train

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

这是什么

第 2 级合并列车(#4525 的续集):15 个绿票一次落位。合并方式请选 Merge commit。

⚠️ 本列车包含一个 master 紧急修复

#4525 遗留了一个编译错误:src/agent/dsh-native.ts 引用的 sidScope 只存在于 #4424 分支(#4223 与它堆叠)。本列车的 #4424 修复了这一点——建议尽快合并以恢复 master 可编译状态。(列车1 的教训:typecheck 只看 tail 输出没验 exit code,已在本列车修正流程。)

车上清单(15 个 PR)

PR 修的 issue 作者 落位
#3683 孤儿(SSE framing 预算) WSXYT 干净
#3973 Refs #3900(headroom 完成份额) Zhishu 干净(4 commits)
#4141 Closes #4131(跨构建 id churn 收养) ranxianglei import 并集
#4257 Closes #4254(上游失败可诊断) ranxianglei 与 #3996 codex 豁免并集
#4267 Closes #4254(shell.overlay 药丸) ranxianglei 与 #4237 门并集,测试预期更新
#4308 Closes #4305(代际边界 standing note) ranxianglei 7 处与 MCP-dialect 链并集
#4424 修复 #4525 的 sidScope 编译错 ranxianglei verdict API + scoped 回退嫁接,测试 5/5
#4389 Refs #4125(give-up 事件具名) ranxianglei 干净
#4453 Closes #4438(auto-review 侧请求出主会话,p1) ranxianglei 干净(3 commits)
#4455 Closes #4452(网关改写溢出饥饿日志) ranxianglei 并集;携带 #4383 cycle-stale 基座(b10cf4f,测试 8/8)
#4459 Closes #4458(acp-status 压缩着陆标记) ranxianglei 干净
#4461 Closes #3693(面板可复制错误) ranxianglei i18n 并集
#4483 Closes #4482(dump 凭据警告横幅) ranxianglei 并集
#4494 Closes #4488(尾帧 usage 收割) ranxianglei 干净
#4496 Closes #4124(图片数量上限学习) ranxianglei 双 else-if 并集

出车与延后(诚实记录,5 个)

PR 原因
#4280(车上已 revert) 与 master #4505 的 fetch 链路由真语义冲突——织入后它自己的 2 个测试红(直连判定/链安装),证明硬织不通,需在现 master 上重做
#4443 stale-small 锚与 #4383 堆叠家族(cycle-stale 基座在本车)互锁;union 出现双次重算行为漂移(其测试暴露),延后重做
#4413 同一堆叠家族的第三层,基座已上车
#4430 与列车1 #4053 同一 capture 区域的重构,需仔细读两侧管线后合并
#4445 同类重叠,未深试,诚实延后

另:#4534、#4211 目标 issue 已关,建议直接关单。

验证

  • typecheck 干净(exit 0);零 kernel/ 碰撞
  • 定向测试全部等于或优于 master 本地基线(dsh-settings 18/7=基线 7 环境红、opencode-v2 29/3=基线 3、invariant 40/0、llm-stream 5/0、4383-cycle 8/0、使用pi 压缩后就提示prompt is too long #1195 4/0、4204 10/0)
  • 全量以 CI 为准

关单

Closes #4131, Closes #4254, Closes #4305, Closes #4438, Closes #4452, Closes #4458, Closes #3693, Closes #4482, Closes #4488, Closes #4124
Refs #4125, Refs #3900

外部贡献者署名(merge commit 保留作者)

Co-authored-by: WSXYT ws675170761@gmail.com
Co-authored-by: Zhishu 963336185@qq.com

WSXYT and others added 25 commits October 24, 2026 20:31
…lane stops re-overflowing (#3900)

An anthropic-wire upstream can still bill max_tokens inside the context wall:
DeepSeek's /anthropic endpoint answers a 1,048,576-token request with
"requested 1370540 (1114540 in the messages, 256000 in the completion)", while
shouldReserveOutputHeadroom() exempts the anthropic protocol, so the preflight
judged those messages against the full window.

#3450 already learns the stated completion share from that rejection, but it
books it on the one-shot overflow arm, and the next real usage report retires
the arm and the share together (#1110). The recovered session then judges on
the declared window again, over-fills, and re-earns the same 400 — each
recovery paying a fresh multi-range emergency fold during which the client
sees only a timer.

Keep the proof on its own lifetime: a session-level provider fact scoped to the
upstream origin and model that stated it. armOverflowShrink() stamps it; the
output-headroom step reserves it on wires that do not reserve output budget
themselves, so the nudge/preflight denominator becomes window - proven share.
A route or model change, or an absent/zero proof, falls back to the untouched
protocol rule; the one-shot arm and its #1110 retirement are unchanged.

Tests: tests/e2e-proven-completion-wall.test.ts proves the share survives the
usage report, that the next turn judges on window - completion, and that a
foreign origin cannot claim it; context-overflow.test.ts covers scoping and
corrupt walls.

(cherry picked from commit 62289ef)
After an overflow proves a completion share against the stated wall, handle.ts rewrites effectiveContextLimit to window - share, so the #3738 arm clamp now lands at min(stated messages, proven window). Pin the new number and keep the stated ceiling assertion so the rule, not the width, is what the case checks.

(cherry picked from commit e6bf8c0)
#4205 split the display window (effectiveContextLimit, the declared wall)
from the judged one (reservedContextLimit). The proven-share clamp acts on
the judged window, so the e2e assertions must read reservedContextLimit;
effectiveContextLimit stays the declared 40000 and is asserted as such.

(cherry picked from commit da1c3ac)
… 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.

(cherry picked from commit 4cb446b)
The client renders the `error` string, but bili answered every proxy failure
with the constant "acp-proxy failure" while the real cause already sat in
detail / upstreamFailure / alerts. Changes:

- buildProxyFailureResponse() shared by both egress sites (startServer dispatch
  catch + inline engine): `error` is now informative ("acp-proxy failure: <kind>
  [<code>] [host[:port]]", legacy prefix kept, stays a string); a round-level
  circuit-breaker fail-fast answers 503 + Retry-After (next-probe interval)
  instead of a generic 502. Initial transport failures stay 502; overflow 413.
- structured upstreamFailure gains host/circuitOpen/retryAfterSeconds (additive,
  byte-identical when absent); the inline lane now emits it too (was omitted).
- circuit-open fail-fast logs throttled to one [warn]/window/host; the per-request
  [error]+stack is suppressed via an expected-fail-fast marker (stops the storm).
- free-text circuit reason masks the host through the same rule as the embedded
  last-failure detail (non-public -> <private-host>), ending the raw-next-to-masked
  contradiction; the actionable endpoint rides structured upstreamFailure.host
  (loopback-exempt). Free-text masking otherwise unchanged (#255 intact).
- circuit last-failure detail truncates at a whitespace boundary, not mid-token.

Addresses #4254 (defects 1,3,4,5,6; defect 2 pending
a design decision tracked in-thread). Regression: tests/issue4254-*.test.ts.

(cherry picked from commit 670f4a0)
Defect 2 render half of #4254, pairing with the GET /__bili/upstreams read surface shipped in #4257. bili's DSH client registers a frame-level shell.overlay slot that polls /__bili/upstreams every 10s and renders one row per unreachable upstream (host:port, kind, errno code, consecutive-failure tally, and a retry ETA while the circuit is open); it returns null while every upstream is reachable, so the banner auto-dismisses when circuits close + alerts clear.

- Display identity is the forwarded host:port (bili is a transparent pipe); provider/model reverse lookup stays host-side (DSH reads its own baseURL).

- Defensive parse of the report never throws: a broken health report cannot break the DSH client.

- Purely additive UI, no request-path changes; deferred injection is fail-open on hosts whose build lacks the shell.overlay slot.

- i18n zh/en in sync; deterministic render test (populated / healthy / multi-host).

(cherry picked from commit dce1538)
…ute model traffic at fetch time (#4276)

The V2 opencode entries (native + launcher) rewrote event.request.url in place inside their unscoped http.request hook. opencode hands that one mutable event to every matching hook — provider-ID-scoped plugins included — so URL-validating provider plugins saw the proxied <origin>/bili/<upstream> URL instead of the real upstream: Kiro's AWS bridge silently never engaged (empty assistant replies), Antigravity's request hook threw on the non-canonical origin.

Root fix: the routes keep only discovery/degrade (resolveLive -> proxyBase, oversize local refusal); both setup()s now install the shared native fetch intercept chain which performs the rewrite at fetch time, leaving every hook-visible payload untouched. The WS handshake hook builds its tunnel itself (byte-identical output for formerly-working shapes).

Also honors providers.<url>.direct in these lanes (previously ZCode-only): the hook marks exempt requests (no stamping/incremental/ws-tunnel) and the chain sends them direct before any proxy resolution. No new config surface — completes the documented field in the opencode lanes.

Behavior changes: provider plugins now see original upstream URLs (the fix); signed-body model requests in the V2 lane get the local #1884 refusal instead of unsigned-through-bili -> upstream 401; #3304 prepareDirectSend + #3248 onResponse acks activate in V2 processes (were dead code); launcher dead-origin handling moves from preemptive unwrap to chain recover/retry/give-up-direct.

Closes #4276

(cherry picked from commit 5602c07)
…oad — route model traffic at fetch time (#4276)"

This reverts commit 05b7dfd.
…ef residue

After a mid-conversation model/upstream switch the ref ledger restarts at
m00001 while the client-persisted history keeps citing previous-generation
refs, which look exactly like live ones — the model keeps aiming compress at
dead ranges until the circuit breaker fires. The reactive receipts (#1029,
flails through them, so the fix is a STANDING part of the context.

One-shot evidence-based detection (>=2 distinct above-frontier citations in
visible history), verdict persisted in session metadata, static number-free
note appended to the ACP standing system block on every wire plus the
round-2 fold rebuild in exact steady-path order. Display-only: no storage,
ref-space, gate or breaker changes.

(cherry picked from commit ae1f0f0)
The 'proxy respawn gave up' dsh client event was a single bare sentence
with no branch information, so users hitting hours-long direct-send
episodes could not self-diagnose which failure mode they were in.

- native-intercept: the two send-failure recovery paths now record
  lastUnavailableReason='send-failed' immediately before firing onGiveUp
  (the resolver paths already recorded dead/busy-exhausted/bootstrap-
  failed); UnavailableReason gains the member. Consumers compare only
  against 'busy-exhausted', so the new member is inert elsewhere.
- dsh-native: the three duplicated onGiveUp closures collapse into one
  fireGiveUp seam that appends ': <reason>' to the persisted line when
  the interceptor recorded one, keeping the original bare wording when
  none is present. _resetRegisterForTest clears the reason between
  scenarios; _nativeStateForTest exposes the shared state to suites.
- launcher: killAbandonedChild no longer swallows win32 taskkill
  failures or a failing final SIGKILL — both now log a warn naming the
  pid, since a surviving listener is exactly the confusion #4125's
  netstat showed.

Behavior change (intended): the give-up line gains a ': <reason>' suffix
(dead | busy-exhausted | bootstrap-failed | send-failed) whenever the
interceptor recorded why nothing was routable; hosts parsing the exact
old string will see the longer form. No wire, config, persistence or
kernel changes.

(cherry picked from commit b3d8ee7)
… checkpoint-framing quotes from triggering rebases

Incident (v0.1.204, dsh web profile + Cherry Studio gateway): an UNSTAMPED
dsh auto-review request (fixed REVIEW_POLICY system + one freshly-flattened
history blob, no tools) arrived on a session already sticky-bound to dsh by
earlier main turns. Every isolation gate keyed on the LIVE x-bili-plugin
header missed it, so it rode the raw main session key through the full
pipeline: its blob quoted an old compaction checkpoint mid-text, which
false-positived the #2432 native-compaction rebase (0/112 ids retained ->
total refs/blocks/fold-state wipe), and its tiny usage sample clobbered the
usage-grade baseline the #453 output clamp anchors on (676541 -> 127497) —
max_tokens stayed 384000 against real inputs past the window edge, producing
permanent upstream 400s.

- side-request.ts: new isDshAutoReviewRequest() shape detector (fixed
  REVIEW_POLICY system prefix + exactly one user entry + no tools),
  version-pinned like DSH_TITLE_SYSTEM_PREFIX (#2503).
- handle.ts: review shape joins sideRequestLike and rides the #388 stateless
  side passthrough on every wire — no refs/blocks, no rebase detection, no
  usage settle (relay useRewriter gate keeps side responses off the ledger).
  Gate is asymmetric by design: identity side requires the LIVE dsh stamp
  (unstamped reviews already key verbatim there); lane side accepts dsh
  evidence = live stamp OR sticky session binding — the sticky leg is what
  catches the incident's unstamped extension traffic.
- dsh-compaction-guard.ts: carriesDshLocalCompactionSummary now requires the
  standalone-checkpoint shape (message STARTS with the preamble or the open
  tag); framing QUOTED mid-message no longer counts — per the file's
  version-pin doctrine, template drift degrades to a miss, never a false
  positive that wipes a live session.

Behavior change (disclosed): dsh auto-review requests that used to fork onto
persisted |sub:<fp> sessions (#1916/#2203) now ride the stateless passthrough
regardless of stamping. The forked lane had no functional value (each review
re-serializes the whole blob, so ref ids churn and nothing ever folds) and
#3681 showed it overflowing its own window.

Tests: new tests/issue4438-dsh-review-lane-isolation.test.ts (unit both
directions + e2e incident repro: unstamped review on a sticky-bound folded
session leaves refs/boundary/baseline intact and forwards byte-exact);
tests/issue2432-dsh-local-compaction.test.ts carrier pins flipped to the
standalone contract; tests/dsh-persona-fingerprint.test.ts rewritten from the
superseded fork contract to the stateless-passthrough contract on all four
wires; side-lane-invariants allowlist extended with justification.

Fixes #4438

(cherry picked from commit bfe25bf)
…passthrough

tests/dsh-persona-continuity.test.ts still pinned the superseded fork behavior (review request mints a |sub: session) that this PR replaces: the openai-lane e2e now asserts no fork minted, main session requests/refs untouched, 200 verbatim forward. The gated real-dsh twin (e2e-native-dsh-persona scenario B) flips its acceptance from forks>=1 to zero forks for the same reason. Resolver-level unit blocks are unchanged by design: the resolver itself still forks a discontinuous shape — only the wiring keeps auto-review blobs off it.

(cherry picked from commit b306aa6)
Cosmetic follow-up on b306aa6: six header lines carried a stray leading
space, inconsistent with the rest of the file's column-0 // block. No
content change.

(cherry picked from commit a552b0c)
…cit per-session marker (#4458)

Add a HOST-NATIVE COMPACTION section to acp_status, shown per-session when a
host-native compaction landing is recorded (session.metadata.nativeCompactionBoundary),
in BOTH compression modes. It distinguishes "the host compacted this conversation"
from "bili folded it", shows pending-rebase vs rebased state, and names the resolved
client/lane. Proactive visibility ahead of the reactive DEAD REFS / breaker /
conflict-watch surfaces, so an operator or agent can see a landing BEFORE a follow-up
call burns a ref minted before the boundary.

Additive display-only output: absent when no landing was recorded; no wire, config,
cache, or identity impact. Regression test covers both gate directions (#3617).

(cherry picked from commit 3676ee6)
…#4452)

An intermediate gateway that rewrites every reject into one generic body
(Cherry Studio et al.) strips the token counts / window from each overflow
400, so inspectContextOverflow returns isOverflow:false and bili's whole
overflow self-heal silently never engages — the route starves of calibration
and re-sends the same oversized payload forever.

Decision-free observability slice of #4452 (behavioral recovery awaits owner
sign-off on thresholds):
- add isMarkerlessOverflowReject() (util.ts): a 4xx overflow-shaped reject
  whose body matches no CONTEXT_OVERFLOW_PATTERNS (excludes 5xx, which already
  self-heal via the local estimate, and any body that carries a marker).
- the relay logs a loud warn naming the starvation + the last meter reading
  (settled input vs effective window) instead of only the opaque
  `<- upstream 400` line.

Purely additive logging — no behavior change when isOverflow is true, no new
config surface.

(cherry picked from commit 6906dfa)
…eadout

Closes #3693

A failing embed-route probe or Context-tab status poll was swallowed
silently: the settings panel and the Context tab spun on the connecting
hint forever with zero evidence of what actually failed. Remote-access
users (page served from a LAN IP while the panel binds to server
loopback) could neither see nor copy the underlying network error.

Every failure state now renders the raw engine message plus the failed
URL in a selectable block with an explicit copy action
(navigator.clipboard.writeText with an execCommand fallback, since
LAN-http pages are not secure contexts). Probe errors are transient by
design: never cached, retried on every poll tick until a verdict lands.

(cherry picked from commit a8acce2)
…on-empty with an empty delta (#4488)

(cherry picked from commit 908e59c)
…4482)

Opt-in body dumps (ACP_DUMP_BODY / diagnostics.dumpBody, REREQUEST dumps,
BILI_DUMP_4XX err/summary-err dumps) write the full plaintext conversation
body to disk and may carry real credentials (tool arguments, host-injected
content), yet nothing warned the user. #4397's reporter saw live credentials
in such dumps; sharing them into an issue leaks them publicly.

No automatic redaction — byte-exactness is the point of these dumps. Instead
every plaintext-body dump file now starts with one standing warning line
(PLAINTEXT DUMP ... scrub before sharing):

- raw/*-REQ.txt, raw/*-INCOMING.txt, dumps/req-*.json, req-*-REREQUEST.json
  and err-*.json / summary-err-*.json all get the banner (DUMP_PLAINTEXT_BANNER
  in src/dump-banner.ts).
- bili acp-cache diff strips the known leading banner line explicitly, so
  pre-banner (old-format) dumps keep parsing byte-identically — old and new
  files also mix fine in one dir (tested). The banner text must stay free of
  ": " (head lines parse as headers); pinned by test.
- tools/analyze-dumps.mjs strips the banner before JSON.parse; its local copy
  of the literal is checked against src/dump-banner.ts by test.
- CONFIGURATION.md en+zh warnings for dumpBody/dumpReq/dump4xx go through the
  gen-config-docs seed (website/config-reference/diagnostics.yaml), keeping
  the docs-sync drift guard green.

Behavior change (intended, disclosed per owner policy): dump file FORMAT
changes for all six writers — a one-line banner is prepended. Consumers that
strictly parsed dumps/req-*.json or err-*.json as pure JSON from outside this
repo will see a non-JSON first line; in-repo consumers are updated. Body bytes
after line one are untouched (byte-diff forensics preserved).

Tests: new tests/issue4482-dump-banner.test.ts (banner pinned + tool sync,
stripDumpBanner unit, real-writer harness asserting INCOMING/REQ/JSON banners
with intact bodies, runDiff old/new/mixed-dir equivalence) plus banner
assertions added to error-dump / issue1993 / reasoning-echo suites.

(cherry picked from commit e354285)
…d images — #4124

Upstreams with a hard per-prompt image-count wall (e.g. vLLM
--limit-mm-per-prompt) reject the whole request ("Too many images in
request: 165 > 30"); bili forwarded that 400 verbatim every round and
the session dead-ended. The reject vocabulary only knew token/context
shapes, so the error was not even classified as a capacity rejection.

- inspectContextOverflow detects image-count rejections on an
  independent channel (imageCountCap/imageCountSeen); isOverflow keeps
  its token-only meaning so no token-space arm can be minted from a
  count wall (parseOverflowWindow/toTokenNumber would reject <1000).
- shedWireImagesToCount: count-space twin of shedWireImages (rung A
  only — downscaling cannot reduce count; tail yields to the hard cap;
  no MAX_SHED_IMAGES slice) converging the outbound copy to cap-1.
  Originals stay in session storage behind stable placeholder refs
  (decompress({imageRef}) recovers them).
- Relay: same-request rescue (converge + re-send once; the client sees
  success), the learned cap persists per-origin in
  stats.imageCountCaps, later requests to the same route converge
  pre-send (no rejected round-trip), and the pck A/B probe skips
  content-wall 400s instead of white-running a key-less ablation that
  fails identically.

(cherry picked from commit 632b28a)
@ranxianglei ranxianglei changed the title merge: tier-2 consolidation train — 15 PRs + repair of #4525's sidScope compile break [PR] merge: tier-2 consolidation train — 15 PRs + repair of #4525's sidScope compile break Oct 24, 2026
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-24_tier2-consolidation-train (afd6287)

Latest build — install this exact version

npm install -g billion-context@0.0.5499-pr.4551.e44a70d

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

Recent builds (commit → version)

commit version build
afd6287 billion-context@0.0.5499-pr.4551.e44a70d build
bd38d15 billion-context@0.0.5492-pr.4551.f820dc3 build

Download artifact

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

Install instructions are refreshed on each push.

…#4308 follow-up) + register BILI_CODEX_STATE_MODE in LIFECYCLE.md (#3321 follow-up, also red on master)
@ranxianglei
ranxianglei merged commit b5a4f60 into master Oct 24, 2026
44 of 54 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

📦 Merged Build Published

This merge to master was published as 0.0.819-master.b5a4f60 (rolling master tag).

npm install -g billion-context@0.0.819-master.b5a4f60  # this exact build
npm install -g billion-context@master  # always the newest merged build

npm view billion-context@master prints the current version at any time.

This was referenced Oct 24, 2026
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.

Switching model mid-conversation restarts the ref ledger, but the rendered transcript still shows the old refs — the model keeps aiming compress at dead ranges until the circuit breaker fires | 切模型后消息编号整体换账本,但渲染历史仍显示旧编号,模型对着死区间反复压缩直至熔断 Circuit-open 502 keeps an opaque error string, and connect-refused alerts never reach the host GUI | [Bug][dsh] 熔断/连接失败时客户端只见常量 "acp-proxy failure":detail/upstreamFailure/alerts 全都没到用户眼前 [Bug][opencode lane] Cross-version restart replays the whole session: old v0.1.197 and new v0.1.202 write the same sessions dir, 212 folded refs re-anchor onto churned bytes and the ref map is pruned to 392 raw ids No per-prompt image count cap: upstream "Too many images in request: 165 > 30" is forwarded as-is | 缺少按 prompt 的图片张数上限:上游 "Too many images in request: 165 > 30" 被原样转发 dsh remote access needs to display error messages. | dsh远程访问需要提示报错内容

4 participants