feat(proxy): add observe mode — measure what compaction would save, without enforcing it - #43
Merged
Merged
Conversation
…ithout enforcing it
To find out whether context-guru helps a workload you previously had to enforce it in
production and compare against history. Observe mode answers the question without
touching a single request.
The agent receives its request untouched, byte for byte. Byte-identity is STRUCTURAL,
not a property of careful copying: the request path does not run the pipeline at all,
and skips expand tool injection too (a tool declaration is a modification). A copy runs
off-path against observe's own state store, disjoint from the live one, purely to record
what compaction would have achieved. Measured cost to the enforced path: 0.062 ms/req
against sync's 1,599 ms on the same benchmark.
Two things make the projection trustworthy, and both were found by comparing it against
sync's actuals rather than by reading code:
- It shares the per-session cached-prefix boundary the enforced path uses. Without
that, cache-awareness gates nothing, every message looks compactable, and the
projection overstates by exactly what cache-awareness costs — 9.5% projected against
0.8% actually achieved on the same tasks.
- Its store is persistent, because offloaders freeze a decision and replay it on every
later turn, which is where most of the sustained saving lives. Running against a
discarded buffer instead makes it see only the current tail and UNDER-project by 3x.
The store is nevertheless disjoint from the live one: a decision observe made must
never be replayable by a real request, which would be a request modification
arriving by the back door.
On identical traffic the projection now matches sync's actual exactly (10,020 tokens /
23.06% both sides), and on Terminal-Bench — where sync achieves 1.02% — it correctly
projects 0% rather than inventing a headline. That negative control is the stronger half
of the evidence.
Observe results live in physically separate accumulators serialized under potential_* /
projected_*, sharing no key with an enforced metric, so a hypothetical cannot be summed
into a real saving even by accident. Two enforced keys stay deliberately real there:
cg_added_ms_avg (the actual enforced-path latency — reading ~0 IS the headline, zeroing
it would hide the result) and context-guru's own model spend, which is real money really
spent and is labelled by observe_llm_notice as the cost of measuring rather than
enforcing.
Also folds the per-session cached-prefix boundary into one locked call (modes.Tracker).
It was read from the store and written back in a `defer`, so two concurrent turns of one
session both read the same length and the second's write-back could land first, leaving a
boundary describing neither turn — which lets an offloader mutate content the provider
has already cached. Independent of modes; callers without a tracker keep the legacy path.
mode: sync remains the default and is byte-identical to before, asserted by a golden test
comparing the two entry points' output. /stats gains mode, sync_enforced and the observe
hypotheticals; every pre-existing field keeps its name and shape, since
deploy/harbor/*.py parses it.
Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
OsherElhadad
force-pushed
the
feat/i31-observe
branch
from
August 10, 2026 10:13
c19092e to
48eeb2a
Compare
Collaborator
Author
|
Rebased onto current Re-verified after the rebase: the projection-agreement result is unchanged ( Worth restating since #40 has now landed: observe's shadow store is a plain |
This was referenced Aug 10, 2026
This was referenced Aug 10, 2026
OsherElhadad
added a commit
that referenced
this pull request
Aug 10, 2026
…rve merges (#49) Five PRs landed in quick succession (#33, #36, #40, #42, #43), each updating its own docs. Nobody checked they were coherent together, and several pages described behaviour that no longer exists. The largest error: every one of the nine preset compositions in docs/reference/presets.md still ended in `cacheinject`, which #36 removed from all of them in favour of the new `cachesplit` marker component. `agent`, `aggressive` and `general` were also missing `extract_llm`, `general` was documented only in prose, and `balanced` was called "the default" when the proxy has defaulted to `codesmart` for some time. Each of the eleven pipelines is now verified component-for-component against the `presets` map. `cachesplit` was a registered component with no page and no nav entry; it has both now. That was the only registered/documented gap — every other components.Register call already had one. Reference pages were the other systematic gap. docs/reference/routes.md documented 5 of the ~45 fields the `Snapshot` struct serves; it now covers all of them, grouped, including `discarded_changes`/`top_discarded` (#36), the SSE quintet (#33), the cmdfilter ledgers (#42) and the observe namespace (#43). config.md gained the `store` block, five missing env vars, and lost a `cacheinject` example. Corrections carrying evidence discipline rather than just names: - cacheinject's placement section was headed "measured, not asserted" over a simulation, and its one live post-fix reading (n=1, +7.9% cost per step, +61.9% cache-write, mechanism unexplained, 0 of 106 marks landing where the suspected mechanism requires) was buried below the favourable numbers. The simulation is now labelled as one, and the negative live reading leads. - The root cause of the discarded `tool_result` breakpoint was attributed to bifrost. It is this repo's own `toolMessage()` in `normalize`. - routes.md now warns that `saved_tokens` is cumulative: the unique totals behind the two studies are 234,119 and 15,457 tokens, 21x and 8x smaller. - cmdfilter.md said 23 filters and first-line selectors; it is 24 filters over six leading lines. The four filters predicted to matter fired zero times and apt+gcc carried ~73% of live savings — recorded as a failed prediction, not quietly dropped. - The `repairLostResult` removal and extract_llm's exclusion from freeze-repair (its replacement is a sampled model output) are now in design.md. - docs described `extract` as the LLM component. `extract_llm` is; `extract` never calls a model. Historical results pages keep their original pipeline names, annotated with what changed since, rather than being rewritten to numbers the runs did not produce. Untouched: docs/results/terminal-bench-*.md and improvement-plan.md (held on #23), and extract_llm's economics (#34's subject). mkdocs build --strict passes; no orphan pages, no dangling nav entries. Assisted-By: Claude Opus 5 Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com> Co-authored-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
This was referenced Aug 10, 2026
Merged
OsherElhadad
added a commit
that referenced
this pull request
Aug 10, 2026
…cords nothing enforced (#54) cmdfilter called c.FilterStats with no mode check, so an observe-only run -- zero enforced requests, sync_enforced 0 -- still reported real-looking cmdfilter_families, cmdfilter_filters and cmdfilter_selector_misses entries. Those tokens were never saved: nothing was forwarded. Unlike the two fields observe deliberately shares (cg_added_ms_avg, a true measurement of the enforced path whose reading of ~0 IS the headline result; and context-guru's own model spend, which is real money and labelled by observe_llm_notice), these three sit in the enforced namespace with no mode label and no potential_* counterpart. A consumer cannot tell them from real savings. That is the failure #31 named as its primary correctness risk -- a mislabelled hypothetical is worse than no number, because it silently inflates the product's own headline claim. The gate is a Ctx.Stats() accessor rather than a check at cmdfilter's two call sites. A component author reaching for c.FilterStats has no reason to think about operating modes, so the next sink added to Ctx would reproduce this exactly; an accessor makes the safe path the only convenient one. New leakage from #42 and #43 composing -- neither PR's own review could see it, because the stats sink and observe mode landed independently. The test asserts sync DOES record, so it proves the gate rather than a dead sink, and fails without the gate with the reported symptom (acts=1 in observe mode). Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com> Co-authored-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
This was referenced Aug 10, 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.
Partially closes #31: observe mode lands here; async is deferred to #35 (kept as a
draft, all its review fixes intact).
Why split
#31 specified three modes. Observe is the smaller, lower-risk half and delivers the
adoption story on its own — the issue itself suggested landing it first. Async is held
because its measured benefit has since evaporated:
extract_llm, and perf(extract_llm): economic gate, global result cache, and the honest verdict (#28) #34 hard-declinesextract_llmon caching backends — so on theprimary workload there is nothing expensive left to defer. Terminal-Bench already showed
exactly this: zero LLM calls, async 26.8 ms vs sync 26.9 ms;
inert on claude-code, which sets its own breakpoints), so re-establishing it needs a fresh
50-task paired arm;
dropped/stale_discardedpaths — the ones guarding the correctness invariant —are exercised by tests only, never by production load.
Carrying a bounded worker pool, per-session generations, a copy-on-write store overlay and a
cache policy that must decline on the default agent is a lot of concurrency surface for an
unproven benefit. The primitives held up under a hostile review, which is why #35 stays on a
branch rather than being discarded: it is ready to measure, not ready to merge.
What observe does
The agent receives its request untouched, byte for byte.
Byte-identity is structural, not a property of careful copying: the request path does
not run the pipeline at all, and skips
expand.Injecttoo — injecting a tool declarationwould be a modification. There is no code path in observe mode that could alter a forwarded
body. A test asserts it anyway, including on a tool-carrying request where
expand.Injectwould otherwise append.
A copy runs off-path against observe's own store, purely to record what compaction would
have achieved.
Cost to the enforced path: 0.062 ms/req, against
sync's 1,599 ms on the samebenchmark. Confirmed independently in a live Claude Code session (0.209 ms vs 28.964 ms) and
on Terminal-Bench (0.076 ms vs 26.9 ms).
headroom has no observe/shadow/dry-run mode at all — its
tokenandcachemodes are bothenforcing, and its only control arm is a 10% output-shaper holdout. This is a genuine
differentiator, not a port.
Why the projection is trustworthy
Both requirements were found by comparing the projection against
sync's actuals, not byreading code — and each was a real bug:
cache-awareness gates nothing, every message looks compactable, and the projection
overstates by exactly what cache-awareness costs: 9.5% projected against 0.8% actually
achieved on the same tasks. A projection that ignores cache-awareness is a projection of
what a cache-blind proxy would do.
turn, which is where most of the sustained saving lives. Against a discarded buffer observe
sees only the current tail and under-projects by 3x.
It is nevertheless disjoint from the live store: a decision observe made must never be
replayable by a real request — that would be a request modification arriving by the back
door. A test asserts zero writes to the live store.
Three lines of evidence, strongest first:
The negative control is the more convincing shape: on traffic with almost nothing to save,
observe reports almost nothing rather than inventing a headline. A mode that only ever
agreed on high-savings traffic would be far weaker proof.
Metric namespace separation
Observe results live in physically separate accumulators serialized under
potential_*/
projected_*, sharing no key with an enforced metric. In observe mode every enforcedsavings aggregate is zero by construction —
requests,saved_tokens,sync_enforced,components: {}— which is the machine-readable form of "context-guru did not modifyrequests". A mislabelled hypothetical silently inflates the headline claim, so this is a
correctness boundary, and a test asserts no enforced aggregate can reach an observe result.
Two enforced keys are deliberately real there rather than zeroed:
cg_added_ms_avg— the actual enforced-path latency. Reading ~0 is the headline;zeroing it would hide the result.
llm_calls/llm_input_tokens/llm_output_tokens— context-guru's own model spend.Observe measures off-path and that costs real money, so relabelling it
potential_*wouldbe a worse lie than leaving it. It stays where cost tooling reads it, labelled by
observe_llm_noticeas the cost of measuring rather than enforcing.Also included: the
prevLenrace fixIndependent of modes, and worth flagging for #40's author. The per-session cached-prefix
boundary was read from the store and written back in a
defer, so two concurrent turns ofone session both read the same length and the second's write-back could land first — leaving
a boundary that describes neither turn. A boundary that is too high lets an offloader mutate
content the provider has already cached, costing a full cache-write of the suffix.
modes.Tracker.Turnnow reads and records it in one locked call. Callers without a tracker(library users,
/compact) keep the legacy path unchanged.store.Buffer/FrozenLoser— async-only, flagged deliberatelyI raised that a component asserting
c.Store.(store.FrozenLoser)would see a wrapper andsilently take the degraded path. That concern does not apply to this branch: observe's
shadow store is a plain
*store.Memory, which implementsFrozenLostnatively. Thecopy-on-write
store.Bufferexists only on #35, where the forwarding fix lives with it.Nothing is lost by #35 going to draft.
Compatibility
mode: syncremains the default and is byte-identical to today, asserted by a golden testcomparing the two entry points' output.
/statsgainsmode,sync_enforcedand theobserve hypotheticals; every pre-existing field keeps its name and shape (a test pins the
full set, since
deploy/harbor/*.pyparses this payload)./compactis mode-independent bycontract, also tested.
Rebased onto current
main, so it includes #36 and #40; the projection-agreement result wasre-verified after that rebase.
Tests
go test -racethroughout:syncbyte-identical to the legacy entry point, and unset mode identical to explicitsync(golden);expand.Injectwould otherwise modify;sync's actual on identical traffic (fails at ratio 0.33without the shadow store);
vocabularies disjoint in the serialized payload;
lost;
and never blocks; a panicking observation is contained;
Stopdoes not wait on anuncancellable job; no goroutine leak on
StoporHandler.Close, both idempotent;/statsbackward compatibility;/compactmode-independence; unknown mode rejected(including
async, which is not valid on this branch).Gates:
build·test -tags cg_skeleton·-race -count=5·gofmt -l·go vet·mkdocs build --strict— all clean.Docs
New
docs/how-to/operating-modes.md(when to use each, how to read the projection, andwhat observe cannot tell you — cache effects are projected not measured, no expand bounce
is exercised, and measuring still spends cheap-model tokens). New
docs/results/observe-mode.mdwith the three evidence lines and a "what is not established"section.
docs/design.md,docs/reference/config.md,docs/how-to/measure-savings.md,mkdocs.yml, README.Remaining for #31
Async mode, on #35 (draft): benefit conditional and currently unmeasured now that
extract_llmno longer runs on caching backends. What would establish it: a 50-task pairedarm with
async.strip_caller_breakpoints: true, showing cache-write no higher thansync'sand a real latency reduction on traffic that still makes model calls.