Status: normative build specification, as implemented (#35). Rust-first (cce-ruby reconciles to the byte-pinned formats later). Additive — with the footer off (the default) every existing byte-pinned surface is preserved.
Renumbered from the v2.7 draft (the v2.7.x line shipped
cce update); functional content is the draft's, with the drift fixes noted in §9.
When an agent has cce mcp connected, the user has no terminal-native or in-conversation way to
see how much the agent leaned on CCE. The data already exists — every context_search writes a
cce.metrics/v1 search event with source:"mcp" — but today it surfaces only through the
browser dashboard (cce dashboard [--workspace]). Two gaps:
- No one-shot CLI answer. You can't ask, from a terminal or CI, "how many times did the agent
call CCE, and how many tokens did that save, since this morning?"
cce savingsprints the seven-bucket savings ledger but not the who-used-it / how-many-calls / which-queries view;cce dashboardneeds a browser and a running server. - Nothing in the conversation. The MCP tool result ends with
query_id+ arecord_feedbackhint and deliberately says nothing about savings, so the numbers never reach the user inline.
This track closes both — without computing anything new. Both features are projections of the already-recorded metrics event; neither changes a single recorded number.
- Pure projection — zero new accounting. Both surfaces render the existing
searchevent / aggregate. Turning either on must not change any recorded metric (served_tokens,baseline_tokens,tokens_saved,savings_ratio, the savings-layer buckets,by_source, …). A test asserts: same query, footer off vs on ⇒ an identical recordedsearchevent (the per-callts/id/latency_msvary run-to-run by construction and are set aside; every other byte must match). - Additive + byte-pinned. With
mcp.result_footer: off(default), the MCP tool-result bytes,conformance.json, and the MCP goldens are preserved byte-identical.cce usage's human render and itscce.usage/v1JSON are new byte-pinned goldens (they add surfaces; they move nothing). The two aggregate fields v2.8 adds (§2.5) are additive. - Deterministic + cross-engine identical. Both reuse the pure
aggregate(events, now, price)function, so cce-rust and cce-ruby produce identical numbers from the same log.nowis injected (wall clock only at the CLI edge; fixed in tests). Every new grammar line is a pure, byte-pinned function. - Same logs the dashboard reads.
cce usage --workspaceaggregates every member log and the workspace-root.cce/metrics.jsonl(thecce mcp --workspaceagent-search log), folded in once and guarded against a member whose path is the root — the exact rule shipped in #28.cce usageandcce dashboard --workspacetherefore report identical totals. - Offline + read-only. No network; nothing mutates state. Same posture as
cce dashboard/cce savings. - Off by default = context hygiene. The in-conversation footer is opt-in precisely because
printing savings into every tool result costs the agent's own context window. The default keeps
the tool result lean; the aggregate surfaces (dashboard,
cce usage) carry the numbers.
The CLI counterpart to the dashboard's agent-vs-human panel: a one-shot, greppable, CI-friendly answer to "how much was CCE used, by whom, and what did it save — over this window?"
cce usage [<dir>]
[--workspace] # federate: member logs + workspace-root log (#28 rule)
[--dir <dir>] # project/workspace root (default: cwd)
[--store <path>] # single-repo: metrics beside a specific store
[--metrics <path>] # single-repo: an explicit metrics.jsonl
[--since <when>] # window start; default: all time
[--source mcp|cli|all] # display filter; default: all
[--json] # emit cce.usage/v1 instead of the human render
--since <when>accepts either an ISO UTC instant/date (2026-07-01,2026-07-01T09:00:00Z; a bare date is midnight UTC) or a relative duration (90m,24h,7d,4w). Events withts < cutoffare dropped before aggregation;cutoffis derived from the injectednow. Malformed--sinceis a clear error listing the accepted forms (no silent all-time fallback), exit non-zero.--sourcefilters the display only: it narrows the human split lines and the human recent list to one source (climatches every non-mcpsource, the aggregate's rule). The JSON always carries both splits and the full recent list, with the filter echoed assource_filter, so a pipeline never loses data. An unknown value is a clear error.- Log resolution is identical to
cce dashboard: single-repo reads<root>/.cce/metrics.jsonl(or--store/--metrics);--workspacereuses the federated aggregation (federation::federated_metrics_json_since(members, Some(root_log), now, price, since)).
A compact block — window header, the agent/human split, the by-package table (workspace only), and
the recent queries. Pinned example (the committed test/fixture/usage/metrics_usage.jsonl,
all-time):
CCE usage — all time
agent (mcp) : 2 searches · saved ~16,500 tok (88%) · quality 0.79 · 58 ms avg
human (cli) : 1 searches · saved ~ 2,100 tok (81%) · quality 0.74 · 12 ms avg
recent (newest first)
mcp 09:58 "how does the payment flow create a new case" 5 hits ~8.6k saved
mcp 09:52 "where is the retry idempotency boundary" 5 hits ~7.9k saved
cli 09:00 "rrf fusion constant" 3 hits ~2.1k saved
Pinned rules:
- Header:
CCE usage — all time(no--since),CCE usage — last <spec> (since <cutoff ISO>)(relative,<spec>normalized lowercase), orCCE usage — since <cutoff ISO>(ISO form). - Split lines: one per displayed source, label
agent (mcp)/human (cli). Search counts and thousands-separated token counts are right-aligned to the widest displayed line (hence~ 2,100). Percent =round(mean_savings_ratio × 100); quality =mean_top_scoreto 2 dp; latency =round(mean_latency_ms)+ms avg. A source with no searches in the window renders a zero line (never disappears) unless--sourcefiltered it out. --workspaceinserts aby packagemini-table (the aggregate'sby_package) between the split andrecent:<package> : N searches · saved ~X tok (P%) · quality Qwith the same alignment rules and no latency column. Federated agent searches (from the root log) count in the split/headline but not inby_package— same honesty rule as #28.- Recent: newest first (the aggregate's order), at most 10 lines, then the pinned elision
… (N more; --json for all). Each line: source padded to 3,HH:MMfrom the eventts, the quoted query (longer than 44 chars ⇒ cut on a char boundary + one…) padded so the hits column aligns,N hits, and~<short> savedwhere<short>is the pinned short token form (< 1000verbatim;< 100kone-decimalk; else integer-kby floor). - Empty window ⇒ the header plus the pinned
no searches in this windowline, exit 0. - Every number is a pure function of the log ⇒ identical to the dashboard and to
cce savings.
A stable, versioned projection of the aggregate — totals, by_source, and by_package are
lifted verbatim from the aggregate value, so where they overlap with /api/metrics the shapes
and numbers are byte-identical (a re-shape, never a re-computation):
{
"schema": "cce.usage/v1",
"generated_ts": "2026-07-06T10:00:00Z",
"window": { "since": "2026-06-29T10:00:00Z", "until": "2026-07-06T10:00:00Z" },
"source_filter": "all",
"totals": { "searches": 3, "tokens_saved": 18600,
"mean_savings_ratio": 0.856667, "mean_top_score": 0.773333 },
"by_source": {
"cli": { "mean_latency_ms": 12.0, "mean_savings_ratio": 0.81, "mean_top_score": 0.74,
"searches": 1, "tokens_saved": 2100 },
"mcp": { "mean_latency_ms": 58.0, "mean_savings_ratio": 0.88, "mean_top_score": 0.79,
"searches": 2, "tokens_saved": 16500 }
},
"by_package": [ /* present iff --workspace; the aggregate's array, verbatim */ ],
"recent": [ { "ts": "2026-07-05T09:58:00Z", "source": "mcp",
"query": "how does the payment flow create a new case",
"result_count": 5, "tokens_saved": 8600 } ]
}window.sinceisnull(all time) or the cutoff instant;generated_tsandwindow.untilare the injectednow(not conformance-anchored; excluded from the byte-pinned body the same way/api/metrics'sgenerated_tsis).by_packageis present iff--workspace.recentis the aggregate'srecent_searches(≤ 20, newest first) re-shaped to the five fields above — unfiltered by--source.
| Surface | Question it answers | Form |
|---|---|---|
cce dashboard [--workspace] |
live, visual, all panels | browser + local server |
cce usage (new) |
how much / by whom / which queries, over a window | one-shot terminal + JSON |
cce savings [--json] |
the seven-bucket savings ledger + $ |
one-shot terminal + JSON |
cce usage is the terminal answer to the user's question; it does not replace either neighbour.
$ cost stays in cce savings (deliberately: one money number).
Two fields join the pure aggregate (and therefore /api/metrics), both additive and log-derived:
by_source.<cli|mcp>.mean_latency_ms— mean of the events' recordedlatency_ms(an absent field on a pre-v2.4 event reads as0.0), rounded like the other means.recent_searches[].source— the event'ssourcetag, so the recent view can label each query agent-vs-human.
The read side gains latency_ms on the parsed search event (absent ⇒ 0.0). No write-side change.
Make this call's usage visible in the conversation, opt-in, one byte-pinned line.
Per-project .cce/config (tolerant YAML, loaded like every other config block; precedence
default → .cce/config, no per-call arg in v2.8 so the agent can't flip it mid-session):
mcp:
result_footer: "off" # off (default) | on | sessionoff(default) — no footer. The tool-result bytes are byte-identical to v2.7 ⇒conformance.jsonand the MCP goldens do not move.on— append one line reporting this call's accounting.session— ason, plus a running session clause from the server's in-memory session usage counters.- YAML 1.1 note: a bare
on/offparses as a boolean; the loader accepts both the quoted strings and the boolean forms. Unknown values fall back tooff(tolerant, like every config block). The mode is read once atcce mcpstartup.
The footer is appended after the existing query_id + record_feedback hint, as the LAST
line, and it is an observability annotation, not retrieved context:
- It is rendered after all savings measurement, from the values already on the recorded
searchevent. It is excluded fromserved_tokens/baseline_tokens/ the grammar bucket. - ⇒ Invariant 1 holds: the recorded
searchevent and everyby_source/savings number are identical whether the footer isoff,on, orsession. The footer only prints what was already computed. (This is what keeps the dashboard andcce usagehonest regardless of the toggle.) - It applies wherever a
context_searchrecords asearchevent: the single-repo path, the workspace (federated) path, and the code side of a knowledge blend (source:"both", where the chunk count shown is the blended total the renderer shows). A knowledge-only search records nosearchevent and carries no footer.
A new crate::grammar line (usage_footer_line, a sibling of compact_line), so Ruby/Rust render
identical bytes and it is conformance-testable when enabled.
on:
cce: 5 results from 38,628 chunks · served ~1,204 tok vs ~9,880 baseline · saved ~8,676 (88%)
session adds a trailing clause:
cce: 5 results from 38,628 chunks · served ~1,204 tok vs ~9,880 baseline · saved ~8,676 (88%) · session: 42 searches, ~310k saved
- Numbers come straight off the event:
result_count, the corpus chunk count already passed to the result renderer,served_tokens,baseline_tokens,tokens_saved, andround(savings_ratio × 100). Thousands separators are byte-pinned; the session clause's token form is the §2.2 short form (~310kfrom 310,880). - The session clause reads the server's in-memory session usage counters: the count of
searchevents THIS session recorded and their summedtokens_saved(values read off each already-built record — no new accounting). The running total includes the current call, is accrued whatever the footer mode (so enablingsessionmid-project shows honest totals), and resets with the server process — it never leaks across sessions. - One line, no blank line above (keeps the context cost to a single line — the whole point of it being opt-in).
- Config: new
mcp:block in.cce/configwithresult_footer(M2). NewMcpConfig::loadmirroringOutputConfig/RetrievalConfig(tolerant parse, defaultoff). cce init: the report's "confirm it was used" step now points atcce usagealongsidecce dashboard. (cce initdoes not generate a.cce/config, so the footer key is documented indocs/mcp.mdrather than written commented-out — see §9.)- CLI: new
Command::Usage { … }inmain.rs→cmd_usage, reusingaggregate/federated_metrics_json_since+ the small--sinceevent pre-filter and the two byte-pinned renderers (crate::usage). - Docs:
docs/mcp.md(footer toggle + "off by default for context hygiene" +cce usageas a confirm-usage signal),docs/dashboard.md(cross-linkcce usage; the §2.5 additive fields),docs/how-to.md(the "who used CCE" recipe), and the command table inREADME.md.SPEC-MCP.mdgains the footer note (output formatting only; contract unchanged).
- Footer-off byte-identity: the MCP goldens +
conformance.jsonunchanged; an explicit test asserts the default andoffserve no footer byte and equal each other. - Toggle-invariance: same query,
offvsonvssession⇒ identical recordedsearchevent (modulo the per-callts/id/latency_ms); only the returned text differs. (Guards Invariant 1.) - Footer grammar goldens: byte-pinned
onandsessionfooter lines (unit) + the live line's shape and determinism over the real binary. cce usagegoldens: byte-pinned human render +cce.usage/v1JSON with injectednow(unit), plus process-level byte-pins for the wall-clock-free forms (all-time, ISO--since), single-repo and--workspace(the latter proving the root log is folded in — asource:"mcp"root event appears inby_source.mcpbut notby_package).- Dashboard parity: tests run BOTH real paths over one fixture log — the dashboard's
/api/metricsbody vscce usage --json— and asserttotals/by_source/by_package/recent agree field-for-field (single-repo and workspace). --sincefilter: events before the cutoff excluded; malformed--sinceerrors listing the accepted forms.- Cross-engine anchor (deferred to cce-ruby's catch-up): the
cce.usage/v1body from the shared fixture log matches between cce-rust and cce-ruby.
- v2.8.0, additive.
SYNC_FORMAT_VERSIONis untouched (no cache/artifact change). MCP protocol version untouched (no new tool; the footer is output formatting behind a config flag). - Rust-first; cce-ruby reconciles to the
cce.usage/v1and footer goldens afterward.
- A runtime MCP tool to flip the footer (à la
set_output_compression). Config-only in v2.8 keeps the agent from toggling its own observability; revisit if a real need appears. - A
usageMCP tool / resource exposing the aggregate to the agent. The question is a user's, answered by user surfaces (CLI/dashboard); feeding usage back to the agent is a separate idea. - Per-package attribution of federated agent searches (issue #28's Option 2/hybrid). Unchanged
here: federated searches stay out of
by_packagein both new surfaces. - Cost in
$oncce usage—cce savingsalready owns the$estimate; keepusageabout counts + tokens to avoid two divergent money numbers.
cce usage --since 24handcce usage --workspace --since 24hprint the byte-pinned block;--jsonemitscce.usage/v1; numbers equal the dashboard's for the same window (test-proven, both paths over one fixture).- With
mcp.result_footer: on, acontext_searchresult carries the one-line footer; the recordedsearchevent is identical to theoffrun (per §5.2). - Full suite green;
conformance.json+ the MCP goldens unchanged; clippy + fmt clean; docs updated.
- Latency needed a home. The draft's human render showed a per-source latency mean, but the
aggregate carried no latency at all. v2.8 adds
by_source.*.mean_latency_ms(and the read-sidelatency_mson the parsed search event) additively — §2.5 — so the render stays a pure projection of the one aggregate rather than a side computation. - Recent needed
source. Same story: the recent view labels each querymcp/cli, sorecent_searches[].sourcejoins the aggregate additively instead ofcce usagere-deriving a second recent list. - The session clause reads the session usage counters, not the L6 ledger. The draft said the
clause "reads the L6 session ledger totals", but the L6 ledger (SPEC-V2.5 Layer 6) records
queries/ids only — it has no token totals. As built, the server keeps two in-memory session
counters (searches, summed
tokens_saved), accrued from each already-built record; the "omit the clause when no session ledger is active" case is gone because a running server always has the counters (they start at zero). cce initwrites no.cce/config. The draft had init writing a commentedmcp.result_footer: offinto "the generated.cce/config", butcce inithas never generated that file (it writes.mcp.json,CLAUDE.md,.gitignore). As built, init's report points atcce usage/cce dashboardas the prove-the-agent-used-CCE surfaces, and the footer key is documented indocs/mcp.md.- Byte-identity of the recorded event is asserted modulo
ts/id/latency_ms. Those three are per-call values (wall clock + unique id) that differ between any two runs by construction; every other recorded byte is compared exactly (§1.1, §5.2). - Exact pinned formats. §2.2/§2.3 now show the as-built byte-pinned renders (alignment rules,
the short token form, the JSON's alphabetical
by_sourcekey order from canonical serialization) instead of the draft's illustrative sketches.