Skip to content

Add the Work page: spend by kind of work, labeled locally with Gemma - #63

Merged
bhavsarpratik merged 135 commits into
mainfrom
feat/work-insights-v2
Oct 26, 2026
Merged

bhavsarpratik merged 135 commits into
mainfrom
feat/work-insights-v2

Conversation

@bhavsarpratik

@bhavsarpratik bhavsarpratik commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

What this adds

A new Work page (⌥6, macOS only) shows what your coding-agent spend went into, whether it worked, and where a cheaper model or lower reasoning effort would likely have done. Google's Gemma 4 E4B, running in Ollama on your Mac, labels each request with its kind of work, area, and complexity, and flags follow-ups where you pushed back. Each request carries its share of the session's cost. Prompts never leave the machine; only salted keys, labels, and counts are stored.

How it works, rule by rule: specs/WORK_INSIGHTS.md. Evaluation history: specs/2026-09-30-work-insights-design.md (iteration 13 covers Gemma).

How to use it

  1. Check out this branch and run ./scripts/install.
  2. Turn on Settings → Work insights. Setup runs in the background:
    • It reuses your Ollama 0.34+, or installs a pinned, signature-checked Ollama 0.34.4 under Token Meter's folder (no sudo).
    • It downloads Google's official 4-bit model, gemma-4-E4B_q4_0-it.gguf (Apache-2.0, about 5 GB, pinned by commit and SHA-256), adds it to Ollama as token-meter-gemma, and deletes the download.
    • It needs about 11 GB free while it runs. The model uses about 5.5 GB of memory while it labels.
  3. Labels fill in at 5 model calls a minute by default. Labeling pauses on battery and when the Mac is busy, and the Work page shows progress.

What the page shows

  • Right-sizing: three cards (Model, Reasoning, Pushback) with estimated savings, plus spend by model tier and reasoning effort.
  • Where the spend went: spend by area, on the day it happened, with a trend line per area.
  • How sessions ended: accepted, recovered after pushback, or ended on pushback.
  • Session tags: such as Marathon, Long thread, Big spender, Subagent team, Rescued, and One-shot.
  • When you work, Pushback over time, Cost per resolved task, and Model choices.

Every bar, tag, and suggestion opens the sessions behind it. The page uses the shared design system from main: tokens, type, weight, radius, and spacing scales.

Memory safety

