Conversation
Document-level concurrency becomes the throughput lever (16/16), with three structural changes on top of the per-call write path: - worker: batch per-task pipeline items into grouped Pipeline calls -- same-article items within a batch share one write LLM call, bounded by max_items/flush_ms and a concurrent-call cap tunable at runtime via KAAS_WORKER_PIPELINE_BATCH_MAX_INFLIGHT; per-article lock serializes same-article writes across concurrent calls (lost-update fix) - indexer: debounced, staleness-bounded index rebuilds behind the rebuild_index gate, replacing per-call rebuilds (rebuilds ~4x fewer under load) - merge: full-rewrite threshold configurable via KB_MERGE_FULL_REWRITE_LIMIT with a degraded-diff fallback to full rewrite; extract_workers renamed to document_workers with a deprecation shim Config plumbing: new tuning keys with env overrides via a shared warn-and-fallback envInt helper (invalid values never abort startup); validate() rejects resolved values that cannot work (workers < 1, inflight < 1 with batching on). Replay benchmarks on a 150-doc production corpus (deepseek-v4-flash): baseline 136.0 min -> 95.3 min at inflight=2 -> 58.9 min at inflight=8, zero merge fallbacks, success rates unchanged. Full report: #1 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ge, and prompt tightening Sprint s2 of the distillation-throughput work, squashed: - llm: raw-text 3-tuple completion core with case-insensitive truncation matching, restart ladder with discarded-token alerts, continuation mode that keeps truncated partials (seam-safe dedup, bounded rounds, ceiling accounting), estimate_max_tokens sizing with a 16384 reasoning headroom, reasoning_effort passthrough with graceful 400 degrade, and empty-completion retry - merge: section-level merge machinery (router under semaphore, cost guard, section -> diff -> full-rewrite fallback chain), diff patches applied to the full on-disk article with validation, sized full-rewrite/create first rungs - extract: tightened prompts suppressing pre-JSON prose, hard end-with-JSON constraint, retry of only failed phase-2 groups, sized summarize calls - pipeline: write/classify per-call LLM observability lines - chore: ignore the .bench/ replay-benchmark harness 150-doc replay (round C): 150/150 success, extract -44% tokens/call, write discards -87%, zero merge fallbacks and zero continuation seams; wall 101.1 min at inflight=2 (round A reference: 95.3 min). The dominant remaining cause is per-call reasoning burn vs sizing. Full benchmark report: fork issue #1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tybot02
marked this pull request as draft
September 3, 2026 02:56
…ge#62 review) Findings from a review of PR bybit-exchange#62, verified against the PR head and fixed: Section merge (py): - Degrade to the legacy chain on duplicate ## headings, checked before the router call: reassembly keys bodies by heading text, so a duplicated heading would replace both occurrences with one rewrite built from the last body -- silently deleting the first occurrence's content on disk. - Catch EmptyCompletionError/DeadlineExceededError in the section router and EmptyCompletionError in the diff leg. Both inherit KBError rather than RuntimeError, so they escaped the designed fallbacks and the write phase Acked the article as an error instead of degrading (the empty-body mode was measured 3/3 on deepseek-v4-flash). EmptyCompletionError is now re-exported from kb_ai.llm. - Strip an echoed section heading only on an exact line match, so "## Notes" no longer eats the body of an echoed "## Notes on X". - Document that in auto mode the section path sits ahead of KB_MERGE_FULL_REWRITE_LIMIT; the rollback is KB_MERGE_SECTION_MODE=off. Batcher/worker (go): - Bound the flush daemon call with BatchDeadline + margin on the Go side. The deadline previously bounded only the shutdown wait, so a wedged daemon held the flush's inflight slot forever and hung the drain (cooperative enforcement cannot help when the daemon cannot answer at all). - Abandon on ErrBatcherClosed like circuit.ErrOpen: a shutdown racing Submit means no call was made and must not burn a task attempt. - Make the 2400s batch deadline configurable (pipeline_batch_deadline_sec, KAAS_WORKER_PIPELINE_BATCH_DEADLINE_SEC; negative rejected) -- a slow batch used to fail items a direct, deadline-free call would finish. Indexer/config/LLM: - Give the IndexRefresher its own circuit breaker: an index rebuild reads every article, so a deterministically broken index must not open the breaker gating extract/pipeline. - Blame reasoning_effort for a 400 only when the error body names it; any other 400 keeps the operator's knob and surfaces the real error. - Widen the continuation dedup window to 2048 chars so a re-emitted table or list block is not persisted as duplicated text. - Pass an explicitly negative document_workers through to validation instead of masking it with a positive deprecated alias. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Second review of PR bybit-exchange#62, verified against 9063758 and fixed: - llm: clamp every per-request HTTP timeout to the remaining batch deadline (a deadline already passed fails before the call is issued). A first attempt was never deadline-gated, so a healthy-but-slow batch could outrun the deadline by a whole write-timeout ladder while the Go-side watchdog killed it as wedged and its orphaned daemon request kept running and writing; the daemon now answers by deadline+epsilon and the watchdog margin only catches a daemon that cannot answer at all. - config: reject pipeline_batch_deadline_sec < 1. Zero silently lifted every batch bound at once -- no DeadlineSeconds, no Go-side call timeout, and an unbounded Close drain. - llm: a 400 whose body does not name reasoning_effort probes once without the param on the same attempt: success pins the blame and disables the knob, a repeat 400 raises with the knob intact. Generic gateways that refuse unknown fields unnamed keep working instead of hard-failing every call until the env var is removed. - merge: the diff leg degrades on OutputTruncatedError and DeadlineExceededError too, matching the section router's set. - merge: the heading-echo strip compares the first line, normalized, so echoes carrying trailing whitespace or a CR are still stripped while "## Notes" cannot eat the body of an echoed "## Notes on X". - merge: the router drops new-section headings duplicating the article's own or another proposed section -- they collide in the body map or mint duplicate headings on disk. - merge: a degraded diff with no fitting rewrite now says loudly that the extraction was not merged instead of Acking silently; _full_rewrite_limit documents the section path's precedence and the KB_MERGE_SECTION_MODE=off rollback. Full Python suite diffed against a clean-tree run: zero new failures (+14 tests). GOOS=linux build/vet clean; config tests green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Third review round of PR bybit-exchange#62, verified against 54e63b1 and fixed: - llm: the batch-deadline clamp is recomputed on every attempt inside the retry loop. A value frozen at the first attempt let a retry that started just inside the deadline outrun it by (frozen timeout - the wait+60 the guard insists on) -- up to ~830s past the deadline and beyond the Go-side watchdog margin, whose orphaned daemon request then kept running and writing. - merge: a new-section proposal naming a heading the article already carries is remapped to a route onto that section instead of dropped; dropping it silently left the material unmerged while the task Acked "merged". A heading duplicating another proposal still drops with an alert (no unambiguous placement). - llm: the generic-400 probe pins the blame on reasoning_effort only when the probe's own request succeeds. Any probe failure -- a repeat 400, a timeout, anything -- cancels the attribution, so an eventual ladder success after a timeout keeps the operator's knob enabled instead of disabling it process-wide on a coincidence. - merge: a degraded diff with no fitting rewrite raises instead of returning the unchanged article. Both callers' per-article handlers put the error into the task's Ack payload / the compile error list, and the skipped write keeps the frontmatter from recording a source whose content never landed. - config: correct the validation comment -- go-zero's default tag maps only an omitted key to 2400, so an explicit zero in the file reaches validation and is rejected there (proven by the new file-zero test). Acked as follow-ups, unchanged: batch-failure Nack correlation, the 2048-char seam-dedup residual. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…emap Fourth review round of PR bybit-exchange#62, verified against 4e161e1 and fixed: - llm: the generic-400 probe cancels its attribution only when refused with a 400 of its own. A transient probe failure (timeout, 503) cancels nothing, so a flaky-but-refusing gateway disables reasoning_effort on its first call -- with the previous any-failure cancel, such a gateway replayed the with-param -> 400 -> probe -> backoff -> 200 ladder on every call until the batch deadline ran out. - merge: DeadlineExceededError propagates out of _merge_diff instead of degrading to "diff result unparsable" -- no batch time left is a clock problem, and no fallback leg can run inside an expired deadline anyway; relabelling it sent operators to debug the model. - merge: routing a new-section proposal onto an existing heading emits a section_route_remapped diagnostic alert, so a router systematically drifting placement (and cost) from "insert" to "rewrite" stays visible to the degrade diagnostics instead of silently succeeding. Follow-ups acknowledged, unchanged: batch-failure Nack correlation, the seam-dedup heuristic. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tybot02
marked this pull request as ready for review
September 3, 2026 15:32
lucasmaan
approved these changes
Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A 500-document distillation run took 9.97 hours wall clock. The RCA attributed
~82% of LLM time to the write phase: documents ran through per-doc pipeline
calls at 4-way document concurrency, every pipeline call rebuilt the KB indexes
(534 rebuilds per run), and any article over 30 KB took the diff path with no
way to tune that threshold per deployment.
Follow-up replay analysis on the batched path showed the remaining cost was
generation economics, not structure: ~50% of article writes hit the 16,384-token
truncation rung (one replay round discarded ~1.1M completion tokens), the
restart ladder threw away every truncated partial, the diff path applied
patches to a truncated view of large articles (silently dropping content), and
extract spent roughly half its output on pre-JSON prose.
Change
Six commits on this branch: two feature commits and four review-hardening
fixes.
1. Batched write, debounced indexing, tunable merge threshold
Batched write
Per-task pipeline items now flow through a batcher that groups same-article
items into one write LLM call (
max_items=16, 2 s flush window, boundedconcurrent calls). The concurrent-call cap is runtime-tunable via
KAAS_WORKER_PIPELINE_BATCH_MAX_INFLIGHT. A per-article lock serializessame-article writes across concurrent calls, closing a lost-update window that
the old one-call-per-doc path never had. Rollback:
pipeline_batch_max_items=1.Debounced index rebuilds
Pipeline calls stop rebuilding the indexes per call (
rebuild_index=false);completions mark them dirty and one rebuild follows
index_debounce_secafterthe last completion, forced when dirty state ages past
index_max_stale_sec.Measured ~4x fewer rebuilds under load. The refresher runs on its own circuit
breaker, so a deterministically broken index cannot open the breaker gating
extract/pipeline. Rollback:
index_debounce_sec=0.Tunable merge threshold
The 30 KB full-rewrite threshold is configurable (
KB_MERGE_FULL_REWRITE_LIMIT,warns and falls back on invalid values), and a degraded diff result now falls
back to full rewrite when the rewrite fits the prompt budget instead of
silently dropping content.
Config plumbing
extract_workersis renamed todocument_workerswith a deprecation shim. Newtuning keys get env overrides through a shared warn-and-fallback
envInthelper — invalid values never abort startup — and
validate()rejects resolvedvalues that cannot work (workers < 1, inflight < 1 with batching on).
2. Reasoning-aware sizing, continuation mode, section merge, prompt tightening
Truncation handling (
py/src/kb_ai/llm/)finish_reasonvalues are matched case-insensitively acrossgateway spellings (
length/max_tokens/max_output_tokens); restartsemit alerts carrying the discarded token count.
continue_on_length=True) keeps a truncated partialas an assistant message and re-calls for the continuation, deduplicating seam
overlap on raw text — plain-text write paths stop discarding completed work.
estimate_max_tokens()sizes the firstmax_tokensrung from the expectedoutput length plus a 16,384-token reasoning headroom: reasoning-style models
burn 8-15K invisible tokens per call before any text appears, and billing is
per generated token, so over-granting is free while under-granting pays twice
(a restart discards, a continuation re-reasons).
reasoning_effortpassthrough (env knob) with graceful 400 degrade, and aretry for reasoning-only empty completions (
finish_reason=stop, empty body).Section-level merge (
py/src/kb_ai/core/merge.py)section-merge router (sized, semaphore-bounded) and rewrite only the affected
sections; a cost guard chooses full rewrite when more would change than a
rewrite costs, and a section -> diff -> full-rewrite fallback chain bounds
the worst case. Cost scales with changed sections, not article size.
Rollback:
KB_MERGE_SECTION_MODE=off.truncated view) and patch responses are validated before application.
expected output and use continuation mode.
Extract prompt tightening
JSON object; measured -44% completion tokens per call (9,349 -> 5,235) on the
replay corpus, with sampled extractions clean.
calls are sized.
Observability
The orchestrator logs per-batch write/classify LLM call counts, LLM time, and
completion tokens — the lines the replay harness parses for per-phase cost
accounting.
3. Review hardening (four follow-up commits)
Four review rounds on this PR surfaced issues in the new code itself; every
finding was verified against the tree before fixing, with tests added per fix:
##headings:reassembly keyed rewritten bodies by heading text, so a duplicate would
have replaced both occurrences with one rewrite and silently deleted the
other's content. The router also remaps (with a diagnostic alert)
a new-section proposal that names an existing heading — dropping it
silently unmerged its material while the task Acked "merged".
unchanged article: the error lands in the task's Ack payload / the compile
error list, and the frontmatter never records an unmerged source.
an echo cannot eat a shared-prefix body or slip through with trailing
whitespace.
(
pipeline_batch_deadline_sec, envKAAS_WORKER_PIPELINE_BATCH_DEADLINE_SEC;values < 1 are rejected — zero would silently lift every bound at once).
recomputed per attempt, so a batch cannot outrun its deadline by a
write-timeout ladder while an orphaned daemon request keeps writing. The
Go-side watchdog timeout then only catches a daemon that cannot answer at
all — previously nothing bounded a wedged call, and shutdown could hang.
EmptyCompletionError/OutputTruncatedError/DeadlineExceededErrorflow through the designed fallbacks instead of escaping to a generic
per-article error (
EmptyCompletionErrorwas not even exported before).Deadline exhaustion propagates as itself rather than being relabelled
"diff result unparsable".
reasoning_effortonly when the error body names theparam or a without-param probe succeeds outright; flaky-but-refusing
gateways converge on their first call instead of failing every request.
ErrBatcherClosedabandons the task like an open breaker: a shutdownracing Submit made no call and burns no attempt.
document_workersreaches validation instead ofbeing masked by the deprecated alias; the seam-dedup window covers
realistic re-emitted blocks (2,048 chars).
Deliberately left as follow-ups (not regressions vs
main): attempt-burncorrelation on batch call failures (each task's burn rate is unchanged from
the direct path), and the bounded seam-dedup heuristic (overlaps beyond 2,048
chars pass through).
Verification
reports: wangdahoo/kaas#1):
inflight=2 (1.43x) -> 58.9 min at inflight=8 (2.31x, +41% LLM work — not
the shipped default).
tokens/call, write-side discards -87% (1.10M -> 144K tokens), zero merge
fallbacks, zero continuation seams across 46 continuation events, list
coverage +5% vs round A.
95.3 min). The dominant cause — write-call sizing under-provisioning the
per-call reasoning burn — is what the 16,384 headroom addresses; its
projected effect (~0.80x round-A total tokens, wall back to ~90 min) has
not been re-measured end to end yet.
the hardening above. Per round, the full Python suite was failure-list
diffed against a clean-tree run of the same selection (zero new failures;
the flaky-on-Windows compile suite was compared via intersected stable
sets), and all four rounds verified zero regressions in earlier fixes.
internal/configsuite green;GOOS=linux go build ./...andgo vetclean (bridge/daemon.go is Unix-only, so worker/cmd packages are
cross-compile checked on Windows).
newly passing tests with zero new failures (the 175 Windows-environment
failures are pre-existing and unchanged).
🤖 Generated with Claude Code