U8 — Context, processing, native & Rust¶
120 Python modules / 40,103 LOC + 13.4k LOC Rust workspace / 726 churn commits (12m). Reviewer: parallel unit review, 2026-09-03.
Overview¶
The native/Python split is genuinely roofline-aware for CPU-bound work (tokenize, similarity, pattern-match, chunking) and the fallback discipline is real — but semantic parity has drifted in exactly the two functions that guard the LLM context budget (token counting, context fitting), and a 4th "protocol" accelerator stack plus a dead victor/context/manager.py multiply maintenance cost. GIL handling and FFI data movement are good on similarity (batch, py.detach), absent everywhere else.
Findings¶
| ID | Sev | Location | Finding | Why it matters | Fix sketch | Effort |
|---|---|---|---|---|---|---|
| F1 | P1 | victor/processing/native/context_fitter.py:144-152 vs rust/crates/python-bindings/src/context_fitter.rs:124,152-155 | Strategy vocabularies diverged: Rust accepts fifo/priority/smart (default smart); Python fallback accepts recency/priority/balanced (default recency). Production passes strategy="smart" (victor/agent/conversation/assembler.py:388) which the fallback silently maps to balanced; "recency" passed to Rust silently maps to fit_smart |
Which conversation history survives truncation depends on whether the wheel is installed — the exact fallback-drift correctness risk the architecture mandates against. Zero parity tests (only tests/benchmarks/test_hot_path_performance.py:154 exercises native) | One shared Strategy enum (contracts or processing/native/_base); reject unknown names loudly; native-vs-fallback equality matrix over all strategies |
S |
| F2 | P1 | victor/native/rust/tokenizer.py:70; victor/processing/native/tokenizer.py:57-66,85; rust/.../tokenizer.rs:283-341 | Three token-count semantics in production: "exact" native = Rust punct/CJK-weighted heuristic (~90%); fallback exact = tiktoken cl100k; fast fallback = word×1.3. A full BPE BpeTokenizer pyclass exists (tokenizer.rs:195) but is constructed only in a benchmark |
Token budgets shift ~±20% (worse on code/punctuation-dense text) between native and fallback installs; fit_context budgets, providers/base.py:964 and store.py estimates inherit the skew |
Ship cl100k ranks once (lazy) into BpeTokenizer; make exact paths (native and fallback) both BPE; keep heuristic only for count_tokens_fast |
M |
| F3 | P2 | rust/Cargo.toml:35 (panic = "abort") vs context_fitter.py:93,198,265 |
Release profile builds the cdylib with panic="abort"; the pervasive except Exception: pass → Python fallback pattern is not a panic boundary — any Rust panic aborts the whole Python process |
Silent-fallback wrappers create a false safety story for a long-lived agent/serve process; one slicing/indexing bug in 10k LOC of Rust becomes a hard crash | Override to panic="unwind" for the extension (or catch_unwind in bindings); or document panics as fatal |
S |
| F4 | P2 | context_fitter.py:74,93; victor/native/rust/context_fitter.py:97; context_fitter.rs:53 | msg.get("priority", 1.0) feeds a float to MessageSlot(priority: u8); PyO3 rejects floats → TypeError for every message lacking an int priority; the flat shim swallows it (line 93) and silently downgrades to the Python path |
The Rust fitter is effectively dead for default-shaped inputs, hidden by the silent except; EWMA dispatch never sees honest rust samples | Default to 50 (int) at both call sites; log at debug on native-path exceptions |
S |
| F5 | P2 | rust/.../tokenizer.rs:274; context_fitter.rs:125; chunking.rs | GIL released only on the similarity path (py.detach, similarity.rs:191). All other batch entry points — count_tokens_fast_batch (rayon), fit_context, chunk batches — hold the GIL for the whole call |
Rayon parallelism buys nothing for multi-threaded victor serve: every other request thread stalls during batch counting/fitting |
Add py: Python<'_> + py.detach around the pure-Rust impl bodies (the _impl/wrapper split already exists in similarity.rs) |
M |
| F6 | P2 | victor/native/python/* + victor/native/rust/* (2,539 LOC, 14 files); context_fitter.py:100-174; context/manager.py:242-358 | Triple accelerator stacks: flat shims (production path), protocol classes (only real consumer: victor-coding chunker's get_text_chunker), plus manager.py pruning. Three near-copies of the fitting algorithm with three strategy vocabularies |
Every semantics fix must land 3-4 places — F1/F2 are the direct product; churn concentrates on the drifted, duplicated surfaces | Declare flat shims canonical; reduce protocol classes to thin delegates (or delete unused: token counter, context fitter, arg normalizer have zero production consumers); delete manager.py | M |
| F7 | P2 | victor/agent/response_sanitizer.py:415-417 vs rust/.../streaming_filter.rs:124,236 | _NativeFilterWrapper.flush() returns "" with comment "native filter has no buffering", but the Rust StreamingFilter does buffer partial <think… tags — never emitted at stream end. Python fallback's flush emits its buffer (response_sanitizer.py:353) |
Tail-of-stream content dropped only on the native path — backend-dependent content loss on the streaming hot path | Add a Rust flush() returning the residual buffer; wrapper delegates |
S |
| F8 | P3 | victor/context/manager.py:273-283,244,255 | _prune_fifo iterates reversed(other_messages) dropping the newest messages — FIFO inverted (drops newest, keeps oldest). Module is production-dead (one unit test) yet churn=13; uses print() with emoji |
401 LOC of maintained dead code containing a logic bug invites cargo-cult reuse into the real path | Delete the module — assembler.py is the real pruning path |
S |
| F9 | P3 | context_fitter.rs:145-147 | kept_indices.iter().filter_map(|&idx| messages.iter().find(...)) — O(n²) in kept messages when computing totals |
Negates part of the native win on long histories (200 msgs → 40k scans) | Index map or fold totals during selection | S |
| F10 | P3 | victor/classification/pattern_registry.py:181-184,1118,1134 | compiled_pattern property docstring says "(cached)" but calls re.compile fresh on every access, inside match loops over 76 patterns |
Per-query overhead; relies on re's internal 512-entry cache for the claim to hold |
Precompile in __post_init__ (or functools.cached_property) |
S |
| F11 | P3 | victor/processing/native/streaming.py:204 | Infrastructure-layer fallback imports victor.agent.response_sanitizer (runtime layer); response_sanitizer itself imports victor.processing.native — circular-by-design cross-layer dependency |
Streaming fallback impossible to extract/test independently; no guard covers this direction | Move StreamingContentFilter fallback into processing/native |
S |
| F12 | P3 | victor/agent/conversation/assembler.py:383-384,395-396 | Mixed budget currencies: assembler budgets in chars, converts via //4 chars-per-token to a token budget for fit_context, then re-sums results in chars |
Two approximation errors compound with F2's estimator skew; no single unit of budget accounting | One currency end-to-end; convert once at the provider boundary | M |
Co-design opportunities¶
- One-crossing slot fitting:
fit_contextcosts N FFI crossings to buildMessageSlotpyobjects + 1 to fit. Accept parallel lists (roles, token_counts, priorities, recencies) or build slots inside Rust. - Zero-copy embeddings: similarity takes
Vec<Vec<f32>>(full copy per call, similarity.rs:128) while the NumPy fallback wins benchmarks partly because it's zero-copy. Enable PyO3 buffer-protocol support; persistentEmbeddingIndexpyclass as the only batch path. - Content-hash memoized token counts: stored messages are immutable;
hashing.rsalready provides xxh3 signatures. Cache counts by signature in the conversation store/assembler instead of re-tokenizing the whole history every pass. - Release the GIL on batch entry points (tokenizer.rs:274, context_fitter.rs, chunking.rs) using the pattern proven in similarity.rs.
- Parity harness as CI law: generated test matrix asserting native == fallback for every flat shim (strategies × token estimators × streaming flush). F1/F2/F7 are precisely the drift class it would have caught.
- Ship the BPE ranks: one lazy
BpeTokenizer("cl100k", ranks)at startup unifies exact counting across both backends.
Keep intact¶
victor/processing/native/_base.py:30-65— single_NATIVE_AVAILABLEflag, one-time install hint,_PYTHON_PREFERRED_OPERATIONSbenchmark-aware dispatch (NumPy wins batch similarity); the honest single source of truth.pattern_match.rs:34-68— bounded (512-entry) Arc-shared Aho-Corasick automaton cache keyed by flags+patterns; exactly the right compiled-pattern amortization.victor/native/observability.pyadaptive EWMA dispatch (69-174, consumed by accelerator.py:460-498): per-backend EWMA withupdate_ewma=Falsefor non-attributable observers — a genuinely closed measure→dispatch loop.- API-reported usage written back via
store.py:1208 update_message_token_count— self-corrects estimator drift after each real LLM call; F2 is tolerable only because this exists. project_context.pysignature caching keyed by (path, mtime, size) with force_reload — correct incremental recompute for the I/O-bound side of context assembly.
Quick metrics¶
- LOC: processing 17,837 | security 9,307 | native 3,875 | context 3,698 | optimization 3,697 | classification 1,689 | rust/crates 13,371 (python-bindings 10,258).
- Rust: pyo3 0.29, rayon, aho-corasick, lru, no numpy interop;
py.detachonly in similarity.rs; releaseopt-level=3, lto=fat, panic=abort. - 27 per-call
hasattr(_native, …)checks in flat shims; 6 silentexcept→fallbacksites in processing/native. - Token estimators reachable in production: 4 distinct formulas (tiktoken exact; word×1.3; Rust punct/CJK heuristic; chars//4 in assembler).
fit_contextimplementations: 4 (Rust, flat-shim fallback,PythonContextFitter,manager._prune_*); strategy vocabularies: 2 disjoint sets; parity unit tests: 0.- Near-dead weight:
ProjectContextLoader(401 LOC, 1 test consumer),project_manifest.py(892, fan-in 0),command_parser.py(630, fan-in 0) ≈ 1,923 LOC.