Labeling never pushes a Mac into swapping on its own account:

  • The model is loaded only when free memory covers it plus a margin of 2 GB (or a tenth of the Mac's memory, if larger).
  • Before every model call, Token Meter checks memory. On critical pressure, or when free memory drops below the margin, it unloads the model and waits ("Waiting: memory is low").
  • If memory stays short, the wait doubles from 1 to 15 minutes, and labeling resumes on its own once memory frees up.
  • Macs with less than about 10 GB of memory never download or load the model, and the page says why.

Also in this branch

  • Models page: a sortable Pushback column. It uses the same rule as Work: each request is credited to the model that answered it.
  • Current sessions: a session counts as live from its last model reply, not from its trace file changing. Opening an old session no longer makes it live or triggers a notification.
  • Sessions summary: "vs yesterday" always shows a number on partial days, and the estimate suffix is gone from the spend value.
  • Performance page: Work appears in its sidebar, and its ⌥ shortcuts match the dashboard.
  • Migration: saved settings that name Winnow or Jet move to token-meter-gemma. The Work page asks before the new download, and Token Meter's own Ollama removes the retired models.

Label accuracy

Real local requests from three session-disjoint sets. Gold labels were set blind, and the data stays private on the developer's machine. Every setting was fitted on dev only.

Test / held-out Winnow-E4B (previous) Gemma 4 E4B QAT (this PR)
Work type 76% / 85% 76% / 82%
Area 71% / 75% 77% / 80%
Complexity, exact 64% / 75% 69% / 74%
Pushback F1 0.83 / 0.86 0.80 / 0.69

Pushback on the held-out set is the remaining gap. The pushback decision point keeps reported rates close to the truth: 22% flagged against 18.5% real on test.

Testing

  • Full suite: 1583 tests OK. The design-system contract passes, the embedded JS parses, and py_compile and git diff --check are clean.
  • High-risk route on 20f7758: an independent tester and two reviewers (correctness, and resource safety/privacy) pass. The reviews caught and closed a reload loop in the memory guard.
  • The tester ran the real classifier against Ollama with a simulated memory budget:
    • Labeling completes when the loaded model leaves only modest free memory.
    • Critical pressure unloads the model.
    • The same script reproduces the reload loop on the earlier commit.
  • The prompts the app sends are byte-identical to the evaluated ones: 1,040 request prompts and 780 pushback prompts.
  • Google's pinned file gives exactly the same log-probabilities as the Ollama build used for tuning.
  • Installed and running from 20f7758: /health and /menubar OK, and the runtime matches the source. 1bb98e8 after it only removes a stale diagram from the spec.

Not verified

  • Browser checks of the Work page after the design-system port, and of the new memory messages, at 1440 and 1024 px.
  • A fresh-machine setup end to end (download, signature check, LaunchAgent) is covered only by tests with faked downloads.
  • Real behaviour under memory pressure is covered by simulation and unit tests, not an actual low-memory Mac.
  • Relabeling a long history with the new model takes hours at the default pace.
  • Linux and Windows: Work insights stay disabled.

🤖 Generated with Claude Code

bhavsarpratik and others added 30 commits September 30, 2026 18:28
Label sessions locally (area, work type, complexity, pushback) with Jet in
the user's Ollama so the new Work page can show what spend went into:
monthly allocation, workstreams, cost by work type, rework, and model
right-sizing. Opt-in, paced, pausable, loopback-only; labels persist, text
never does.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Keep subagent parents in Work, drain and attribute the backlog correctly,
retry failed items, pace and gate every model request, re-check the model
version, refuse cloud Ollama models, pin localhost to 127.0.0.1, key turns by
position only, create nothing until enabled, verify download hashes, and
describe exactly which text is read.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Clear the wake event after each pacing sleep so queued work cannot turn the
rate-limit wait into a busy loop, keep future retry rows when a session is
re-parsed, re-check the model whenever the endpoint or name changes, recreate
incompatible ledgers instead of dropping tables in place, and pin the model
download to a commit with every file hash-verified.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Treat lock, open, and I/O errors as transient storage errors instead of an
incompatible ledger, record failures in memory before scheduling their retry,
fence intake against a concurrent clear, count retries per item, check the
local model entry before every item, and stream download hashes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Read the clear generation before computing salted keys and fence again after
the ledger is recreated, re-check the local model entry before every request,
keep setup state while the queue is empty, schedule one retry per item, and
reopen and requeue after a storage failure.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Show session outcomes with cost per outcome and an estimate of spend after
the first pushback, pushback by turn position, model fit by work type, and
evidence-gated "What stands out" cards. Report failed label deletes and retry
them before reopening, throttle backlog refills, and fence backlog writes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Write a content-free marker before deleting labels so a failed or
interrupted delete is finished before any reopen, including after a restart
or while the feature is off; always delete the files even when the ledger is
closed. Classify outcomes only once the last follow-up is labeled, add an
Unclear outcome, align headline denominators with their modules, mark partial
months, memoize per-turn lookups, and describe the new insights in the README.

Co-Authored-By: Claude Code <noreply@anthropic.com>
… queue state

Record a delete request in memory before writing the marker so a read-only
directory cannot bring labels back, serialize the delete retry with clear,
report a successful delete whose reopen failed as a storage error, retry and
show pending deletes while no service is running, classify session outcomes
once no work is queued or backlogged for them, and read memoized per-turn
labels from a single snapshot.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Open the sessions behind any Work bar, row, or cell (and via
#work-sessions deep links), add reasoning-effort right-sizing, cost per
resolved session, and a spend-up/value-flat warning. From the live-label
audit: give Codex follow-ups the preceding assistant message, strip runtime
wrappers, skip continuation and attachment-only turns, label sessions from
their first substantive turn, narrow ops/debug options, lower the work-type
Unclear cutoff, and version prompts so stale labels keep classifying until
relabeled. Declare Work state before the initial route so cold loads work.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Replace the stacked horizontal allocation and workstream tables with period
KPIs and a mirrored monthly column chart (spend, turns, or sessions by area
above the line; outcomes below) whose segments open the matching sessions in
All sessions behind a removable Work filter. Add a pushback trend with a model
filter and per-session cost bars. Key labels per trace file, since one Codex
session id can span several rollout files, and recreate the ledger for it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Replace the estimated rework-spend KPI with spend on unresolved sessions,
limit headlines to three change-or-opportunity cards, show judged outcomes
only below the ledger, and re-slice the lower page into value by kind of work
(cost per resolved session and resolved rate), model choices against the
usual model, and a ranked right-sizing opportunities list, with the full
grids behind toggles.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…view

Skip model choices when the usual model lacks labeled evidence, label sessions
from the current opener, count only classifiable turns toward outcomes, accept
non-Latin requests, compare KPIs with the preceding calendar months, ignore
stale Work filter responses, make drillable cells keyboard buttons, and reject
repeated or impossible drill-down parameters.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…dows

Session labels now come from the recorded opener turn (by position), so a
stale fallback opener written later cannot win; queued work for turns whose
role changed is dropped. The current window is the N calendar months ending
this month, labeled sessions with never-labeled follow-ups are Unclear, and
drillable table cells use inner buttons instead of role=button cells.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Codex resumes and spawned subagents share a logical session id, so the
page collapsed 994 trace files into 511 rows, understating cost, and
delete-by-id could trash a different file. Rows, routes, Spend links, the
Work filter, and deletes now use each file's unique session key.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Check the read-only provider on the exact trace that would be trashed,
keep the All sessions highlight when a session is opened by logical id,
record openers for sessions past the backfill horizon so older labeled
sessions keep their outcome, and stop counting pushback labels on turns
before a moved opener.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A recorded opener now hides other turns' session labels only once it has
its own label, so sessions past the backfill horizon keep theirs. The
All sessions highlight falls back only for the pinned session, and a
read-only session split across files reports read-only before ambiguous.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A permanently failed opener now settles its session questions, so an
earlier fallback turn's labels no longer stand in for it. Also cover a
split session that mixes read-only and deletable files.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Work History now offers 1 day, 1 week, and 1 month with daily buckets
compared against the span before. Model choices becomes a scorecard plus
paired usual-versus-alternative bars, right-sizing shows spend by tier and
effort with mismatched segments and an estimated saving, charts use the
product's validated hues, and the two-up cards align.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Mismatched spend now comes from unique sessions on the server, so a
session flagged for both tier and effort no longer counts twice. The
1-month trend keeps all 30 days, cards state they cover sessions started
in the period, and the saving note, lowest badge, legend, and trend
height behave as labelled.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The saving note no longer reports an absence when model price tiers are
unavailable, the mismatched share names its complexity-labeled base, KPI
tooltips state they cover sessions started in the period, tied cheapest
models all get the lowest badge, and flagged-spend tests cover high-impact
and unknown-tier sessions.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Replace the mirrored monthly column chart with a "Where the spend went"
bar list and a single "How sessions ended" bar, remove the headline
cards, the paired model comparison, and the measure toggle (and their
backend aggregation), shorten copy, and use the dashboard font.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Codex resumes and spawned subagents share a logical session id, so All
sessions resolved clicks, live markers, and deletes by id against the most
active file, and delete-by-id could trash a different rollout. Rows, routes,
Spend links, and deletes now use each file's unique session key; an id-only
delete spanning several files is refused as ambiguous, and the read-only
provider check runs on the exact trace that would be trashed.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A 4B local model at 20 requests a minute keeps low-end laptops busy, so
new installs start at 5 (now a Settings choice); saved paces are kept.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Resumed rollouts and subagents share a logical id, so the header could show
a sibling file's title; it now uses only the row with the same trace key.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Work insights need a local Ollama model and are supported on macOS only:
other platforms report them off, refuse to enable them, hide the Work tab
and Settings card, and the setup script exits early. Work gets Option+6
and the rail renumbers top to bottom (Git 7, Learn 8, Tools 9, Settings 0).
Right-sizing names each flag instead of saying "looks mismatched".

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Only the 60 most recent rows ride along with the session state, so older
traces fell back to a generic title; the header also checks the All
sessions inventory, still matching by trace key.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The header now reads the inventory, and the initial route rendered it
before the declaration ran.

Co-Authored-By: Claude Code <noreply@anthropic.com>
# Conflicts:
#	AGENTS.md
#	page.html
#	specs/AGENTS.md
#	tests/test_meter.py
#	token_meter/app.py
# Conflicts:
#	specs/ARCHITECTURE.md
#	tests/test_meter.py
#	token_meter/app.py
bhavsarpratik and others added 29 commits October 13, 2026 18:08
A short reply such as "go on" released the question before it too early.
Requests held on purpose no longer count as left to label, a turn in retry
backoff is counted once, and a busy recheck measures the whole wait.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A stale hold (session deleted, row dropped, or quiet time passed) could hide
another session's pending work from the status.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ace its actions

Save stayed live with nothing to save, and the action row had no layout rule,
so mixed-height buttons ran together. Area actions, status, and Save now sit
in one row, with pause and delete in a separate row below.

Co-Authored-By: Claude Code <noreply@anthropic.com>
An empty or partly filled Work page read as broken. A notice under the status
card now says how much is labeled and that the charts update as labeling
continues, and hides once everything is labeled.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…otice

A toggle saved at once and reset unsaved model, address, and area edits. The
notice no longer reads "100% ... partial" during a relabel, leaves the request
count to the status card, and is not a second live region. The status line
says when changes are unsaved.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The "not saved yet" note now clears when edits are undone, follows the on/off
message too, and Restore default areas keeps an unsaved model or address.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A fixed 5 a minute left a long backlog even when the Mac sat idle. Automatic
labels as fast as the model answers while CPU time is quiet and slows to 5 a
minute while the Mac is in use. A 5 saved under the old default becomes
Automatic; other chosen paces stay as fixed caps.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…reen

With the battery pause off, Automatic ran at full speed unplugged. The pace
hint is now visible text tied to the select, and the copy says what is
measured: CPU time, at most 60 a minute.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Main removed numbered page shortcuts, so the Work tab, rail link, and
palette entry drop Option+6 and its tests follow the shortcut-free contract.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
OpenCode's V2 store keeps the user's typed request in the session_message
row's own data.text; message.data.content does not exist in that schema, so
Work classified every OpenCode session as having no request text.

Measured on a real database: 332 of 402 sessions in a six-month window were
"No request text" before this change and 11 after, with Work requests rising
from 544 to 1797. The part table is V1's -- all 143697 of its message ids
resolve to V1 message rows, so joining it to V2 messages only ever worked by
id collision -- and 9762 of its text parts are synthetic echoes. Reading
data.text instead also keeps context the stale part rows had lost, such as
the migration notice on 228 migrated prompts.

A usable string content still wins, and the fallback is keyed on that shape
rather than on the schema: a user message with missing, null, non-string or
blank content falls back to its own text field, in either schema. On real V1
rows that fallback is inert, because their data carries neither key.
Applies the review findings on top of the V2 data.text fix. No change to
what the fix resolves; these close the cleanup items around it.

- Force work_insights_enabled in every test that calls summarize_legacy, so
  the file no longer depends on the host's settings file.
- Pin the V1 behaviour change the fix also made: a user message with no
  usable string content now resolves through its own text.
- Rename the reused `content` local to `request_text`, cap the prompt before
  compacting it, and correct the module docstring, the inline comment, and
  the architecture map so both read paths are described accurately.

Tests: 1683 pass, 17 skipped.
…ee claim

The map stated both OpenCode read paths stay content-free while, three lines
above, naming data.text as where a user message's typed request lives. Scope
the claim to what actually holds.
Pi sessions were all "No request text" because the adapter never handed
its typed requests (or tool evidence) to Work insights. Read the user's
own request text from Pi user messages, gated on work_insights_enabled,
and pass {ts, text, model} turns plus content-free tool actions through
the shared attach helper.

Request text passes Pi's existing redaction gate (_has_secret_token,
_is_host_like, _has_account_at); a request that trips any check is
dropped whole, never partially scrubbed. Text is capped at
WORK_REQUEST_TEXT_CAP before the adapter compacts it to a short preview,
and only the leading slice is read, matching the OpenCode adapter.
Actions carry only a tool kind and timestamp, because Pi's adapter keeps
no tool arguments.
OpenCode's storage migration appended a machine-generated
"[Attachment unavailable after migration: ...]" notice to user message
text when an attachment payload could not be carried across, and the
notice is stored verbatim in the V2 message row's `text`. The Work
insights classifier therefore spent part of its budget classifying
OpenCode's own migration notice rather than the request.

Strip the notice from the request text read for the Work insights
queue, before the 90-character preview is taken. Stripping first is
what makes the fix work: the preview keeps only the first 90
characters, so a leading notice would otherwise be all the classifier
ever sees. A message left with nothing of the user's own emits no
turn, because scaffolding alone is not a request.

The pattern requires the parenthesised media type that the migration
emits. That suffix is what separates a real notice from a user merely
quoting the phrase, and it also lets the payload span path characters
such as "[slug]" that a first-"]" bound would cut short. It is matched
anywhere on the line because the migration appended it inline as often
as on its own line.

Only this marker is stripped, because only this marker has established
provenance. Measured read-only over a real database: 396 occurrences
across 229 V2 user prompts, 64 of them inside the preview the
classifier sees, all of which now resolve to the user's own words.

Deliberately not stripped, having checked the same corpus and the
filesystem: "<system-reminder>" wrappers (zero genuine occurrences),
"<path>"/"<content>" file injection (zero genuine occurrences), and
the leading "<tag>" prompt templates such as "<objective>" and
"<planning_context>", which are the user's own command templates and
whose tag text is the request itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Work insights being off must not read prompt text. The OpenCode summary
read, stripped and compacted every user request on each recompute even
with the feature disabled, then discarded the result. Pi already skips
this work when the flag is off, so the two adapters disagreed about what
"off" means and OpenCode did regex and compaction work for a consumer
that was switched off.

Wrap the read in work_on so the adapter honours the documented contract
that adapters skip Work-only extraction unless Work insights are on, and
no request text is touched on the off path. last_user_ms still updates
because it is timing metadata rather than text. Behaviour with the feature
on is unchanged.
…uest

The gate test mocked _strip_harness_text, which only proves the strip step
was skipped. The request is read out of the message row before that, so a
build that read `content` or `text` and then discarded the result would
still have passed.

Wrap the decoded row instead, so the first request-field access fails, and
assert the summary still completes with its normal turn count so the gate
is shown to cost no other evidence. Verified red against a partial gate
that reads both fields but keeps strip and append behind the flag, and
again against a fully ungated block.
The attachment-migration notice pattern used a greedy `[^\n]*` payload run,
so when two notices landed on the same line a single match spanned from the
first notice's "[" all the way to the last media-type suffix, silently
deleting the user's own words between them. Real read-only measurement over
the local OpenCode database found one prompt where this removed 14 extra
user characters. Over-stripping is data loss, worse than the injection being
removed.

Make the payload run non-greedy so each match ends at the first media-type
suffix on its line and covers exactly one notice. The `[^\n]` class keeps
matches on a single line, and the run still reaches the media type rather
than the first "]" so paths containing "[" stay handled.

Add regression tests for user text between same-line notices, notices on
separate lines, a payload path containing "[", same-line scaffolding-only
text, and a user quoting the phrase without a media-type suffix.
The OpenCode adapter queued a user's typed request for Work insights without a
redaction gate, so a credential-shaped token, a host, or an account address in
an OpenCode prompt reached the classifier queue. The Pi adapter already drops
such a request whole.

Move the credential and host predicates into domain/redaction.py, shared by
both adapters, and add a token-bounded account/host matcher. OpenCode now drops
a whole request, before the 8 KiB cap and the compaction, when it trips any
check. The account matcher is bounded to the address token after each "@"
rather than Pi's whole-string remainder, so ordinary prose such as
"@scope/pkg@latest" is kept; Pi's matcher and observable behaviour are
unchanged.
Three comments claimed OpenCode "passes the same redaction gate Pi
applies". It does not, and the difference is the point a reader needs. The
credential and host predicates are the shared ones Pi runs; the account
check is the shared token-bounded matcher, which keeps @scope/ ids and
version tags, while Pi keeps its broader matcher that also fires on any "@"
followed by later prose. The two adapters therefore drop different
requests, and saying otherwise hides the asymmetry in a privacy boundary.

Also drops a stale gate comment that sat inside the branch it described,
and names the OpenCode adapter in the Pi note instead of "newer
adapters". No behaviour change.
Pi kept its own account matcher, which matched a version or host pattern
against the whole remainder of the string after every "@". Any request whose
tail happened to read like a version was dropped whole, so the two adapters
did not agree on what to keep: measured on the local Pi store it dropped 14 of
60 prompts, 9 of them benign ones such as a scoped package install, a
mid-sentence model namespace, or a package version.

Use the shared token-bounded matcher at both Pi call sites, the request-text
gate and the subagent model-id path, so OpenCode and Pi drop the same
requests. The credential and host predicates are untouched: they stay the same
shared functions, called at the same places in the same order, and the gate
still drops whole before the cap and the compaction.

Sharing the matcher loosens the model-id path for a bare-word "@" suffix,
which is indistinguishable from a model tag. See the report: one shipped
sanitizer-table case, user@host, now follows that tag shape.
0c60fc8 pointed the subagent model-id sanitizer at the shared token-bounded
matcher, which is right for request text but wrong here: a model id's "@"
introduces a version, a tag, or a leading namespace, never an account, and the
shared matcher cannot tell a bare-word tag from a bare-word account. It accepted
"user@host", which the shipped sanitizer-table privacy invariant rejects, so the
suite was red.

Restore an explicit, local allowlist scoped to the model-id path only: after a
non-leading "@" only an 8-digit date, a version with at most three numeric
components, or the tags latest/default may follow, and a leading namespace such
as @cf/meta/... stays valid. The request-text gate keeps the shared
has_account_at, so the benign prompts 0c60fc8 recovered stay kept. Credential
and host predicates are untouched: same call sites, same order.
Keep both platform power probes: the PR's power_source for Work insights and
main's power_constrained/CPU/memory probes for cold indexing. Cold-index
workers now hand captured work turns back on the row so parallel indexing
still feeds Work insights; the parent pops them before caching.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Brings in the Splunk by Cisco branding (#93), the trace-path privacy fix
(#89), and the cross-session perf work (#88, #90).

- All Sessions rows now key on the server-sent trace key, since #89 removed
  the path the branch used to hash.
- Regenerate the agent_usage parity digests for the new model_runtimes and
  model_days fields; every pre-existing field still matches main's golden on
  Python 3.9 and 3.14.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@bhavsarpratik
bhavsarpratik merged commit 4fdd4d1 into main Oct 26, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants