Skip to content

v1.7.23

Choose a tag to compare

@marmutapp marmutapp released this 01 Jun 18:14
· 19 commits to main since this release

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 CompressTypes restored 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.toml recipe → compress_types = ["json","logs","code"]
    in both docs/recipes/ and internal/config/recipes/ (mirrored
    to satisfy TestRecipes_InternalAndDocsAreIdentical). Inline
    comments capture V7-23's empirical baseline and the ENABLE_TOOL_SEARCH
    operator requirement.
  • claude-code.toml recipe — 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 to TestDefaultCompressTypesIsJSONLogsCode; assertion
    flips from len(got) == 0 back 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 from len == 0 to ["json","logs","code"] plus
    a new check that Stash.Enabled is 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-code recipe behavior reverts to the v1.4.40+
    semantic — per-type compression on. Other knobs unchanged.
  • The codex-variant recipe is unchanged — still []. The V7-21
    $0.270 measurement holds; codex operators see no behavior change.
  • The codex-safe recipe is unchanged — still ["logs"].
  • Operators who want v1.7.22 passthrough behavior opt out by
    setting compress_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 CompressTypes flips 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 through marshalEnvelope (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.toml recipe → compress_types = [] in both
    docs/recipes/ and internal/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 to TestDefaultCompressTypesIsEmpty; assertion flips from
    {json,logs,code} to len(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"] to len == 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-code recipe 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-variant recipe is unchanged — still []. Codex
    operators see no behavior change from V7-21's $0.270 cost mean.
  • The codex-safe recipe 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
    setting compress_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 disables ToolSearch deferred MCP
      loading whenever ANTHROPIC_BASE_URL is 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's compress_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 runnable scripts/cost-check.sh
      that reads observer's own /api/cost endpoint to show actual
      per-tool $/turn over a chosen window. Discoverable from
      docs/architecture.md (mcp row) and docs/claude-code-smoke.md
      (cost-tuning callout). Operators install the skill by symlinking
      docs/skills/observer-cost-tuning/ into ~/.claude/skills/;
      automated install via observer init --claude-code --skill is
      tracked for v1.8.x.

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 in marshalEnvelope. 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 from compress_types. Whenever a
    tool entry's description tail or parameters.examples shrank — i.e.
    on every real tool schema — the early-return fast-path was bypassed,
    serializeAnthropic / serializeOpenAIResponses re-marshaled the
    envelope, and json.Marshal reordered 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 default compress_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 behind allow[types.Tools].
    Default compress_types stays ["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 via compress_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" in compress_types) → tool-defs trim
      skipped → body forwarded byte-identically → examples survives,
      description tail survives, fast-path taken.
    • TestPipeline_ToolsDefsGated_InCompressTypes_DoesTrim — opt-in
      recipe ("tools" added) → tool-defs trim fires as before →
      description tail trimmed, examples stripped.
  • V7-21 compendium row in
    docs/v4-codex-compression-recipe-and-issues.md documenting the
    finding, the +14% net-input vs the +10% cost ratio, the cache-hit
    parity, and the BC blast radius.

Fixed (v1.7.21)

  • compressToolDefinitions gated behind compress_types at all
    three pipeline sites
    (internal/compression/conversation/ pipeline.go:358 Anthropic, :447 OpenAI Chat, :509 OpenAI
    Responses). Pre-v1.7.21 the trim ran unconditionally; post-v1.7.21
    it only fires when compress_types includes "tools". The
    allow := buildAllow(p.cfg.CompressTypes) value is also hoisted
    out of the immediate compressToolResults call so each pipeline
    path computes it once.
  • TestPipeline_EmptyCompressTypes_SkipsAllPerTypeCompression
    extended to assert no tools mechanism event
    — the V7-19 +
    V7-21 combined contract. Pre-v1.7.21 this test allowed tools to
    fire under empty compress_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's tools field 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 riskcompressToolDefinitions'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
    via compress_types = [..., "tools"].
  • The default CompressTypes list in internal/config/config.go: 703 is 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 at monthStart instead 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 unexported now func() time.Time field defaults to
    time.Now().UTC(); tests override to pin date-sensitive handlers.
    Only the analysis-headline handler is wired through s.now() in
    this PR (the other 7 time.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 UTC via the new Server.now override 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
      into priorMonthSameDayUSD and the assertion =0.30 fails
      with 0.9000…).

Headline (v1.7.19): CriticalbuildAllow([]) 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 returns true for 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 under compress_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_types unset / 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.Run path through buildAllow(p.cfg.CompressTypes), and
    every shipped recipe + default sets a non-empty compress_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 under buildAllow's gate
    and continue to
    fire regardless of compress_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-19buildAllow([]) 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 full tools
    array on every turn, so before this fix compression_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_est in
    cost.Engine.Summary: savedBytes is now floored at 0 before
    flowing into TokensSavedEst and 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_bytes on the row are preserved
    unchanged so operators can still see wire inflation via the JSON
    summary or direct SQL queries against api_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-
    negative cost_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 SAVED column 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_hash was 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_hash wiring gap

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.jsonl with {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.AvgLatencyMS field on the JSON summary (omitempty when
      zero — JSONL-only buckets emit 0 → field absent).
    • LATENCY_MS column 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 can rm the file at any time.
  • V3-5: LATENCY_MS column shows only when populated; CLI layout
    unchanged for JSONL-only summaries. AvgLatencyMS uses omitempty
    in JSON — pre-v1.7.17 consumers reading specific fields see no drift.
  • V4-2: --filter is 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.go is the single source of truth.
  • Live regex parser (item 2 + 4 foundation): new
    internal/codegraph/livesym package with Parse(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 -1 in 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-result degraded: true; envelope warning
    codegraph_unavailable. Unsupported extensions return empty matches
    • regex_fallback_language_unsupported warning — 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 new index_lines
    ({start, end}) and live_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_nearby opt-in (item 1 — naming overlap with V7-17 item
    1 unintentional)
    : get_symbols requests can set include_nearby: N (>0) to get nearby_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.

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: true flag stays semantically identical. warnings
    is additive context, not a replacement.
  • index_lines + live_lines are populated ONLY when they differ;
    agreement = both fields omitted.
  • nearby_symbols opt-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/jobobject package:
    • AttachProcess(*exec.Cmd) (io.Closer, error) — creates an
      anonymous Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
      and assigns the started process to it. cmd MUST have been
      Start()'d already (its Process populated).
    • 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) call Close() to fire
      KILL_ON_JOB_CLOSE on every assigned process immediately.
  • cmd/observer/codex.go split child.Run() into
    child.Start() + child.Wait() so AttachProcess can 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 refuse CreateJobObject or AssignProcessToJobObject,
    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.ExitError unwrap 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_audit table:
    • 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_requested values, 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 <= 0 as a footgun.
  • New observer mcp-audit CLI (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
    • --since accepts Go-style duration strings (30m, 24h,
      168h for 7d, etc.); empty/0 = all time.
    • purge requires both --older-than and --yes so a
      misclick on the flag set doesn't wipe history.
  • New printListRows / printStats / printTopPaths helpers use
    text/tabwriter (matching observer 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 under observer mcp-audit. Older operators not setting
    any new flags see no behaviour difference.
  • audit.Stats / audit.TopPath / audit.ListRow are new
    exported types in internal/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/stashalign package:
    • WriteSidecar(homeDir, stashDir) / ReadSidecar(homeDir)
      round-trip the active proxy-side stash dir via the
      .proxy-stash-dir.txt sidecar. 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 via filepath.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): after stash.New succeeds,
    writes the sidecar. Failures log Warn and continue — stash
    compression still works.
  • MCP server side (cmd/observer/serve.go): after stash.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.Detect now 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
    \A regex anchor. Negative exit codes and the three Wall-time
    suffix variants (Xs / X seconds / bare X) all recognised.
    (V7-3.)
  • compression_events.body_hash column (migration 031) — sha256-hex
    of the pre-compression body. Populated at the 10 single-body Event
    construction sites in budget.go, anthropic.go, and openai.go.
    Pre-v1.7.12 rows stay NULL and are excluded from the dedup query.
    Partial index idx_compression_events_turn_body_hash WHERE body_hash IS NOT NULL keeps 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.Client captures startup mtime and emits a one-shot
    warning on every subsequent query when the DB file's mtime has
    changed since Open. 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
    CountUniqueCompressions reader needs the column.
  • V7-13 Gap 5 (b) is observability-only — no query results change.
  • bodyHashHex takes 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_stashed accepts sha: string | string[].
    Array form returns the new {ok, responses: [...]} envelope with
    per-sha ok / 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_line slice 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
    path startLine == 0 && endLine == 0 is 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].features list (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-tool enabled = false always wins.
  • Per-tool kill switch:
    [intelligence.mcp.retrieve_stashed].enabled = false keeps
    proxy-side stash compression active while denying the agent the
    retrieval surface (asymmetric-trust scenarios).
  • V7-8 transparency: observer serve startup 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_audit row per resolved sha.
    N shas in an array call → N rows. path_requested encodes the
    sha as stashed://<sha> so WHERE 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: string with no other new
    parameters get the v1.7.10 response shape byte-identical
    (including pretty-print indentation). Pinned by
    TestRetrieveStashed_BackwardsCompat_SingleStringByteIdentical
    • _MaxBytesNoNewKeys.
  • Options.RetrieveStashedDisabled is 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.Features field 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 Chtimes mtime-bump-on-read +
    raised MaxTotalMB=1024 default 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. kind enum: 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
    with candidates: [...] so the agent can immediately re-call
    with a pinned fqn. (V7-12, third of four.)
  • BFS via single recursive CTE in SQLite — cycle-safe via
    UNION dedup 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 []Reachable plus truncated flag) and
    CountEdgesByKind(ctx, kind) (used to detect "CONTAINS not
    populated by codebase-memory-mcp" and surface a degraded: 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.Options gains GetRelations + 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 name and fqn to 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_line ASC → fqn ASC → file ASC
    id ASC. ambiguous: true fires when len(matches) > 1 and no
    fqn pinned; disambiguation_hint carries a literal recipe
    (Use fqn (e.g. "Editor.handleClick") to ...). is_exported
    factor deferred (codegraph schema lacks the column).
  • include_relations: true adds callers_count/callees_count
    totals + top-N callers/callees lists 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: true when
    codegraph.Stale(file) returns true. Recovery suggestion in
    reason directs the agent to get_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 rich Caller rows
    (name, fqn, kind, file, start_line) for V7-12 include_relations;
    the existing CallersOf([]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_exported deviation
    rationale, body-cap algorithm, common flows, audit-log queries.
    Public-doc carve-out via scripts/release.sh (3-site sync).

Changed (v1.7.9)

  • cmd/observer/serve.go now wires mcp.Options.CodegraphClient
    from [compression.code_graph].path (with FindProjectDB(cwd)
    fallback). This Options field was declared but never assigned in
    serve.go pre-v1.7.9, so check_file_freshness / get_file_history
    silently returned empty structure: {} enrichment in production.
    Bonus fix bundled with v1.7.9 since get_symbols needed the wiring
    anyway.
  • Refactored internal/mcp/tools_get_file.go:readSlice into shared
    internal/mcp/fileread.go so get_file and get_symbols
    truncate identically. Wire-shape unchanged — getFileResult.Lines
    serializes as the same {start, end, total} JSON.
  • internal/mcp.Options gains GetSymbols, GetSymbolsEnabled
    fields. Backward-compatible: nil values plus
    GetSymbolsEnabled: false keep 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].enabled is
    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, calls get_file for 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_audit SQLite table (migration 030) + writer package
    internal/mcp/audit. Every get_file call (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 via sqlite3 ~/.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] enabled switch (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.md operator guide: config
    reference, tool schema, defense-layer rationale, audit-log
    example queries, accepted-risk discussion (TOCTOU, symlink
    semantics). Public-doc carve-out via scripts/release.sh.
  • Cross-reference in docs/codex-compression-recipe.md linking
    the v1.7.7 marker enrichment to v1.7.8's get_file retrieval
    path for codex-variant model operators.

Changed (v1.7.8)

  • internal/mcp.Options gains GetFile, GetFileEnabled, and
    AuditWriter fields. Backward-compatible: nil AuditWriter
    defaults to audit.NewNoopWriter() so existing tests that
    construct mcp.Server without 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 HintedCompressor interface 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 basic Compress when 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 in absPath (functions, methods,
    classes, interfaces, types). Sorted by start_line ASC for
    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
    mirroring WithStash. 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] path config 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.Open no longer passes immutable=1 in 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. Plain mode=ro uses
    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, tail knobs for LogsCompressor truncation.
    Default 200/100/100 (unchanged from the prior hardcoded
    constructor). Set max_lines = 0 to 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 = []; *-codex family).
    Recipes live at internal/config/recipes/*.toml (embedded) with
    byte-identical mirrors at docs/recipes/*.toml; a sync test
    pins both copies in lockstep.
  • docs/codex-compression-recipe.md operator-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 in observer 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_types is non-empty.
    Per-session dedup via a bounded sync.Map on the Proxy struct
    (cap 10k). Operator transparency contract preserved: at most ONE
    additive stderr line per session, ZERO on clean configurations.
    Switching to observer start --recipe codex-variant silences it.
    (V7-2 lightweight)
  • compression_cost_saved_usd_est USD column now weights by the
    row's realized input vs cache_read share rather than the
    single-tier pricing.Input multiplier. 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_mb default 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.md plan
    doc captures the per-item scope, checklist, decisions, and
    rationale (untracked persistent, mirrors the v1.7.5 plan-doc
    pattern).
  • scripts/release.sh carve-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.23

The 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.com

Supply 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