v1.7.23
Release-arc summary (v1.7.6 → v1.7.23). 18 versions consolidated
into a single release covering the V7-1 through V7-26 work-stream:
new MCP knowledge tools (get_file/get_symbols/get_relations/
retrieve_stashed batch + codegraph BFS), Windows Job Object for
observer codex, the V7-19 critical buildAllow([]) nil-trap fix
(inverted the codex-variant recipe in v1.7.6-v1.7.18), the V7-21
tools-defs gate, the V7-22 → V7-24 compression measurement arc that
ended with restoring compress_types=["json","logs","code"] as the
claude-code default after n=8 A/B (−6.9% mean cost vs OFF), the
V7-25 stash-vs-Anthropic-cache finding (stash stays disabled by
default), and the V7-26 codex baseline (inconclusive on gpt-5.4;
codex-safe recipe unchanged). Operators MUST set
ENABLE_TOOL_SEARCH=true in the launching shell when using Claude
Code with observer's proxy — without it, Claude Code's SDK eager-
inlines MCP schemas under ANTHROPIC_BASE_URL and the proxy becomes
a net loss. Full empirical writeup in
docs/v1.7.23-compression-savings-empirical-2026-06-01.md.
Headline (v1.7.23): Re-measured the v1.7.22 catastrophic regression
on the V7-22 binary and found it gone. V7-22's preceding fixes
(V7-19 nil-trap + V7-21 tools-defs gate) closed enough of the
re-marshal pathway that per-type compression no longer cascades on
the V7-22+ binary. n=8 Claude Code B-arm with per-arm prompt salts on
V7-22 binary 0a2b7900...: −6.9% mean cost vs n=4 OFF baseline
($1.069 vs $1.148), CV 7.6% (tighter than OFF's 7.5%), zero tail
outliers (max cost $1.168, max turns 18). The claude-code recipe
default compress_types is restored to ["json","logs","code"] —
the empirical winner. Operators MUST set ENABLE_TOOL_SEARCH=true
in the launching shell when using this recipe; without it Claude
Code's SDK eager-inlines MCP schemas under ANTHROPIC_BASE_URL
(~+21K tokens/turn), turning the proxy into a net loss. Plus: stash
is documented as cache-breaking on Anthropic traffic (V7-24: n=1
showed +25% cost / +97% cache_creation) and stays disabled by
default. Full empirical writeup:
docs/v1.7.23-compression-savings-empirical-2026-06-01.md.
Fixed (v1.7.23)
- Default
CompressTypesrestored to["json","logs","code"]
(internal/config/config.go:703). V7-22's defensive[]flip was
based on n=4 on V7-21 binary; V7-24 n=8 on V7-22 binary shows the
cascade is gone. The compressor implementations were already kept
in place; this just re-enables them in the default allow-set. claude-code.tomlrecipe →compress_types = ["json","logs","code"]
in bothdocs/recipes/andinternal/config/recipes/(mirrored
to satisfyTestRecipes_InternalAndDocsAreIdentical). Inline
comments capture V7-23's empirical baseline and theENABLE_TOOL_SEARCH
operator requirement.claude-code.tomlrecipe — stash documented as DO NOT ENABLE
on Anthropic traffic (V7-24). Stash's content-replacement breaks
Anthropic's prefix cache, costing more than the bytes-on-wire
savings. Disabled by default; operators can opt in for measured
workloads but shouldn't expect savings.
Updated tests (v1.7.23)
internal/config/config_test.go::TestDefaultCompressTypesIsEmpty
renamed toTestDefaultCompressTypesIsJSONLogsCode; assertion
flips fromlen(got) == 0back to{"json","logs","code"}.
Comment captures the V7-24 n=8 measurement and the historical
default lineage.internal/config/recipes_test.go::TestLoadRecipe_ClaudeCode
assertion flips fromlen == 0to["json","logs","code"]plus
a new check thatStash.Enabledis false. Comment captures the
V7-24 rationale.
Backwards compatibility (v1.7.23)
- Anyone using no explicit
[compression.conversation]block sees
the default flip from[](v1.7.22) back to["json","logs","code"].
This is the same default value as pre-v1.7.22 — operators who
were on v1.4.40-v1.7.21 see no semantic change relative to that
era; operators who were on v1.7.22 see compression re-enabled.
Per the V7-24 measurement, this is empirically a net win on
Claude Code traffic; codex and other OpenAI traffic see the same
default but the fast-path early-return makes it a no-op when no
event fires. - The
claude-coderecipe behavior reverts to the v1.4.40+
semantic — per-type compression on. Other knobs unchanged. - The
codex-variantrecipe is unchanged — still[]. The V7-21
$0.270 measurement holds; codex operators see no behavior change. - The
codex-saferecipe is unchanged — still["logs"]. - Operators who want v1.7.22 passthrough behavior opt out by
settingcompress_types = []in their config or recipe override.
Operator note (v1.7.23)
Set ENABLE_TOOL_SEARCH=true in your shell when using Claude Code
with observer's proxy. Claude Code's SDK disables its
ToolSearch:optimistic deferred-MCP-loading feature under
ANTHROPIC_BASE_URL. Without the override, all MCP tool schemas
(observer's 17 + any Google Auth/Drive/Calendar/Gmail) are eager-
inlined into every request — ~+21K tokens per turn on the reference
rig. Observer's proxy forwards tool_reference blocks byte-identically,
which satisfies the SDK's documented safety condition for the override
(strings ~/.local/share/claude/versions/*/claude | grep ENABLE_TOOL_SEARCH).
For operators wanting to verify: the cost-check.sh script in
docs/skills/observer-cost-tuning/scripts/ reads observer's
/api/cost endpoint and shows real per-tool $/turn from your
actual sessions.
v1.7.22 (superseded by v1.7.23)
Headline (v1.7.22): The first real Claude Code A/B against the
proxy (n=4 OFF + n=4 B-arm via observer on :8831 running the V7-21
binary with the claude-code recipe compress_types = ["json","logs","code"], mode = cache_aware) measured a +60% cost
regression and +88% turn-count regression vs no-proxy Claude Code.
The model was doing nearly 2× the work to complete the same refactor
task. Root cause is the same shape as V7-19: every per-type
compression event bypasses the fast-path early-return in
runAnthropic and triggers serializeAnthropic re-marshal. The
marshalEnvelope helper sorts top-level keys alphabetically + escapes
HTML off, producing a byte-different shape than Claude Code's native
ordering. Anthropic's prefix cache (which keys on the first ~N bytes)
misses; cache_creation doubles; the model loses visibility into
prior content and re-derives → V7-11 cascade. v1.7.22 flips the
default compress_types from ["json","logs","code"] to [] and
mirrors the change in the claude-code recipe. Per-type compression
becomes opt-in everywhere — operators who want it (e.g. cache-cold
short sessions where logs/code byte savings outweigh the re-marshal
tax) opt in explicitly. The codex-variant recipe (which V7-21 made a
small net win vs OFF at $0.270 codex / $0.30 OFF) and the codex-safe
recipe (still ["logs"] — explicit operator opt-in) are untouched.
Plan + BC analysis:
docs/v1.7.22-claude-code-passthrough-plan-2026-06-01.md.
Fixed (v1.7.22)
- Default
CompressTypesflips to[]
(internal/config/config.go:703). The 2026-06-01 Claude Code A/B
showed["json","logs","code"]triggered the V7-11 re-derivation
cascade because every per-type compression event re-marshals the
envelope throughmarshalEnvelope(alphabetical key order),
producing a byte-shape Anthropic's prefix cache won't hit. Empty
default → fast-path early-return on every turn → byte-identical
forwarding → cache hits hold. The compressor implementations
(compressToolResults,LogsCompressor,CodeCompressor,
JSONCompressor, etc.) are kept; they just don't fire by default. claude-code.tomlrecipe →compress_types = []in both
docs/recipes/andinternal/config/recipes/(mirrored to satisfy
TestRecipes_InternalAndDocsAreIdentical). Inline comments capture
the V7-22 measurement + opt-in path.
Updated tests (v1.7.22)
internal/config/config_test.go::TestDefaultCompressTypesIncludesCode
renamed toTestDefaultCompressTypesIsEmpty; assertion flips from
{json,logs,code}tolen(got) == 0. Comment captures the V7-22
finding (+60% cost / +88% turns under prior default) and the
historical default lineage (["json","logs"]→["json","logs","code"]
in v1.4.40 →[]in v1.7.22).internal/config/recipes_test.go::TestLoadRecipe_ClaudeCode
assertion flips from["json","logs","code"]tolen == 0. Comment
captures the V7-22 rationale + opt-in path.
Backwards compatibility (v1.7.22)
- Anyone using no explicit
[compression.conversation]block sees
the default flip from["json","logs","code"]to[]. For Anthropic
traffic this is unambiguously a win (the A/B measurement was −60%
cost). For OpenAI traffic the codex-variant + codex-safe operators
already had explicit configs so no change; operators with no config
hitting OpenAI would also see the change but their behavior was
governed by the same fast-path/slow-path split as Anthropic, so the
outcome there is also expected to be neutral-to-positive. - The
claude-coderecipe behavior changes from per-type-on to
per-type-off. The recipe's other knobs (mode = cache_aware,
target_ratio = 0.85,preserve_last_n = 5) are unchanged. - The
codex-variantrecipe is unchanged — still[]. Codex
operators see no behavior change from V7-21's $0.270 cost mean. - The
codex-saferecipe is unchanged — still["logs"]. That's
an explicit opt-in to logs compression and respects operator intent.
If real-world codex-safe usage hits the same re-marshal regression
it can be addressed in a future release. - Operators who want pre-v1.7.22 per-type behavior opt in by
settingcompress_types = ["json","logs","code"](or any subset)
in their config or recipe override.
Documentation (v1.7.22)
- New:
docs/skills/observer-cost-tuning/— a Claude Code skill- operator guide explaining the ~+36% per-turn cost overhead
observers see when routing Claude Code through observer's proxy.
Diagnosis: Claude Code's SDK disablesToolSearchdeferred MCP
loading wheneverANTHROPIC_BASE_URLis set, which forces all
registered MCP tool schemas (observer's 17 tools + any Google
Auth/Drive/Calendar/Gmail tools the operator has registered) to
be eagerly inlined into every request's prefix — ~+17K tokens of
prefix per request, ~+36% per-turn cost vs no-proxy (measured
n=3 OFF / n=3 valid B-arm, 2026-06-01). This is an SDK behavior,
not a proxy bug; v1.7.22'scompress_types = []default already
makes the proxy byte-passthrough on Anthropic traffic. The skill
presents two operator tiers (hooks-only =observer init --claude-code --skip-mcp→ 0% overhead; full MCP = default
→ +36% overhead, in exchange for the 17 MCP tools being callable
from Claude Code in-session) and explicitly marks the light MCP
tier (subset-of-tools knob) as planned for v1.8.x. Ships with
three reference files (operator-guide.md, setup-matrix.md,
why-toolsearch-disabled.md) + a runnablescripts/cost-check.sh
that reads observer's own/api/costendpoint to show actual
per-tool $/turn over a chosen window. Discoverable from
docs/architecture.md(mcp row) anddocs/claude-code-smoke.md
(cost-tuning callout). Operators install the skill by symlinking
docs/skills/observer-cost-tuning/into~/.claude/skills/;
automated install viaobserver init --claude-code --skillis
tracked for v1.8.x.
- operator guide explaining the ~+36% per-turn cost overhead
Open follow-ups (post-V7-22)
The real structural fix is to make serializeAnthropic produce
byte-stable output that matches Claude Code's (and OpenAI clients')
native key ordering, OR to do byte-level in-place tool_result
substitution instead of re-marshaling the full envelope. Either
approach would let operators turn on per-type compression AND keep
the prefix cache hits. Tracked as a v1.7.23+ or v1.8.x follow-up:
- Option A: parse the original body's top-level key order once,
restore it inmarshalEnvelope. Smaller change. - Option B: byte-level rewriting — when a tool_result content
shrinks, find it by offset in the original body and splice the new
content in. Larger change but more thorough.
Headline (v1.7.21): The V7-19 quantitative validation (n=8 codex-variant
runs through the V7-19-fixed binary, 2026-06-01) showed the catastrophic
+9-11% regression was gone but a residual +10% proxy overhead
remained vs the no-proxy baseline. Cache-hit ratios across the n=8
B-arm and n=10 OFF were statistically identical (+0.5%), so the
already-flagged "C16 read-cache breaks prompt_cache_key" hypothesis
was disconfirmed. The real cause: compressToolDefinitions (Anthropic
- OpenAI Responses + OpenAI Chat — all three pipeline.go sites) ran
unconditionally, with no gate fromcompress_types. Whenever a
tool entry's description tail orparameters.examplesshrank — i.e.
on every real tool schema — the early-return fast-path was bypassed,
serializeAnthropic/serializeOpenAIResponsesre-marshaled the
envelope, andjson.Marshalreordered top-level keys + rewrote HTML
chars. This doesn't break the prefix cache (the new shape stabilises
and re-caches), but every re-marshaled turn permanently inflates
prior-turn bytes carried forward — net-new input per turn is ~14%
larger than no-proxy. The hit is universal across providers, not
codex-specific: Claude Code on the defaultcompress_types = ["json","logs","code"]recipe paid the same overhead (just hidden
in the noise of larger conversation footprints — we have no Claude-
Code-traffic empirical data; the lumen C-arm tested the claude-code
recipe applied to codex traffic, not real Claude Code). v1.7.21 adds
Tools ContentType = "tools"as a sentinel and gates all three
compressToolDefinitions*call sites behindallow[types.Tools].
Defaultcompress_typesstays["json","logs","code"](tools NOT
included), so every shipped recipe stops trimming tools and the +10%
overhead disappears for current operators. Operators who want to
preserve the trim explicitly opt in viacompress_types = [..., "tools"]. Plan + BC analysis:
docs/v1.7.21-tools-defs-gate-plan-2026-06-01.md.
Added (v1.7.21)
types.Tools ContentType = "tools"sentinel
(internal/compression/conversation/types/detect.go:27). Unlike
the other content types (json/code/logs/text/diff/html),Tools
is not returned by any detector — it only appears as a key in
buildAllow's map. Sole consumer: the gate at the three
compressToolDefinitions*call sites.- Regression tests pinning the V7-21 contract
(internal/compression/conversation/pipeline_test.go):TestPipeline_ToolsDefsGated_NotInCompressTypes_NoTrim— default-
list recipe (no"tools"incompress_types) → tool-defs trim
skipped → body forwarded byte-identically →examplessurvives,
description tail survives, fast-path taken.TestPipeline_ToolsDefsGated_InCompressTypes_DoesTrim— opt-in
recipe ("tools"added) → tool-defs trim fires as before →
description tail trimmed,examplesstripped.
- V7-21 compendium row in
docs/v4-codex-compression-recipe-and-issues.mddocumenting the
finding, the +14% net-input vs the +10% cost ratio, the cache-hit
parity, and the BC blast radius.
Fixed (v1.7.21)
compressToolDefinitionsgated behindcompress_typesat all
three pipeline sites (internal/compression/conversation/ pipeline.go:358Anthropic,:447OpenAI Chat,:509OpenAI
Responses). Pre-v1.7.21 the trim ran unconditionally; post-v1.7.21
it only fires whencompress_typesincludes"tools". The
allow := buildAllow(p.cfg.CompressTypes)value is also hoisted
out of the immediatecompressToolResultscall so each pipeline
path computes it once.TestPipeline_EmptyCompressTypes_SkipsAllPerTypeCompression
extended to assert notoolsmechanism event — the V7-19 +
V7-21 combined contract. Pre-v1.7.21 this test allowedtoolsto
fire under emptycompress_types; that gap was the V7-21 footprint
in the regression suite.
Backwards compatibility (v1.7.21)
- All shipped recipes stop trimming tools.
claude-code(default
list["json","logs","code"]),codex-safe(["logs"]), and
codex-variant([]) all omit"tools"— operators on any
shipped recipe see tool-defs trim stop firing. Wire effect: the
envelope'stoolsfield is forwarded byte-identically. Net effect:
removes the ~$0.03/session (~10%) re-marshal overhead measured on
codex-variant; same effect on claude-code (unmeasured but
structurally identical). - Cold-cache regression risk —
compressToolDefinitions's own
doc comment says: "on warm turns it produces zero net savings on
the wire (the tools field is cached either way)". The savings
materialise on cache-cold turns (first message of a session, or
after the cache TTL elapses). Multi-turn sessions: re-marshal tax
dominated; v1.7.21 is a clear win. Very-short sessions with large
tool schemas: the cold-cache savings might exceed the
per-session re-marshal tax; operators with this profile opt in
viacompress_types = [..., "tools"]. - The default
CompressTypeslist ininternal/config/config.go: 703is unchanged. Tools is opt-in everywhere.
Headline (v1.7.20): Dashboard analysis-headline "prior month same day"
window overflowed past first-of-current-month when the prior month
was shorter than today's day-of-month (e.g. on May 31, the
[priorMonthStart, priorMonthStart + (now - monthStart)) window
ended at May 1 ~14:00 UTC instead of April 30 23:59), silently
double-counting MTD turns in the prior-month baseline. The bug
surfaced as a CI failure on TestAnalysisHeadline_PriorMonthSameDay
during the V7-19 validation cycle (PR #36, 2026-05-31). For
operators viewing the Analysis tab on the 29/30/31 of a 31-day
month after a 30-day prior month, the displayed vs_prior_month_pct
KPI was wrong — typically showing a smaller (or inverted)
month-over-month delta than reality. Fix is a one-line clamp:
if priorMonthSameDayEnd.After(monthStart) { priorMonthSameDayEnd = monthStart } at internal/intelligence/dashboard/analysis.go:71.
A new deterministic test TestAnalysisHeadline_PriorMonthShorterThanCurrentDay
pins the May-31 regression case under an injected clock so the
guarantee holds regardless of when CI runs.
Fixed (v1.7.20)
- Analysis headline prior-month window clamp
(internal/intelligence/dashboard/analysis.go:75). When prior
month is shorter than today's day-of-month (any 31st in a row
preceded by a 30-day month — Mar 31, May 31, Jul 31, Oct 31, Dec
31 — plus Mar 29/30/31 after Feb), the prior-month-same-day
comparison window now correctly stops atmonthStartinstead of
leaking into the current month. The fix is additive — it only
changes behavior on days where the window would have overflowed. - Clock injection on
dashboard.Server(dashboard.go:75).
New unexportednow func() time.Timefield defaults to
time.Now().UTC(); tests override to pin date-sensitive handlers.
Only the analysis-headline handler is wired throughs.now()in
this PR (the other 7time.Now().UTC()sites in the dashboard
package are unchanged and remain candidates for follow-up if
their tests start flaking on a calendar boundary).
Added (v1.7.20)
- Regression test
TestAnalysisHeadline_PriorMonthShorterThanCurrentDay
(internal/intelligence/dashboard/analysis_test.go). Pins
now = 2026-05-31 14:00 UTCvia the newServer.nowoverride and
seeds:- prior-month turn at Apr 15 12:00 UTC (always inside prior month)
- MTD turn at May 1 08:00 UTC (inside the buggy overflow window
[May 1 00:00, May 1 14:00)— without the clamp, this leaks
intopriorMonthSameDayUSDand the assertion=0.30fails
with0.9000…).
Headline (v1.7.19): Critical — buildAllow([]) nil-trap silently
inverted the codex-variant compression recipe shipped in v1.7.6. The
recipe sets compress_types = [] intending "compress nothing per
type" (the V7-11 mitigation). buildAllow([]) returned nil, and 5
downstream gates use if allow != nil && !allow[ct] { continue } —
which short-circuits to false when allow == nil, meaning every
content type was allowed (compressed) instead of denied (skipped).
Net effect for every operator on v1.7.6 + codex-variant recipe: the
most aggressive per-type compression ran (including the
destructive text head-tail compressor), stripping ~3.8 MB of tool
output per typical session, triggering the V7-11 re-derivation
cascade. Empirical (lumen A/B against codex 0.133 + ChatGPT-Plus,
gpt-5.4 × medium, strict-prompt n=10): the codex-variant arm cost
+9-11% MORE than the no-proxy baseline — the exact opposite of
the recipe's stated goal. v1.7.19 fixes the nil-trap at two layers
(defense in depth): buildAllow now returns a non-nil empty map for
empty input, and all 5 call sites drop the allow != nil && guard
so nil-map access correctly returns the zero value. Full
investigation in /tmp/lumen-ab/INVESTIGATION-codex-regression.md
(empirical evidence + code trace + git archaeology + remediation plan).
Fixed (v1.7.19)
-
V7-19
buildAllow([])returns non-nil empty map
(internal/compression/conversation/pipeline.go:622). Empty input
now produces an empty map that, when accessed with!allow[ct],
correctly returnstruefor every key — expressing the intended
"deny all per-type compression" semantics. Nil input also returns
an empty map for symmetry. -
5 call sites drop the
allow != nil &&guard:internal/compression/conversation/openai.go:989(Responses API
per-type)internal/compression/conversation/openai.go:1327(Chat
Completions per-type)internal/compression/conversation/anthropic.go:328(Anthropic
per-type)internal/compression/conversation/budget.go:286(budget
enforcer compressMessageTextSection)internal/compression/conversation/budget.go:320(budget
enforcer compressStructuredOutputSection)
New gate:
if !allow[ct] { continue }. Go nil-map access returns
the zero value (false), so!allow[ct]correctly returns true on
a nil-or-empty map → skip per-type compression.
Backwards compatibility (v1.7.19)
compress_types = []now correctly compresses nothing per type
(matching the codex-variant recipe's documented intent). Operators
who relied on the v1.7.6-v1.7.18 buggy behavior (silently maximum
compression undercompress_types = []) will see different output.
Practical impact: the buggy behavior was the V7-11 cascade trigger
the recipe was supposed to FIX — operators were never benefiting
from it, only paying for it.compress_typesunset / nil now also means "compress nothing"
(same as empty). Pre-v1.7.19 nil meant "compress all" via the
short-circuit. The only call sites that pass nil are the public
Pipeline.Runpath throughbuildAllow(p.cfg.CompressTypes), and
every shipped recipe + default sets a non-emptycompress_types—
so this BC change is observable only in the explicit
compress_types = []recipe (which got the wrong behavior anyway).- The always-on transformers (tools-defs trim + C16 read-cache
substitution) are NOT underbuildAllow's gate and continue to
fire regardless ofcompress_types. Their cache-key impact is a
separate issue (open follow-up in the V7-19 investigation doc).
Closed V7 issues (v1.7.19)
- V7-19 —
buildAllow([])nil-trap inverts codex-variant recipe.
Was the catastrophic regression measured against the V4 codex
compression doc's V7-11 mitigation strategy — the recipe was
literally doing the opposite of its stated goal. Investigation
surfaced by lumen A/B test on 2026-05-31, fixed same day.
Headline (v1.7.18): Two compression-telemetry fixes surfaced by a
live A/B test against usernametba/lumen on 2026-05-31 (codex-CLI +
ChatGPT-Plus, n=1 per arm). V7-18 Bug 1 clips negative
tokens_saved_est / cost_saved_usd_est_* columns when the
conversation pipeline runs in pass-through mode (codex-variant recipe
with compress_types=[]) — per-turn JSON re-marshaling + envelope
overhead can make compressed_bytes > original_bytes (measured
~3.7% inflation on an 8-turn audit run), and pre-fix observer cost
reported negative dollar savings which misled operators into
believing the codex-variant recipe was actively HURTING them. V7-18
Bug 2 populates compression_events.body_hash on the tools
mechanism Event construction sites (anthropic + 2× openai paths) so
store.CountUniqueCompressions (v1.7.12 V7-9) actually deduplicates
the per-turn re-emission of identical tool-schema events. Both fixes
are observability-only — no compression behavior changes.
Added (v1.7.18)
- V7-18 Bug 2 BodyHash on tools-mechanism Events: the three
tool-definition compressor sites —internal/compression/ conversation/anthropic.go::compressToolDefinitions,
openai.go::compressOpenAIResponsesToolEntry, and
openai.go::compressOpenAIChatToolEntry— now populate
Event.BodyHash = bodyHashHex(string(raw))matching the v1.7.12
V7-9 pattern wired on the logs/code/json compressor sites.
Cross-turn dedup: OpenAI's Responses API re-sends the fulltools
array on every turn, so before this fixcompression_events
recorded N events for the same logical tool definition over an
N-turn conversation.store.CountUniqueCompressions(ctx, sessionID)
now returns the correct deduped count (1, not N). - V7-18 Bug 1 clip negative
tokens_saved_estin
cost.Engine.Summary:savedBytesis now floored at 0 before
flowing intoTokensSavedEstand the three USD savings columns
(CostSavedUSDEst,CostSavedUSDEstInputTier,
CostSavedUSDEstCacheReadTier). Negative savings have no
operational meaning — they're an artifact of the conversation
pipeline's per-turn envelope overhead under pass-through recipes,
not an actual cost increase from compression. Raw
original_bytes/compressed_byteson the row are preserved
unchanged so operators can still see wire inflation via the JSON
summary or direct SQL queries againstapi_turns.
Backwards compatibility (v1.7.18)
- Both fixes are observability-only. No proxy behavior changes — the
conversation pipeline still runs identically,compression_events
rows still emit (now with stable body_hash on the tools mechanism),
cost rollups still compute identically except that previously-
negativecost_saved_usd_est_*values now read 0. - Existing operators reading the JSON summary will see
cost_saved_usd_est_*go from "occasionally negative" to "always
≥ 0". Dashboards graphing the savings column may show a one-time
jump if they previously aggregated negative values (most don't). - The CLI
SAVEDcolumn was already clipping for display (renders
0B (0%)when savings are non-positive), so no CLI-output drift.
This fix moves the clip from the renderer up to the engine so JSON
consumers see the same clipped value as the CLI. compression_events.body_hashwas nullable in v1.7.12 (migration
031); v1.7.18 fills it on the tools mechanism path only. Pre-
v1.7.18 rows stay NULL and are excluded from the dedup query, same
as the other compressor paths from v1.7.12.
Closed V7 issues (v1.7.18)
- V7-18 — compression telemetry surfacing surfaced by the
lumen A/B test (2026-05-31). Both sub-bugs fixed:- Bug 1: negative
cost_saved_usd_est_*under pass-through - Bug 2: tools-mechanism
body_hashwiring gap
- Bug 1: negative
Headline (v1.7.17): Three diagnostic improvements from the V3/V4
carry-forward backlog. V3-4 ships a per-user hook-events.jsonl
log at ~/.observer/hook-events.jsonl so operators can debug "why
didn't the hook fire?" without raising the log level. V3-5 adds
a LATENCY_MS column to observer cost surfacing the
proxy-observed mean request → response time per group bucket from
api_turns.total_response_ms. V4-2 restores the
observer cost --filter <substring> flag (lost in the v1.7.2 cobra
flatten) for narrowing rollups by key substring with OR semantics.
Added (v1.7.17)
- V3-4 hook-event log: every observer hook invocation appends one
JSONL row to~/.observer/hook-events.jsonlwith{ts, event, bytes, session_id?, tool?, action}. Per-process mutex + O_APPEND
for race safety across goroutines and processes. Best-effort —
HOME unset / mkdir fails / write fails = silent no-op. The hook
MUST NOT break the host tool (spec P1). - V3-5 proxy latency in
observer cost:cost.Row.AvgLatencyMSfield on the JSON summary (omitempty when
zero — JSONL-only buckets emit 0 → field absent).LATENCY_MScolumn in the CLI table, appears only when at least
one row has a populated value. Pure JSONL summaries keep the
v1.7.16 column layout — no observable change for non-proxy users.- Rows with
total_response_ms = 0(JSONL adapter path + pre-v1.4.x
proxy rows) are excluded from the mean — including them would
skew the average downward.
- V4-2
observer cost --filter:- Repeatable flag; multiple filters are OR-combined.
- Case-insensitive substring match against
Row.Key. - Totals are NOT recomputed — they represent the full window's
activity. Operators wanting filtered totals use
--group-by none --filter ....
Backwards compatibility (v1.7.17)
- V3-4: new file under
~/.observer/. No existing files touched, no
schema migrations. Operators canrmthe file at any time. - V3-5:
LATENCY_MScolumn shows only when populated; CLI layout
unchanged for JSONL-only summaries.AvgLatencyMSusesomitempty
in JSON — pre-v1.7.17 consumers reading specific fields see no drift. - V4-2:
--filteris purely additive. Pre-v1.7.17 invocations
without the flag behave identically.
Closed carry-forward items (v1.7.17)
- V3-4 — hook event log file
- V3-5 — per-turn proxy latency in observer cost
- V4-2 — observer cost --filter flag restore (lost in v1.7.2)
Headline (v1.7.16): V7-17 — V7-12 response-surface polish from the
notes file. Four items shipped: in-band warnings: [...] array,
regex fallback when codegraph is unavailable, drift signal
(index_lines + live_lines) when codegraph is stale, and
include_nearby opt-in for sibling-symbol context. Closes the V4
codex compression doc — including the notes file's stretch items —
fully observer-side.
Added (v1.7.16)
- Warnings array (item 1): top-level
warnings: [...]on every
V7-12 tool response. Closed-set tag strings:
codegraph_unavailable,codegraph_stale,
index_changed_mid_query,regex_fallback_language_unsupported.
Agents pattern-match on the literals rather than parsing free
text.internal/mcp/warnings.gois the single source of truth. - Live regex parser (item 2 + 4 foundation): new
internal/codegraph/livesympackage withParse(path) → (syms, langUnsupported, err). Supports Go (function / method / type /
interface), TypeScript/TSX (function / class / interface / type),
JavaScript/JSX (function / class), Python (function / async def
/ class). EndLine is-1in v1.7.16 — honest "regex can't tell"
rather than fragile brace-counting; future tree-sitter integration
can lift the limitation. - Regex fallback when codegraph unavailable (item 2): get_symbols
now returns approximate matches via livesym when
codegraph.Available() == false. Matches carry the real name +
kind + start_line + language; end_line = 0 (existing "unknown"
sentinel); per-resultdegraded: true; envelope warning
codegraph_unavailable. Unsupported extensions return empty matchesregex_fallback_language_unsupportedwarning — honest "we can't
help here" rather than misleading silence.
- Drift signal (item 4): when
codegraph.Stale(absPath)fires,
get_symbols runs the live regex parser alongside the index query
and surfaces both positions per match via newindex_lines
({start, end}) andlive_lines({start, end}) fields on
symbolMatch. Steady-state (no drift) keeps both fields omitted —
v1.7.15 response shapes preserved byte-identical when codegraph is
up-to-date. include_nearbyopt-in (item 1 — naming overlap with V7-17 item
1 unintentional): get_symbols requests can setinclude_nearby: N(>0) to getnearby_symbols: [...]per match — up to N before- N after by
start_line, same file. Capped server-side at 20 per
side. Default 0 = omitted field, v1.7.15 BC.
- N after by
Backwards compatibility (v1.7.16)
- Every new field uses
,omitempty. Pre-v1.7.16 callers reading only
the legacy keys see no observable change in steady-state responses. - The
degraded: trueflag stays semantically identical.warnings
is additive context, not a replacement. index_lines+live_linesare populated ONLY when they differ;
agreement = both fields omitted.nearby_symbolsopt-in via request flag; absent flag = absent
field.- Regex fallback only fires when codegraph is unavailable. Operators
with codebase-memory-mcp installed see identical behavior to
v1.7.15.
Closed V7 issues (v1.7.16)
- V7-17 (capture of the V4 notes-file polish bundle) — all 4
items shipped. Full V4 codex compression document is now closed
observer-side. Remaining pure-deferrals (V7-5 operator-script,
V7-7 methodology, V7-10 upstream codex bug) are not in observer
scope.
Headline (v1.7.15): V7-1 Windows Job Object — observer codex now
attaches its codex.exe child to a Job Object configured with
KILL_ON_JOB_CLOSE. If the wrapper dies for any reason (clean exit,
watchdog Stop-Process hammer, SIGKILL), Windows automatically
closes the wrapper's handles and codex.exe terminates with no
zombie left behind to keep writing to its rollout JSONL.
v1.7.5 documented the failure mode + shipped the operator-script
workaround (observer codex --exclusive enumerates and terminates
zombies before exec); this PR closes the loop with an observer-side
fix that prevents the zombie from forming in the first place.
Closes the V4 codex compression issues doc's last observer-codeable
remainder. Remaining V7 items (V7-5 operator-script, V7-7
methodology, V7-10 upstream codex bug) are not in scope for
observer.
Added (v1.7.15)
- New
internal/jobobjectpackage:AttachProcess(*exec.Cmd) (io.Closer, error)— creates an
anonymous Job Object withJOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
and assigns the started process to it. cmd MUST have been
Start()'d already (itsProcesspopulated).- Windows-only impl uses
golang.org/x/sys/windows
(CreateJobObject/SetInformationJobObject/
AssignProcessToJobObject). Non-Windows builds compile to a
no-op stub returning a no-op Closer so the call site doesn't
need build tags. - Returned Closer holds the Job Object handle;
Close()is
idempotent. Operators wanting an explicit kill (vs. waiting
for wrapper process exit) callClose()to fire
KILL_ON_JOB_CLOSEon every assigned process immediately.
cmd/observer/codex.gosplitchild.Run()into
child.Start()+child.Wait()soAttachProcesscan wedge
between them. Attach failure is non-fatal: logged to stderr,
spawn continues without V7-1 protection.
Backwards compatibility (v1.7.15)
- Non-Windows builds: AttachProcess is a no-op stub. Linux + macOS
operators see zero observable change. POSIX signal forwarding
already cascades wrapper death to children, so the protection is
unnecessary outside Windows. - Windows builds where AttachProcess fails (rare — would require
the OS to refuseCreateJobObjectorAssignProcessToJobObject,
e.g. due to existing-job restrictions): stderr log line, codex.exe
continues to run unprotected — same behavior as v1.7.14. child.Run()→child.Start()+child.Wait()refactor at
the call site is semantically identical: same stdio, same exit
code, same*exec.ExitErrorunwrap path.
Closed V7 issues (v1.7.15)
- V7-1 — Windows codex.exe zombie. Observer-side fix complete;
operator-script workaround (observer codex --exclusive) stays
available as a belt-and-suspenders measure for hosts that boot
with pre-existing zombies from previous (pre-v1.7.15) sessions.
Headline (v1.7.14): V7-14 operator CLI — observer mcp-audit with
five subcommands (list / stats / top-paths / denied /
purge). The audit log shipped in v1.7.8 (migration 030 + writer)
required raw SQL to interrogate; this PR adds an operator-facing
read surface so "why was X denied?", "which file is the agent
re-reading?", and "is the MCP load growing?" become one-liners.
Every read subcommand supports --json for machine consumption;
purge requires both --older-than and --yes for destructive
hygiene.
Added (v1.7.14)
- New
internal/mcp/audit/reader.go— read-side queries on the
mcp_audittable:List(ctx, db, opts)— filtered row list (session, tool,
since, only-denied, limit); default cap 50 to keep CLI output
tractable.StatsSummary(ctx, db, since)— aggregate counts (total / ok /
denied / truncated) + per-tool breakdown.TopPaths(ctx, db, since, limit)— most-requested
path_requestedvalues, grouped + summed bytes + ok/denied
split. Excludes NULL paths so tools without a path argument
don't drown the signal.Purge(ctx, db, olderThan)— destructive deletion; rejects
olderThan <= 0as a footgun.
- New
observer mcp-auditCLI (cmd/observer/mcp_audit.go):list [--session ID] [--tool NAME] [--since DUR] [--limit N] [--json]stats [--since DUR] [--json]top-paths [--since DUR] [--limit N] [--json]denied [--since DUR] [--limit N] [--json]purge --older-than DUR --yes--sinceaccepts Go-style duration strings (30m,24h,
168hfor 7d, etc.); empty/0= all time.purgerequires both--older-thanand--yesso a
misclick on the flag set doesn't wipe history.
- New
printListRows/printStats/printTopPathshelpers use
text/tabwriter(matchingobserver cost's output cadence). - Tab-aligned table format for human reading; per-row JSON
marshalling for scripts.
Backwards compatibility (v1.7.14)
- Pure-additive — no existing CLI surfaces change. New top-level
subcommand underobserver mcp-audit. Older operators not setting
any new flags see no behaviour difference. audit.Stats/audit.TopPath/audit.ListRoware new
exported types ininternal/mcp/audit; reader functions are
additive on top of the existing async-writer surface.
Closed V7 issues (v1.7.14)
- V7-14 — MCP audit log + operator CLI. The schema + writer
shipped in v1.7.8 (PR #25); this PR ships the operator surface.
V7-14 is now fully closed.
Headline (v1.7.13): V7-8 cross-process stash-dir detection. The proxy
that initialises stash compression now advertises its active dir in
a sidecar file under the observer home (~/.observer/.proxy-stash-dir.txt).
observer serve reads the sidecar at startup and surfaces one of
three outcomes: aligned (Info), mismatch (Warn with both paths +
recovery hint), or no proxy yet (Info pointing at sidecar path).
Decouples cross-process discovery from any shared registry — works
across multiple observer instances on the same host with last-
writer-wins on the sidecar.
Added (v1.7.13)
- New
internal/stashalignpackage:WriteSidecar(homeDir, stashDir)/ReadSidecar(homeDir)
round-trip the active proxy-side stash dir via the
.proxy-stash-dir.txtsidecar. Atomic write via tmp+rename so a
concurrent read never sees a partial path. Missing sidecar reads
as("", nil); other I/O errors propagate so operators can
distinguish "no proxy yet" from broken permissions.CompareDirs(mcpDir, proxyDir)returns(ok bool, hint string).
Resolves trailing slashes + symlinks viafilepath.EvalSymlinks
so operators symlinking the stash dir to a shared volume still
align cleanly. Mismatch hint names both paths and the specific
remedy ("repoint one side, or start both with same --config").DefaultObserverHome()returns~/.observer/— one place for
the home-dir constant.
- Proxy side (
cmd/observer/proxy.go): afterstash.Newsucceeds,
writes the sidecar. Failures log Warn and continue — stash
compression still works. - MCP server side (
cmd/observer/serve.go): afterstash.New
succeeds, reads the sidecar and compares. Three outcomes logged
distinctly so operators can grep their startup log.
Backwards compatibility (v1.7.13)
- Sidecar write is best-effort — proxies that fail to write
(e.g. read-only home dir) still serve compression normally. - MCP servers that find no sidecar log Info, not Warn — operators
running an older proxy version (no sidecar) don't get a
false-positive alarm. - The detection is advisory: nothing changes in retrieve_stashed's
call shape or the stash's contents. Operators ignoring the warning
fall back to v1.7.12 behavior (silent misses on mismatched dirs).
Closed V7 issues (v1.7.13)
- V7-8 — cross-process stash-dir mismatch detection. The latent
failure mode from the V4 codex compression doc — proxy and MCP
server pointed at different dirs — now surfaces a loud warning at
startup instead of silent retrieve_stashed misses.
Headline (v1.7.12): V7 backlog cleanup — three independent fixes
that close the smaller correctness/telemetry items from the V4 codex
compression document. V7-3 strips codex's shell envelope before
content classification so wrapped JSON / Diff / HTML payloads
classify on their inner content, not as Logs. V7-9 adds
compression_events.body_hash so the "comp_events per turn" metric
stops being inflated 30-50× by re-compression of the same body
across turns; new store.CountUniqueCompressions(ctx, sessionID)
returns the deduped count. V7-13 Gap 5 (b) logs a one-shot
warning per observer serve lifetime when the codebase-memory-mcp
DB is re-indexed between startup and a subsequent query, so
operators know which sessions might have spanned the boundary.
Added (v1.7.12)
internal/compression/conversation/types.Detectnow pre-strips
codex's shell envelope (Exit code: N\nWall time: X.Xs\nOutput:\n)
before classification. Wrapped JSON → JSON, wrapped Diff → Diff,
etc. Bodies without the envelope pass through unchanged via the
\Aregex anchor. Negative exit codes and the three Wall-time
suffix variants (Xs/X seconds/ bareX) all recognised.
(V7-3.)compression_events.body_hashcolumn (migration 031) — sha256-hex
of the pre-compression body. Populated at the 10 single-body Event
construction sites inbudget.go,anthropic.go, andopenai.go.
Pre-v1.7.12 rows stay NULL and are excluded from the dedup query.
Partial indexidx_compression_events_turn_body_hash WHERE body_hash IS NOT NULLkeeps the index small. (V7-9.)store.CountUniqueCompressions(ctx, sessionID)returns
COUNT(DISTINCT body_hash)within a session — the correct answer
to "how many unique tool_result bodies actually hit the
compressor", not the inflated "N turns × same body" gross count.codegraph.Clientcaptures startup mtime and emits a one-shot
warning on every subsequent query when the DB file's mtime has
changed sinceOpen. Atomic CAS gate prevents concurrent queries
from flooding the logs. Wired into all 10 DB-hitting public query
methods. (V7-13 Gap 5 b.)
Backwards compatibility (v1.7.12)
- The V7-3 envelope strip only fires when the body STARTS with the
exact three-line codex envelope. Bodies that merely contain the
phrase later (e.g. shell scripts that print "Exit code:" diagnostic
text) pass through unchanged. Verified by the
\A-anchored regex + the trailing-not-leading test case. - V7-9 migration is additive (ALTER TABLE … ADD COLUMN nullable);
existing aggregate queries (SUM(original_bytes),
COUNT(*) BY mechanism) keep working unchanged. Only the new
CountUniqueCompressionsreader needs the column. - V7-13 Gap 5 (b) is observability-only — no query results change.
bodyHashHextakes a string (not []byte) to match the
pipeline's existing tool_result representation. No public-API
change.
Closed V7 issues (v1.7.12)
- V7-3 — codex shell envelope misclassification.
- V7-9 — compression_events accounting dedup.
- V7-13 Gap 5 (b) — codegraph re-index warning. V7-13 is now
closed modulo Gap 1 (declined by operator in v1.7.11 — disk-path
encode-in-marker variant) and Gap 2 (iv) (per-conversation pinning
sidecar — deferred to v1.8.x).
Headline (v1.7.11): mcp__observer__retrieve_stashed extended to
batch (array of shas in one call) and line-range slicing
(start_line / end_line per call). Closes the V7-12 retrieval
arc — fourth and final of the four V7-12 tools alongside get_file
(v1.7.8), get_symbols (v1.7.9), get_relations (v1.7.10). New
top-level [intelligence.mcp].features allow-list (V7-16) lets
operators gate the four V7-12 tools as a bundle. V7-8 stash-dir
advisory log line at observer serve startup surfaces
proxy ↔ MCP-server alignment to operators eyeballing logs.
Pre-extension callers (sha: "X" single-string form) see a
byte-identical response — pinned by
TestRetrieveStashed_BackwardsCompat_SingleStringByteIdentical.
Added (v1.7.11)
mcp__observer__retrieve_stashedacceptssha: string | string[].
Array form returns the new{ok, responses: [...]}envelope with
per-shaok/reason/content/returned/
total_lines_in_blob. Array-of-one ALWAYS returns the envelope
shape — array literal == explicit caller intent (D-2 in
docs/v1.7.11-stash-retrieval-correctness-plan-2026-05-31.md).- Optional
start_line/end_lineslice the blob (1-based,
inclusive). Single-string + range augments the legacy shape with
returned: {start, end, total}+total_lines_in_blob; array- range applies the range to every sha uniformly.
- New
Stash.ReadSlice(sha, startLine, endLine)in
internal/stash/: returns sliced bytes + total line count. Fast
pathstartLine == 0 && endLine == 0is byte-identical to
Stash.Read. Bumps the blob's mtime so slice-only reads keep the
whole blob warm for LRU eviction (V7-13 Gap 2). - New
[intelligence.mcp.retrieve_stashed]config:enabled
(default true),max_shas_per_call(default 25, matches
get_symbols's batch cap). - New
[intelligence.mcp].featureslist (V7-16): top-level
allow-list scoped to the four V7-12 tools. Default[]= no
filter applied; non-empty = strict allow-list. The 13 built-in
observability tools are NOT subject to the filter — see D-3 in
the v1.7.11 plan doc for the scope decision rationale.
Precedence: per-toolenabled = falsealways wins. - Per-tool kill switch:
[intelligence.mcp.retrieve_stashed].enabled = falsekeeps
proxy-side stash compression active while denying the agent the
retrieval surface (asymmetric-trust scenarios). - V7-8 transparency:
observer servestartup logs
mcp: stash dir active dir=… max_total_mb=…at Info level when
the stash is initialized. Operators verify proxy ↔ MCP-server
alignment by comparing both processes' startup logs. Automatic
cross-process detection remains future work. - Audit semantics (V7-14): one
mcp_auditrow per resolved sha.
N shas in an array call → N rows.path_requestedencodes the
sha asstashed://<sha>soWHERE path_requested LIKE 'stashed://%'queries sweep retrieve_stashed activity uniformly. - New operator reference
docs/mcp-retrieve-stashed-reference.md:
TOML, schema, all three response branches with examples,
failure modes, audit semantics, V7-16 gating, V7-8 alignment,
worked multi-elision example.
Backwards compatibility (v1.7.11)
- Pre-extension callers using
sha: stringwith no other new
parameters get the v1.7.10 response shape byte-identical
(including pretty-print indentation). Pinned by
TestRetrieveStashed_BackwardsCompat_SingleStringByteIdentical_MaxBytesNoNewKeys.
Options.RetrieveStashedDisabledis a negative flag (default
false) so existing test wiring (Options{Stash: st}without
any new fields) still registers the tool — no observable change
for v1.7.10 callers.- The new
Options.Featuresfield defaults to nil/empty, which
skips the V7-16 filter entirely. v1.7.10 behavior preserved.
Closed V7 issues (v1.7.11)
- V7-12 — full four-tool retrieval surface ships.
- V7-13 Gap 2 — already-shipped
Chtimesmtime-bump-on-read +
raisedMaxTotalMB=1024default confirmed; documented in the
plan doc. Per-conversation pinning sidecar deferred to v1.8.x. - V7-16 — features list + BC contract pinned.
Headline (v1.7.10): mcp__observer__get_relations — the third of
four V7-12 retrieval-surface MCP tools. Codegraph-native graph
traversal: ask "what calls handleClick within 2 hops?" and get back
the reachability set as a flat list (depth + via_edge) in one MCP
turn. No recursive grep, no multi-round-trip discovery. Pairs with
v1.7.8 get_file (byte-level) and v1.7.9 get_symbols (per-symbol
bodies) — get_relations is the cheaper metadata-only alternative
for impact-analysis questions where you don't need bodies.
Added (v1.7.10)
- New
mcp__observer__get_relations(project_root, file, name, fqn?, kind, depth?)
MCP tool.kindenum:callers(inbound CALLS),callees
(outbound CALLS),contains(outbound CONTAINS). Returns
{ok, anchor, kind, depth, results: [{symbol, depth, via_edge}], truncated, degraded}.
Single anchor per call; ambiguous (file, name) →ok: false
withcandidates: [...]so the agent can immediately re-call
with a pinnedfqn. (V7-12, third of four.) - BFS via single recursive CTE in SQLite — cycle-safe via
UNIONdedup on(id, depth, via_edge), depth-bounded in the
recursive step, shortest-path-wins via outer
GROUP BY n.id MIN(depth). One query per call regardless of
reachable-set size. - Two new public methods on
*codegraph.Client:
Reachable(ctx, anchorID, direction, maxDepth, maxResults)
(returns[]Reachableplus truncated flag) and
CountEdgesByKind(ctx, kind)(used to detect "CONTAINS not
populated by codebase-memory-mcp" and surface adegraded: true
hint instead of misleading empty success). - New
[intelligence.mcp.get_relations]config block:enabled
(default true),max_depth(default 5),max_results(default
100). Path-safety knobs shared with[intelligence.mcp.get_file]. - New operator reference
docs/mcp-get-relations-reference.md:
TOML, schema, response examples (success / ambiguous / degraded),
CONTAINS-edge population caveat, cycle-termination guarantees,
audit-log queries, common flows (impact analysis, reachability,
structural orientation). - Cross-references added in
docs/codex-compression-recipe.md,
docs/mcp-get-file-reference.md, and
docs/mcp-get-symbols-reference.md.
Changed (v1.7.10)
internal/mcp.OptionsgainsGetRelations+GetRelationsEnabled
fields. Backward-compatible.
Headline (v1.7.9): mcp__observer__get_symbols — the second of four
V7-12 retrieval-surface MCP tools. Batched symbol lookup: one MCP
turn returns N symbol bodies across M files via the codegraph index,
bypassing codex's shell tool (which would re-feed LogsCompressor).
Pairs with v1.7.7 marker enrichment + v1.7.8 get_file: marker tells
the agent what's in the elided range, get_symbols fetches the
bodies + optional callers/callees in a single batched call. Includes
the V7-15 fqn disambiguation ranking (less the is_exported factor,
which the codegraph schema doesn't carry today; documented).
Added (v1.7.9)
- New
mcp__observer__get_symbols(project_root, requests: [{file, name?, fqn?, kind?, include_relations?, include_body?}, ...])
MCP tool. Returns unified envelope
{ok, results: [{request, ok, matches, ambiguous, disambiguation_hint, degraded, reason}], truncated, degraded}.
Up to 25 requests per batched call. (V7-12, second of four.) - Discovery mode: omit
nameandfqnto get every user-facing
symbol in the file (functions, methods, classes, interfaces,
types) — without body. Cheap preview; agent picks bodies to fetch
in a follow-up batched call. - V7-15 ranking for ambiguous matches: exact-fqn (SQL-time) →
kind filter (SQL-time) →start_lineASC →fqnASC →fileASC
→idASC.ambiguous: truefires when len(matches) > 1 and no
fqn pinned;disambiguation_hintcarries a literal recipe
(Use fqn (e.g. "Editor.handleClick") to ...).is_exported
factor deferred (codegraph schema lacks the column). include_relations: trueaddscallers_count/callees_count
totals + top-Ncallers/calleeslists with name/fqn/kind/file/
start_line per entry. Caps configurable via
[intelligence.mcp.get_symbols].max_callers/
.max_callees(default 20 each, V7-12 spec).- 200 KB total body cap per response, deterministic truncation
in input order so OpenAI's prefix cache stays stable across
re-calls. - Stale-codegraph protection: per-request
degraded: truewhen
codegraph.Stale(file)returns true. Recovery suggestion in
reasondirects the agent toget_file. - New
[intelligence.mcp.get_symbols]config block:enabled,
max_callers,max_callees. Path-safety knobs (allow/deny lists,
max response KB) shared with[intelligence.mcp.get_file]— one
place to keep in sync. - Five new public methods on
*codegraph.Client:
FindSymbols(ctx, file, name, fqn, kind),CallersOfSymbol,
CalleesOfSymbol,CountCallers,CountCallees. The two
per-symbol caller/callee accessors return richCallerrows
(name, fqn, kind, file, start_line) for V7-12include_relations;
the existingCallersOf([]string)stays for v1.7.7 marker
enrichment which only needs names. - New operator reference
docs/mcp-get-symbols-reference.md: TOML,
schema, V7-15 ranking explanation,is_exporteddeviation
rationale, body-cap algorithm, common flows, audit-log queries.
Public-doc carve-out viascripts/release.sh(3-site sync).
Changed (v1.7.9)
cmd/observer/serve.gonow wiresmcp.Options.CodegraphClient
from[compression.code_graph].path(withFindProjectDB(cwd)
fallback). This Options field was declared but never assigned in
serve.go pre-v1.7.9, socheck_file_freshness/get_file_history
silently returned emptystructure: {}enrichment in production.
Bonus fix bundled with v1.7.9 sinceget_symbolsneeded the wiring
anyway.- Refactored
internal/mcp/tools_get_file.go:readSliceinto shared
internal/mcp/fileread.gosoget_fileandget_symbols
truncate identically. Wire-shape unchanged —getFileResult.Lines
serializes as the same{start, end, total}JSON. internal/mcp.OptionsgainsGetSymbols,GetSymbolsEnabled
fields. Backward-compatible: nil values plus
GetSymbolsEnabled: falsekeep existing test wiring intact.
Headline (v1.7.8): mcp__observer__get_file — the first of four V7-12
retrieval-surface MCP tools. v1.7.7's marker enrichment told the
agent what lives in an elided range; get_file lets it fetch the
bytes directly, bypassing codex's shell tool (which would re-feed
LogsCompressor and re-truncate the response). Bundled with the
V7-13 Gap 4 path-scoping defenses and the V7-14 audit log schema.
Added (v1.7.8)
- New
mcp__observer__get_file(project_root, path, start_line?, end_line?)
MCP tool registered when[intelligence.mcp.get_file].enabledis
true (default). Returns{ok, path, project_relative_path, lines, body, size_bytes, truncated}. Pairs with the v1.7.7 marker
enrichment: agent reads the marker, sees what's in the elided
range, callsget_filefor the bytes without re-triggering
compression. (V7-12, first of four PRs.) - New
[intelligence.mcp.get_file]config block:enabled,
allow_extensions(default ~25 source-code extensions),deny_paths
(default.env*,*.key,*.pem,.git/**,node_modules/**,
.ssh/**,.aws/**, etc.),max_response_kb(default 100).
Path-safety defenses (V7-13 Gap 4): project-root containment via
filepath.EvalSymlinks(symlink-escape attempts denied),
allow-extension allow-list, deny-glob list, response-size cap.
Deny-glob syntax supports*,?,<dir>/**; unsupported
patterns (character classes, braces, escapes) log one warning
per startup so silently-dead rules are visible. - New
mcp_auditSQLite table (migration 030) + writer package
internal/mcp/audit. Everyget_filecall (success or denial)
writes one row with tool name, session id, request hash,
requested path, response size, OK flag, deny reason, and call
duration. Operator-facing for "why was X denied?" and "what files
is the agent reading?" — queryable today viasqlite3 ~/.observer/ observer.db 'SELECT ...'; managed CLI deferred to v1.8.x.
Async buffered writer (drop-oldest on overflow + once-per-minute
log throttle) keeps the MCP hot path under the V7-14 latency
budget. (V7-14, minimal scope; operator CLI follow-up.) - New
[intelligence.mcp.audit] enabledswitch (default true).
Local-only forensic value; opt-out emits one stderr line at
startup so the operator's choice is visible. - New
docs/mcp-get-file-reference.mdoperator guide: config
reference, tool schema, defense-layer rationale, audit-log
example queries, accepted-risk discussion (TOCTOU, symlink
semantics). Public-doc carve-out viascripts/release.sh. - Cross-reference in
docs/codex-compression-recipe.mdlinking
the v1.7.7 marker enrichment to v1.7.8'sget_fileretrieval
path for codex-variant model operators.
Changed (v1.7.8)
internal/mcp.OptionsgainsGetFile,GetFileEnabled, and
AuditWriterfields. Backward-compatible: nilAuditWriter
defaults toaudit.NewNoopWriter()so existing tests that
constructmcp.Serverwithout audit wiring still work.
Headline (v1.7.7): LogsCompressor marker enrichment — codex-variant
models reading … [231 lines elided: 3 ERRORs, 12 WARNs] distrust the marker and re-derive via shell tools — the
+121% gpt-5.3-codex × high regression measured in the v4 batch
(docs/v4-codex-compression-recipe-and-issues.md). v1.7.7 makes the
marker self-documenting: when the producing tool's argument carried
a file path (and codebase-memory-mcp is installed + the index isn't
stale), the marker reads
… [231 lines elided from src/components/Editor.tsx: 3 ERRORs, 12 WARNs; preserved 5 anomalies; file contains fn handleClick, class Editor, fn useEffect (codegraph)]. Both enrichment layers are
opportunistic — zero hints produces byte-identical output to
pre-v1.7.7. (V7-11 mitigation (e); V7-13 Gap 3 + Gap 5 prerequisites.)
Added (v1.7.7)
- New optional
HintedCompressorinterface in
internal/compression/conversation. Compressors implement
CompressHinted(body, hints)to opt into receiving filename +
codegraph symbol context; the Registry detects via type assertion
and falls through to the basicCompresswhen not implemented.
LogsCompressor opts in; JSON / Code / Text / Diff / HTML stay
on the bare interface. (V7-11 (e) part 1) codegraph.SymbolsInFile(ctx, absPath)— returns the top-N
user-facing symbols defined inabsPath(functions, methods,
classes, interfaces, types). Sorted bystart_line ASCfor
determinism; capped at LIMIT 50; filters noise kinds (variables,
parameters) at the SQL layer. Schema-tolerant — missing schema /
unavailable client returns nil + nil.codegraph.Stale(absPath)— advisory check returning true when
the file's mtime exceeds the codegraph DB's mtime + 5s slack.
Caller pattern: skip symbol pre-fetch when stale so the marker
doesn't claim a function exists at a line where the file no
longer has it. Fail-open. (V7-13 Gap 3 mitigation)Pipeline.WithCodegraph(c CodegraphLookup)— chainable builder
mirroringWithStash. The proxy opens codegraph via the explicit
[compression.code_graph] path = "..."config key (override) or
codegraph.FindProjectDB(cwd)(auto-discover) and passes a narrow
adapter through. Best-effort: missing codegraph degrades marker
enrichment to filename-only without aborting startup.- New optional
[compression.code_graph] pathconfig key for
operators with multi-project setups who want to pin the graph.db
consumed by marker enrichment.
Changed (v1.7.7)
- LogsCompressor elision marker is now self-documenting when hints
are present. The G32 level breakdown + E27 anomaly lift-out from
prior releases are preserved unchanged; the new filename + symbol
enrichments compose additively. (V7-11 (e) part 2) codegraph.Openno longer passesimmutable=1in the SQLite DSN.
The hint told SQLite to skip page locking — safe only when the
file is guaranteed unchanged. codebase-memory-mcp's indexer
re-writes the graph DB concurrently with observer's read-only
queries, so the hint risked torn-page reads. Plainmode=rouses
normal page-level locking and is concurrent-write safe. Throughput
cost: ~3-5% on read latency — well worth the correctness gain.
(V7-13 Gap 5 mitigation)
Headline (v1.7.6): codex-friendly compression. The v4 codex compression A/B
session (docs/v4-codex-compression-recipe-and-issues.md,
2026-05-29) measured up to +121% cost regression on *-codex
reasoning models under the Anthropic-style default
compress_types = ["json", "logs", "code"] — JSON sentinel
substitution destroys data values codex relies on, and the
LogsCompressor middle-truncation pass triggers re-derivation
cascades on reactive reasoning models. v1.7.6 ships five small,
additive surfaces giving operators the knobs and recipes to opt
out without source modifications, plus a per-session warning when
the proxy detects the misconfiguration. (#…)
Added (v1.7.6)
[compression.conversation.logs]config block exposing
max_lines,head,tailknobs for LogsCompressor truncation.
Default 200/100/100 (unchanged from the prior hardcoded
constructor). Setmax_lines = 0to disable middle truncation
entirely — the codex-variant recipe's setting. (V7-11 mitigation b)observer start --recipe <name>flag selecting an embedded
compression recipe applied between defaults and the on-disk
config files. Three recipes ship:claude-code(Anthropic
default formalised),codex-safe(logs-only; gpt-mini family),
codex-variant(compress_types = [];*-codexfamily).
Recipes live atinternal/config/recipes/*.toml(embedded) with
byte-identical mirrors atdocs/recipes/*.toml; a sync test
pins both copies in lockstep.docs/codex-compression-recipe.mdoperator-facing guide: when
to pick which recipe, the OpenAI-vs-Anthropic cache-architecture
asymmetry, override patterns, troubleshooting. Public-doc
carve-out alongside the recipe TOMLs.cost_saved_usd_est_input_tier+cost_saved_usd_est_cache_read_tier
columns inobserver cost --json,/api/cost?bucket=day, and
the cost summary envelope. Operators wanting upper/lower bounds
alongside the weighted column read these directly. (V7-6)
Changed (v1.7.6)
- Proxy emits one stderr warning per session when an OpenAI-shape
request's model matches the codex-variant family
((?i)(?:^codex-|-codex(?:-|$))— gpt-5.3-codex,
gpt-5.3-codex-low/medium/high/xhigh, codex-agent, etc.) AND
[compression.conversation] compress_typesis non-empty.
Per-session dedup via a boundedsync.Mapon the Proxy struct
(cap 10k). Operator transparency contract preserved: at most ONE
additive stderr line per session, ZERO on clean configurations.
Switching toobserver start --recipe codex-variantsilences it.
(V7-2 lightweight) compression_cost_saved_usd_estUSD column now weights by the
row's realized input vs cache_read share rather than the
single-tierpricing.Inputmultiplier. Behavior change for
codex / OpenAI sessions: this column drops ~10× to match
realistic savings (codex is cache_read-dominant; cached input is
~10× cheaper than net input). Anthropic sessions unchanged.
Two new explicit-tier columns sit alongside for upper/lower
bounds. (V7-6)[compression.conversation.stash] max_total_mbdefault 256 →
1024 MB. Code-agent workloads churn large file reads at a rate
that fills 256 MB fast; the new cap better matches typical
working sets. Opt-in stash is still default-off so existing
deployments see no disk-usage change unless they had explicitly
enabled stash. (V7-13 Gap 2 ii)
Documentation (v1.7.6)
docs/v1.7.6-codex-friendly-compression-plan-2026-05-30.mdplan
doc captures the per-item scope, checklist, decisions, and
rationale (untracked persistent, mirrors the v1.7.5 plan-doc
pattern).scripts/release.shcarve-out 3-site sync extended with
docs/codex-compression-recipe.md+docs/recipes/*.toml.
Downloads
Pre-built binaries for each supported platform are attached below. Linux variants bundle antigravity-bridge.exe next to the observer binary for WSL2 users of the Antigravity adapter.
| Platform | Asset |
|---|---|
| Linux x86_64 | observer-v1.7.23-linux-x64.tar.gz |
| Linux arm64 | observer-v1.7.23-linux-arm64.tar.gz |
| macOS x86_64 (Intel) | observer-v1.7.23-darwin-x64.tar.gz |
| macOS arm64 (Apple Silicon) | observer-v1.7.23-darwin-arm64.tar.gz |
| Windows x86_64 | observer-v1.7.23-win32-x64.zip |
Verify with sha256sum -c SHA256SUMS (or shasum -a 256 -c SHA256SUMS on macOS) from the directory containing the downloads.
Also available via npm: npm install -g @superbased/observer@1.7.23
Org server (Docker)
The self-hosted org server ships as a Docker image and as per-platform observer-org-v1.7.23-* archives (attached below).
docker pull ghcr.io/marmutapp/observer-org:v1.7.23The image is keyless-signed with cosign. Verify it:
cosign verify ghcr.io/marmutapp/observer-org:v1.7.23 \
--certificate-identity-regexp 'https://github.com/marmutapp/superbased-observer-private/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comSupply chain
CycloneDX SBOMs are attached: observer.cdx.json and observer-org.cdx.json.
SLSA Level 3 build provenance for the binaries is attached below as a *.intoto.jsonl attestation. The build runs on the private origin repo, so pass that as the source when verifying an extracted binary with slsa-verifier v2.7.0 or newer (older versions fail with unexpected tlog entry type: expected intoto:0.0.2, got dsse:0.0.1):
slsa-verifier verify-artifact ./observer \
--provenance-path *.intoto.jsonl \
--source-uri github.com/marmutapp/superbased-observer-private