You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
perf: avoid transform work that scales with compression history #384
Problem\n\nOpenCode becomes slow in long ACP sessions, especially after many compressions. A local isolated probe measured adaptive candidate planning at approximately 3.2 ms for 100 messages, 34 ms for 500, and 105 ms for 1,000.\n\n## Root causes\n\n- Candidate validation rebuilds global boundary and active-summary lookup structures for each draft.\n- T1 nudge analysis calculates full context/range data even when no nudge can be emitted.\n- Stable transforms replay historical inactive compression blocks and rebuild immutable consumed-call indexes.\n- Asynchronous state saves need ordered coalescing to prevent redundant writes and stale overwrites.\n\n## Proposed fix\n\nUse request-scoped search indexes, lazy no-nudge analysis, verified-state synchronization, transient derived indexes, and ordered state-save coalescing. Preserve candidate executor validation, protection semantics, decompression, fork recovery, and persisted-state compatibility.\n\n## Acceptance\n\nTarget candidate planning at 1,000 messages of 20 ms or less, with benchmark evidence and full regression coverage.
[bot] 🏷 Picking this up. Plan before I touch code:
Verify each claimed hotspot with an isolated benchmark (candidate planning at 100/500/1,000 messages) so the fix targets measured costs, not guesses — I'll report baseline numbers here.
Triage the four root causes against the code paths (resolveBoundaryIds/buildBoundaryLookup per-draft rebuilds, T1 nudge analysis, per-transform block replay, fire-and-forget saveSessionState races) and confirm which are real and which are symptoms of a deeper issue.
Implement per your proposed direction (request-scoped search indexes, lazy no-nudge analysis, verified-state sync, transient derived indexes, ordered save coalescing), preserving executor validation, protection semantics, decompression, fork recovery, and persisted-state compatibility.
Ship with benchmark evidence (target: ≤20 ms candidate planning at 1K messages) + regression coverage + devlog, as a PR.
I'll post verification results as soon as the baseline benchmarks are in.
What was verified first (all four root causes confirmed in code before touching anything):
resolveBoundaryIds() rebuilt the global boundary lookup (O(entire ref history)) per candidate draft — the dominant cost
resolveSelection() ran the real Anthropic BPE tokenizer per message (~27 ms/KB measured in micro-bench)
T1 nudge analysis (composition / protected refs / compressible ranges) ran every transform even when no nudge could fire
sync replayed all blocks (active + inactive) every transform; hide-consumed rebuilt immutable indexes every transform; saves were independent fire-and-forget whole-file writes (stale-overwrite possible)
Fixes: request-scoped boundary-lookup memo · chars/4 fast estimate in selection · needsNudgeAnalysis gate with null guards · transient structureVersion invalidation (bumped at exactly the 3 block-mutation sites, full mutation-site audit) · ordered coalescing per-session save queue (latest-snapshot-wins bursts, FIFO across batches, failure isolation). Persisted-state format unchanged; protection semantics, decompression, and fork recovery untouched.
Benchmark evidence (scripts/bench-candidate-planning.ts, new harness, median of 7 reps; baseline = pristine master run same day):
msgs
A baseline
A fixed
B+C steady-state
100
1608.6 ms
0.40 ms
0.10 → 0.04 ms
500
6798.7 ms
1.09 ms
0.58 → 0.20 ms
1000
13618.0 ms
1.16 ms (~11,700×)
0.76 → 0.14 ms
Acceptance target ≤ 20 ms @ 1000 met with ~17× headroom. Full suite: 1151 tests, 0 failures (+20 new tests covering each regression mode, incl. §5.7 multi-turn production-config nudge cycle). Dual-agent review complete: both reviewers APPROVE-WITH-NITS, zero blocking findings; nits addressed in a follow-up commit.
Merge is a human-only operation — please merge when you're satisfied: #385
[bot] 🏷 Update: PR #385 rebased onto current master (v1.17.0) after two releases landed while it was in flight. One trivial import conflict (lib/gc/merge.ts) and one test-file append collision (tests/inject.test.ts — both sides' tests kept intact, verified by running the merged file). Re-verified on the rebased tree: typecheck clean, 1227/0 tests (includes v1.17.0's new suite), benchmark A = 1.81 ms @ 1000 messages (target ≤ 20 ms). All three CI checks now green and the PR reports mergeable: clean — ready for you to merge whenever: #385
Triage: the profiled code paths are not in released ACP
Audited against master (v1.17.0):
"adaptive candidate planning" / "candidate validation rebuilds global boundary and active-summary lookup structures for each draft" — master has no adaptive-candidate planner. The planner (reason=no_executable_candidates, per-draft candidate validation) exists only on the unmerged #341 / 2026-07-09_more-compression-candidates branch. The 3.2ms@100 / 34ms@500 / 105ms@1000 numbers therefore profile a build carrying that patch — same as the fix: recover compression blocks when forks omit historical compress inputs #375 report (the reporter's build emits that log line too).
If you can reproduce on released v1.17.0 (npm i -g opencode-acp@latest equivalent): please attach the reproduction (session length, /acp stats timing, CPU profile) and we'll treat it as a master bug.
The remaining sub-points (T1 nudge analysis cost, async state-save coalescing) do exist on master but are O(messages) per turn and debounced respectively — no quadratic behavior observed in master's paths. Happy to re-measure with evidence from a stock build.
[bot] 🏷 Thanks for the audit — you're right about the planner, but the hot path being fixed is in released master. Evidence:
Where you're right: master has no adaptive-candidate planner (no_executable_candidates appears nowhere in master's lib/) — that's #341-only, so the issue's original 3.2/34/105 ms log numbers did come from a #341-carrying build (consistent with #375).
Where it differs: the per-draft lookup rebuild isn't part of the planner — it's in the core compress pipeline that every released build runs:
lib/compress/search.ts:60 — resolveBoundaryIds() calls buildBoundaryLookup(context, state)on every call, rebuilding the global boundary + active-summary index over the entire ref history
lib/compress/range-utils.ts:64 — resolveRanges() invokes it once per range entry in a compress tool call
lib/compress/search.ts:328 — resolveSelection() runs the real BPE tokenizer (countAllMessageTokens) per message during selection
No #341 code involved — the harness imports only those core files.
Repro on stock v1.17.0 (as requested): fresh checkout of current master (85c6382, package.json 1.17.0), PR's harness scripts/bench-candidate-planning.ts, 10 range entries per compress call, median of 7 reps:
msgs
A: planning (stock)
A: planning (PR branch)
100
1797 ms
0.4 ms
500
7831 ms
1.09 ms
1000
15250 ms
1.81 ms
So on stock v1.17.0 a single multi-range compress call in a long session costs ~15 s of CPU — superlinear in history size. That's the "slow after many compressions" symptom from the issue title, reproducible without #341.
On the other two sub-points (conceding framing): T1 nudge analysis is O(messages) per transform, not quadratic — agreed; on stock master it runs unconditionally every LLM transform (inject.ts:371/380/384) even when no nudge can fire, so the gate removes wasted linear work rather than fixing a blowup. Save coalescing is likewise about redundancy/correctness (independent full-file writes per trigger, no ordering guarantee → stale-overwrite risk), not speed scaling.
Suggestion: RC1/RC2 fix shared core code (search.ts, token-utils.ts) used by both master's resolveRanges and #341's planner — landing them on master makes #341 strictly faster when it eventually merges. If you'd rather keep the candidate-planning pieces out of this PR, I can split them into a follow-up scoped to #341; say the word and I'll restructure. Otherwise #385 stands as-is with the stock-build repro above.
Activity
ranxianglei commentedon Sep 11, 2026
[bot] 🏷 Picking this up. Plan before I touch code:
resolveBoundaryIds/buildBoundaryLookupper-draft rebuilds, T1 nudge analysis, per-transform block replay, fire-and-forgetsaveSessionStateraces) and confirm which are real and which are symptoms of a deeper issue.I'll post verification results as soon as the baseline benchmarks are in.
ranxianglei commentedon Sep 11, 2026
[bot] 🏷 Done — PR ready for review: #385
What was verified first (all four root causes confirmed in code before touching anything):
resolveBoundaryIds()rebuilt the global boundary lookup (O(entire ref history)) per candidate draft — the dominant costresolveSelection()ran the real Anthropic BPE tokenizer per message (~27 ms/KB measured in micro-bench)Fixes: request-scoped boundary-lookup memo · chars/4 fast estimate in selection ·
needsNudgeAnalysisgate with null guards · transientstructureVersioninvalidation (bumped at exactly the 3 block-mutation sites, full mutation-site audit) · ordered coalescing per-session save queue (latest-snapshot-wins bursts, FIFO across batches, failure isolation). Persisted-state format unchanged; protection semantics, decompression, and fork recovery untouched.Benchmark evidence (
scripts/bench-candidate-planning.ts, new harness, median of 7 reps; baseline = pristine master run same day):Acceptance target ≤ 20 ms @ 1000 met with ~17× headroom. Full suite: 1151 tests, 0 failures (+20 new tests covering each regression mode, incl. §5.7 multi-turn production-config nudge cycle). Dual-agent review complete: both reviewers APPROVE-WITH-NITS, zero blocking findings; nits addressed in a follow-up commit.
Merge is a human-only operation — please merge when you're satisfied: #385
中文摘要:修复了长会话中随压缩历史增长的每次 transform 开销(候选规划 1000 条消息从 13.6 s 降到 1.16 ms,达标 ≤20 ms),持久化格式不变,1151 个测试全过,双代理审查无阻断问题,PR #385 可以合并。
perf: avoid transform work that scales with compression history (#384)
docs: address dual-review nits for #384
docs: address dual-review nits for #384
perf: avoid transform work that scales with compression history (#384)
docs: address dual-review nits for #384
docs: address dual-review nits for #384
ranxianglei commentedon Sep 11, 2026
[bot] 🏷 Update: PR #385 rebased onto current master (v1.17.0) after two releases landed while it was in flight. One trivial import conflict (
lib/gc/merge.ts) and one test-file append collision (tests/inject.test.ts— both sides' tests kept intact, verified by running the merged file). Re-verified on the rebased tree: typecheck clean, 1227/0 tests (includes v1.17.0's new suite), benchmark A = 1.81 ms @ 1000 messages (target ≤ 20 ms). All three CI checks now green and the PR reportsmergeable: clean— ready for you to merge whenever: #385ranxianglei commentedon Sep 11, 2026
Triage: the profiled code paths are not in released ACP
Audited against
master(v1.17.0):reason=no_executable_candidates, per-draft candidate validation) exists only on the unmerged #341 /2026-07-09_more-compression-candidatesbranch. The 3.2ms@100 / 34ms@500 / 105ms@1000 numbers therefore profile a build carrying that patch — same as the fix: recover compression blocks when forks omit historical compress inputs #375 report (the reporter's build emits that log line too).What this means:
npm i -g opencode-acp@latestequivalent): please attach the reproduction (session length,/acp statstiming, CPU profile) and we'll treat it as a master bug.The remaining sub-points (T1 nudge analysis cost, async state-save coalescing) do exist on master but are O(messages) per turn and debounced respectively — no quadratic behavior observed in master's paths. Happy to re-measure with evidence from a stock build.
ranxianglei commentedon Sep 11, 2026
[bot] 🏷 Thanks for the audit — you're right about the planner, but the hot path being fixed is in released master. Evidence:
Where you're right: master has no adaptive-candidate planner (
no_executable_candidatesappears nowhere in master'slib/) — that's #341-only, so the issue's original 3.2/34/105 ms log numbers did come from a #341-carrying build (consistent with #375).Where it differs: the per-draft lookup rebuild isn't part of the planner — it's in the core compress pipeline that every released build runs:
lib/compress/search.ts:60—resolveBoundaryIds()callsbuildBoundaryLookup(context, state)on every call, rebuilding the global boundary + active-summary index over the entire ref historylib/compress/range-utils.ts:64—resolveRanges()invokes it once per range entry in a compress tool calllib/compress/search.ts:328—resolveSelection()runs the real BPE tokenizer (countAllMessageTokens) per message during selectionNo #341 code involved — the harness imports only those core files.
Repro on stock v1.17.0 (as requested): fresh checkout of current master (
85c6382, package.json 1.17.0), PR's harnessscripts/bench-candidate-planning.ts, 10 range entries per compress call, median of 7 reps:So on stock v1.17.0 a single multi-range compress call in a long session costs ~15 s of CPU — superlinear in history size. That's the "slow after many compressions" symptom from the issue title, reproducible without #341.
On the other two sub-points (conceding framing): T1 nudge analysis is O(messages) per transform, not quadratic — agreed; on stock master it runs unconditionally every LLM transform (
inject.ts:371/380/384) even when no nudge can fire, so the gate removes wasted linear work rather than fixing a blowup. Save coalescing is likewise about redundancy/correctness (independent full-file writes per trigger, no ordering guarantee → stale-overwrite risk), not speed scaling.Suggestion: RC1/RC2 fix shared core code (
search.ts,token-utils.ts) used by both master'sresolveRangesand #341's planner — landing them on master makes #341 strictly faster when it eventually merges. If you'd rather keep the candidate-planning pieces out of this PR, I can split them into a follow-up scoped to #341; say the word and I'll restructure. Otherwise #385 stands as-is with the stock-build repro above.Merge pull request #385 from ranxianglei/2026-09-11_perf-transform-hi…