This is the contract for the operations ck-astrocyte serves and the
operations it calls on peer modules. The source of truth is
crates/astrocyte-module/src/ops.rs. See the data model for
the tables these operations read and write.
astrocyte runs under subc, the subconscious daemon, which supervises local
modules and routes requests between them. It serves one management surface
under module id astrocyte. A
client opens a route with target {"kind": "management_surface", "module_id": "astrocyte"} and sends a request body {"method": <operation>, "params": <object>} on that route. spend_drive is a client that does exactly this;
see operations.
- A success is a Response frame whose body is
{"result": <reply>}. - A refusal is an Error frame whose body is
{"code": <code>, "message": <text>}.codeis a closed string from this document;messageis for people and is not a stable interface. - A body that does not decode as
{method, params}is refusedinvalid_request. - A reply larger than the daemon delivers (4 MiB, counting the 21-byte
frame header) is replaced by a refusal
frame_too_largewhosedetailis{"size": <reply bytes>, "limit": <limit>}, on the request's own route and correlation id. Test:oversized_route_reply_is_refused_frame_too_large_on_its_own_correlation. - A request whose handler panics is refused
internal, and the module then stops with a non-zero exit, because a panic may have left shared state broken. Test:handler_panic_replies_before_serving_ends_with_nonzero_exit. - A route opened by a subc flow (work that runs for its owner without the
owner present) may call the read operations. Every write
operation on such a route is refused
flow_scopes_unsupportedbefore dispatch. Test:a_flow_stamped_route_may_read_but_every_write_is_refused.
The manifest advertises the operation list and a versioned params schema per
operation (operation_schemas, OPERATION_SCHEMA_VERSION = 2). Every params
object is closed (additionalProperties: false). A field is required exactly
when the request type has no default for it; an optional field also accepts
null. Integer bounds are the Rust type's range. Tests:
operation_surface_is_set_equal_to_the_versioned_artifact,
every_operation_schema_agrees_with_its_decoder.
PUBLIC_OPERATION_NAMES is the closed list. An unknown method is refused
unknown_method. Writes are serialized through one writer; reads run on a
read-only connection in one snapshot and neither wait for nor block ingest.
Test: every_operation_name_reaches_its_own_handler.
| Operation | Kind | Request fields (required unless marked ?) |
|---|---|---|
budget.verdict |
read | project_root?, provider?, model?, est_cost_nanos? |
spend.report |
read | grouping (string array), grain, timezone |
spend.subscription.report |
read | none |
spend.workclass.report |
read | none |
spend.ledger.coverage |
read | none |
spend.charge.explain |
read | event_id |
spend.reconcile |
read | statement_id |
spend.multiplier.report |
read | from_ms, to_ms |
spend.multiplier.pace |
read | quota_provider, from_ms, to_ms |
usage.cache.sessions |
read | since_ms?, harness?, limit? |
usage.cache.steps |
read | harness, session, limit?, after_cursor? |
usage.runs.list |
read | from_ms, to_ms, harness?, limit?, after? |
usage.runs.detail |
read | key, limit?, after? |
usage.steps.subscribe |
read, watch | harness?, session? |
usage.steps.read |
read | after?, limit?, harness?, session? |
watermark.get |
read | none |
spend.event.ingest |
write | see §3.1 |
spend.correction.ingest |
write | see §3.2 |
spend.chargeability.settle |
write | event_id, to_state, reason |
spend.statement.import |
write | path, provider, account_ref, period_start, period_end, amount_exponent, currency |
spend.subscription.set |
write | account_ref, provider, plan_name, recurring_cost_minor, currency, amount_exponent, period, effective_from |
spend.subscription.close |
write | account_ref, provider, effective_to |
spend.event.reprice |
write | event_id |
spend.basis.correct |
write | event_id, corrected_basis, reason |
spend.multiplier.recompute |
write | observation_day, quota_provider? |
Times are Unix milliseconds. Money is signed integer nanounits of the named
currency (*_nanos); *_minor amounts are minor units scaled by
amount_exponent (0 to 9).
Every request that fails to decode (a missing or unknown field, a wrong type,
a value outside an enum) is refused malformed. Store failures are
internal. A refusal writes nothing. Test:
malformed_operation_is_a_refusal_without_a_write.
Records one usage event pushed by a peer. Required: producer,
producer_ref, charge_basis (metered_api, subscription_included or
unknown). Optional strings: producer_lineage, prior_event_ref,
harness, project_root, session, run_ref, work_class,
terminal_status, vendor_request_id, provider, model, account_ref,
auth_selection. Optional integers: occurred_at_ms, emitted_at_ms,
attempt_ordinal. measurements defaults to empty; each item has role,
unit, kind, source and optional quantity_units, quantity_exponent.
Reply: event_id, disposition, transitions_written,
quarantine_evidence_written, waiting_for_target. disposition is one of:
| Disposition | Meaning |
|---|---|
recorded |
A new event; its measurements are queued for pricing. |
exact_redelivery |
The identity exists with the same payload. Nothing changes except one redelivery_log row. |
quarantined_redelivery |
The identity exists with a different payload. The stored event is kept; the incoming payload goes to rejected_delivery. |
Refusal malformed: empty producer or producer_ref, an empty
prior_event_ref, a measurement with an empty field, a quantity with only one
of its two parts, negative units, an exponent outside -9..0, or two
measurements with the same (role, unit). Tests:
exact_redelivery_is_an_order_independent_content_noop_and_logs_occurrence,
same_identity_different_payload_is_quarantined_without_overwrite.
Appends a signed money adjustment to an event. Required: correction_id (1 to
128 UTF-8 bytes), event_id, currency, amount_nanos, reason
(write_off, vendor_restatement, our_repricing, refund, goodwill),
occurred_at_ms (at least 0). Optional: note. Reply: adjustment_id,
origin (correction_ingest). The retry rules are in
money ingest.
| Code | Condition |
|---|---|
malformed |
An empty or oversized correction_id, an empty event_id or currency, or a negative time. |
correction_id_reused |
The key exists with different content. |
not_found |
No event has event_id. |
undated_target |
The event has no occurred_at_ms. |
unpriced_target |
The event has no currency yet: it has never been priced. |
currency_mismatch |
currency differs from the event's. |
write_off_requires_pricing |
reason is write_off; write-offs are not accepted yet. |
Tests: correction_ingest_requires_key_and_replays_without_moving_money,
correction_key_conflicting_content_is_refused_without_moving_money.
Settles whether an event is chargeable, once. to_state is billed,
written_off or unbillable. Reply: event_id, to_state.
| Code | Condition |
|---|---|
malformed |
An empty event_id or reason. |
operator_cannot_bill |
to_state is billed; only a later record or a statement can bill. |
not_found |
No event has event_id. |
already_settled |
The event already has a transition. |
unpriced_target |
The event has no currency. |
Tests: operator_cannot_mark_candidate_billed_without_effect,
settlement_is_terminal_and_current_state_comes_from_history.
Imports a vendor statement CSV from path and matches its lines against the
ledger. The file is read before the write transaction. Reply: statement_id,
import_id, content_outcome (created or no_op), lines_created,
lines_skipped, matching.
| Code | Condition |
|---|---|
invalid_request |
path is not a regular file, or is over 64 MiB. |
statement_source |
The file cannot be read. |
malformed |
A line or field does not parse. |
invalid_period |
period_start is not before period_end. |
line_outside_period |
A line falls outside the declared period. |
statement_currency_mismatch |
A line's currency differs from currency. |
conflicting_reimport |
A line already imported has different content. |
amount_out_of_range |
An amount cannot be represented in nanounits. |
settlement_act_refused |
A settlement the import makes is refused. |
Tests: statement_import_refuses_a_directory_without_importing,
statement_import_refuses_files_over_64_mib_without_importing,
statement_import_refuses_fifo_without_waiting_for_a_writer.
spend.subscription.set declares a plan for (provider, account_ref) from effective_from,
bounding any open plan for that key at the same instant. period is month
or year; currency is three uppercase letters. Its reply is the plan's
fields plus backdated (effective_from is before the call) and
coverage_notice (the one-way notice text when backdated, else null).
spend.subscription.close bounds the open plan at effective_to. Its reply is subscription,
coverage_is_one_way (always true) and coverage_notice.
| Code | Operation | Condition |
|---|---|---|
malformed |
both | An empty field, a negative cost or time, amount_exponent above 9, or a non-canonical currency. |
recurring_cost_out_of_range |
set | The cost cannot be represented in nanounits. |
overlapping_effective_range |
set | An open plan starts at or after effective_from, or a closed plan ends after it. |
no_open_subscription |
close | No open plan exists for the key. |
close_before_effective_from |
close | effective_to is at or before the open plan's start. |
Coverage is one-way: closing or superseding a plan never uncovers an event the
plan already priced as covered. See operations. Tests:
subscription_set_reply_carries_the_one_way_notice_only_when_backdated,
storage_update_and_delete_guards_preserve_subscription_history.
spend.event.reprice re-resolves an event's measurements whose current head is unpriced
or missing, and keeps priced and covered heads. spend.basis.correct changes the
event's charge basis to corrected_basis and supersedes every head. Both
resolve against the catalog (or the subscription in force) before the write
transaction, then re-check that nothing moved. Reply: event_id,
pricing_generation, measurements_repriced, measurements_preserved.
| Code | Condition |
|---|---|
malformed |
An empty event_id, or (correction) a blank reason. |
not_found |
No event has event_id. |
basis_unchanged |
(correction) corrected_basis equals the current basis. |
no_measurements_to_price |
The event has no measurements. |
already_priced |
(reprice) Every measurement already has a priced or covered head. |
pricing_superseded |
Another pricing act committed between resolve and commit. |
subscription_coverage_superseded |
The subscription in force changed between resolve and commit. |
catalog_currency_mismatch |
A resolved price's currency differs from the event's currency or from another measurement's. |
Tests: op_event_reprice_routes_subscription_included_through_coverage,
basis_correction_op_refusals_write_no_charges,
basis_correction_op_reaches_all_three_arms,
missing_reprice_event_preserves_not_found_and_writes_nothing.
Recomputes one UTC day (observation_day, YYYY-MM-DD) for one configured
quota provider, or for all of them when quota_provider is absent, over the
seven days ending at that day's 00:00Z. One row per configured plan supersedes
that provider/plan/day's head, or
is the day's original when none exists. All rows append in one transaction or
none does. Reply: {"recomputed": [{quota_provider, plan, observation_day, observation_id, supersedes, multiplier, fee_multiplier}]}.
| Code | Condition |
|---|---|
malformed |
observation_day is not a date. |
multiplier_not_recorded |
The committed configuration has no subscription_multiplier member. |
unknown_quota_provider |
The named provider is not configured, or none is. |
scoped_producer_not_ingested |
A requested provider's scope names producers not ingested by the configuration. The message names them; all requested providers are checked before any call or write. |
catalog_unavailable |
No fusiform catalog is connected, or a catalog call failed. |
plan_prices_unavailable |
Plan prices were needed and could not be read. |
rate_fact_unrecognized |
The catalog served a rate this build cannot read. |
source_reads_pending |
An in-scope source is stale or has pending known/unknown backlog, and two hours have not passed. Recompute checks this even for an existing day. After the bound, rows are appended with source_backlog and are routing-ineligible. |
arithmetic_out_of_range |
An exact computation overflowed. |
Tests: a_recompute_supersedes_the_day_and_the_report_serves_it,
recompute_creates_a_missing_day_without_supersedes_and_refuses_unknown_provider,
served_recompute_refuses_excluded_scope_without_calls_or_rows.
Reads never write. A refusal on a read is one of the codes below.
budget.verdict:decision(alwaysallow),consumed_nanos(current heads plus adjustments, by currency, filtered by the optional fields),est_cost_nanos(echoed),limit_nanosandremaining_nanos(alwaysnull),uncertainty(unpriced_events,events_total),uncertainty_scope(machine). Test:only_the_charge_nothing_supersedes_counts_toward_verdict_and_total.spend.report:totals,rows,undated,ingest_freshness,rate_staleness, andhierarchy_resolution(unavailable) when grouped byproject.groupingnames areprovider,model,account,auth_selection,charge_basis,work_class,project;grainisday,weekormonth;timezoneis an IANA name. Refusals:invalid_grouping(unknown or repeated),invalid_grain,invalid_timezone. Test:spend_report_refusal_keeps_its_code.spend.subscription.report:rows,coverage,ingest_freshness.spend.workclass.report:rows,producers.spend.ledger.coverage:set_aside_by_source,events_total,events_unpriced,events_undated,zero_measurement_events,unpriced_events_by_reason,unpriced_measurements_by_reason,ingest_freshness,rate_staleness,capacity. Test:open_coverage_report_does_not_stall_an_ingest_commit.spend.charge.explain:event,measurements,adjustments,transition. Each measurement suppliesroleandunit; its charges are identified bypricing_generationwithin that measurement. There is nocharge_idfield.supersedesis the predecessor's pricing generation (null for an original), not an integer storage key. Event IDs are unchanged and no local keys are served. Refusals:malformed(emptyevent_id),not_found.spend.reconcile:statement,lines,summary. Refusals:malformed,statement_not_found; a store failure isstore_error.spend.multiplier.report:latestcontains each provider/plan's newest head, ordered by(quota_provider, plan). A legacy pooled row (plan: null) appears there only when its provider has no plan-specific observation at all.serieskeeps every head in the requested day range, including both legacy pooled and plan-specific heads. Refusal:invalid_range(from_msnegative or not beforeto_ms). Test:multiplier_report_refuses_a_malformed_range. Rows addplan,routing_eligible,eligibility_reasons,clean_hours_share,saturated_share,gap_share,unpriced_share,unconfigured_accountsandsource_backlog, plusunattributed_event_share,zero_measurement_events,missing_quantity_eventsand the derivedunknown_quantity_measurementscount. Missing-quantity and no-measurement usage exclude account-hours; neither their counts nor their share independently veto a row. Retained sample adequacy and stability govern eligibility. Saturation keeps its separate veto. Quality shares are fractions of all configured account-hours in the row's plan; unpriced diagnostics also include excluded hours. Legacy rows retain their old values with null plan and ineligible legacy defaults.stability_diagnosticsadds the recorded day-block bootstrap interval/width, active-day count, held-out-day six-hour RMSE/R² and resampling parameters; older rows without calibration inputs return null there. Adequacy uses clean hours/points, active days and interval width, not a clean-, gap- or unknown-share floor.spend.multiplier.pace: hourly usage by producer and work class, valued at the latest eligible, unsuperseded head's recorded rates without a catalog call. Accepts optionalplan; it is required when configured plans differ. Replies include the selectedplanandrouting_eligible: true. Eligible heads must end no more than two days beforeto_ms; an older eligible row does not authorize a stale estimate. Refusals:plan_required,unknown_quota_provider,invalid_range,range_too_large(over 192 aligned hours),no_multiplier_observation. Test:pace_refuses_unknown_provider_invalid_or_excessive_range_and_missing_head.
Absent values are null or omitted, never zero. A total that cannot be
priced is reported with its unpriced population, not as a smaller number.
These two operations serve harness steps per session, with cache verdicts.
A harness is a producer whose events are one
model step each: claude-code, codex, gemini, kimi, omp, opencode or
pi. BROCA steps will arrive from its step log, not its exported spend facts.
A request that fails to decode is refused malformed, as everywhere
else. A request that decodes is refused with one of these codes:
| Code | Condition |
|---|---|
invalid_request |
limit is 0, session is empty, or since_ms is negative. |
unknown_harness |
harness is not one of the harnesses above. |
range_too_large |
limit is above its maximum, or since_ms is more than 7 days before now. |
usage.cache.steps returns up to limit steps of one session (default 200,
maximum 1,000): the newest ones, oldest first. With after_cursor, only the
steps of that window whose cursor is greater are returned. The reply has
harness, session, steps, earlier_steps (whether the session has older
steps than the window) and undated_steps (steps without a time, which are
not placed on the timeline). Each step has these fields:
cursor, the durable cache-step cursor. It increases across harness events, survives restarts and is never reused. A late step can have a greater cursor than a step that happened after it.step_ref, the harness's own id for the step, andoccurred_at_ms. BROCA's step-log reader will use the event'sattempt_id, a random id per provider attempt, as described in section 4.2.input,cache_read,cache_write,outputandreasoning, each omitted when the reader did not store it.cache_writesums every cache lifetime Claude Code reports.outputincludes reasoning, the fleet convention:reasoningis a breakdown insideoutput. The ledger stores the two disjoint, and a served row adds them:outputis the stored output plus the stored reasoning when both are present, the stored output when reasoning is absent, and omitted when no output was stored. A count is never made up from reasoning alone. Test:served_output_includes_reasoning_on_both_reads.provider,model,finishandcontext_limit.finishcomes from OpenCode'sfinish, Pi'sstopReason, or the stored stop reason of Claude Code and Oh My Pi.context_limitcomes from Codex.request_body_lenis optional and is not populated by the current readers. On a prompt drop,request_sizeis present when both adjacent steps report body lengths:smallerwhen the current body is shorter, otherwisenot_smaller.prompt, which is input plus cache read plus cache write.retention,verdictandprompt_dropped.retentionis served unrounded and unclamped: a non-negative number that may exceed 1 when a step reads more cache than expected.
The verdict is one of cold_start, stable, warning, bust, full_bust
and unknown. It is computed over the whole session in time order, so a
step's verdict does not depend on the page size. The rules are the Magic
Context dashboard's:
- A step without a stored cache read is
unknown. It is also never the baseline the next step is compared with. - In a session where no step ever read a positive number of cached tokens,
every step is
unknown. Codex is the exception. - The session's first step is
cold_start. - Otherwise, the step is compared with the last earlier step that reported its
cache. When there is none, or that step read zero, the verdict is
stablewith noretention. Otherwise,expectedis that step's cache read plus its growth, which is its cache write when positive and otherwise its uncached input.retentionis this step's cache read divided byexpected. - Codex reports no cache writes, so its
retentionis the cache read divided by the cache read plus the uncached input. When that sum is zero, the verdict isunknown. - A cache read of zero is
full_bust. Otherwise, aretentionof at least 0.95 isstable, at least 0.80 iswarning, and anything lower isbust. prompt_droppedis true whenpromptis at least 15,000 tokens smaller than that earlier step's prompt.
The serving side keeps the newest 1,000 steps of up to 200 sessions active in
the last 24 hours in memory. It loads them at startup and adds each commit's
new steps after the commit. A request it can answer exactly is answered from
memory, and any other request is answered from the store. Both build the reply
the same way. Tests: drained_pi_steps_are_served_alike_from_memory_and_store,
store_steps_are_the_same_whatever_the_window,
prefix_cache_verdicts_cover_every_band_and_edge.
The memory window is distinct from storage retention. BROCA diagnostics are kept for 30 days after the step finished, with at most 500 expired rows deleted per maintenance transaction. Harness ledger steps are kept indefinitely.
Sample replies, published for a client to pin its decoder against, live in
crates/astrocyte-module/test-vectors/usage-cache-steps-v1/, with the request
that produced each one. The step cursor is the cache-step cursor, a
different cursor space from the live feed's cN cursors: it numbers recorded
steps in their own order, so two rows with the same number in a cache read and
a feed read are not the same step. To join a session's history with
the live feed, an app matches rows by (harness, step_ref) and replaces its
stored copy of a matched step with the newer row, and never compares a
cache-step cursor with a feed cursor.
usage.cache.sessions returns since_ms (default: 24 hours ago) and
sessions, the harness sessions with a step at or after since_ms, most
recently active first. There are at most limit sessions (default 50, maximum
200), optionally for one harness. Each session has these fields:
harnessandsession.project_root, from the newest step in the window that has one.steps, the number of steps in the window.hit_ratio, which is the cache reads divided by the sum of input, cache read and cache write. Only steps that report their cache count, and the field is omitted when there are none.busts, the number of steps whose verdict isbustorfull_bust.last_activity_ms.context_limit, the newest positive context limit in the window.
It is always read from the store. Test:
sessions_report_hit_ratio_busts_limit_and_activity.
Apps follow cache diagnostics live through two operations: a subscription
that only says new steps exist, and a paged read that returns them. Apps talk
only to astrocyte. Astrocyte combines the steps it reads from harness files
with the step.finished.v1 events agent runners publish on the bus. A step a
runner announces is in the feed within seconds; billing still comes only from
the durable reads.
Who may call. The daemon-attested principal at route bind decides:
Principal::Direct (local apps on the same computer) and reserved:callosum
with a phone-class device label (fed-phone:<fingerprint>). Every other
principal is refused not_permitted, simulators (fed-sim:) and other
computers (fed-computer:) included.
Cursors are opaque strings. They are ordered by astrocyte's commits, are stable across restarts and are never reused. Apps pass them back and never parse or compare them.
Order rule for apps. Subscribe first and hold the notices, read history
with usage.steps.read, then read again from the read's through. After a
reconnect, read once more a few seconds later: a step that has just finished
can take a few seconds to be committed.
The request is {} or {"harness": "pi", "session": "session-id"}.
harness and session are optional, but must come together; one alone is
refused invalid_request. Unknown members are refused malformed.
Each notice is one StreamData frame:
{"through": "c1824004"}through is the newest committed cursor. Notices are coalesced: at most four
a second, and a notice may cover many steps or none that the caller may see.
With a session filter, a notice is sent only when a row whose own harness
and session match exactly is appended. Its through is the feed-wide cursor
committed with that row; later unrelated rows do not advance it, so it may be
older than the feed's newest cursor. A caller reads after its own last cursor
with the same filter, so this changes nothing it needs. Without a filter,
notices are unchanged.
| How it ends | Meaning |
|---|---|
| StreamEnd | Astrocyte is stopping or the route closed. Subscribe again. |
Error lagged, detail {"through": "<last notified cursor>"} |
The subscriber fell behind its queue bound. Subscribe again and read from the last cursor it holds. |
| Caller's Cancel | Ended by the caller. The caller may still receive Error cancelled (the daemon may drop it) and treats it as the normal end. |
Opening refusals: not_permitted, subscription_capacity (more than 16 open
subscriptions), draining (astrocyte is stopping), malformed, and
invalid_request (only one filter member was supplied).
The request is {"after": "<cursor>", "limit": 200, "harness": "pi", "session": "session-id"}.
All members are optional. harness and session must come together; one alone
is refused invalid_request. Unknown members are refused malformed.
Without after, the read starts at the oldest row the feed still holds. The
feed holds the rows committed in the last 24 hours. limit defaults to 200,
with a maximum of 1,000.
With a session filter, only rows whose own harness and session match exactly
are returned, still excluding superseded identities. limit bounds the rows
returned, not the unrelated rows between them. through remains the newest
cursor across the whole feed. next is the last returned row's cursor while
more matching rows exist up to through, and null when no matching rows remain.
A filtered page is never empty with a non-null next. Cursor expiry is unchanged.
Without a filter, the read and its reply are unchanged.
Reply:
{
"rows": [
{
"cursor": "c1824003",
"harness": "opencode",
"source": "file",
"step_ref": "msg_0a1b2c3d4e5f",
"session": "ses_0a1b2c3d",
"occurred_at_ms": 1792880000000,
"provider": "anthropic",
"model": "claude-sonnet-5",
"input": 1200,
"cache_read": 182000,
"cache_write": 900,
"output": 640,
"reasoning": 120,
"finish": "tool-calls",
"prompt": 184100,
"retention": 0.9912,
"verdict": "stable",
"prompt_dropped": false
}
],
"next": null,
"through": "c1824004"
}rowsare oldest first.nextis the cursor to pass asafterfor the following page, or null when the reply reachedthrough.throughis the newest committed cursor.- A row's identity is
harnessplusstep_ref. When any field of a step changes (a runner-announced step recorded from its log, a finish reason that arrives late, or a verdict recomputed because an earlier step of its session arrived late), the whole row is sent again at a new cursor. The last row for an identity wins: apps replace, never append.
Runner rows are served: a row from a runner's step.finished.v1 event takes
its identity from the event. When the event carries harness_step_ref (runner contract 14.3,
present for runners whose history is the host harness's own record),
harness is the event's harness (the hosting program, such as
claude-code or opencode) and step_ref is harness_step_ref, so
astrocyte's later read of the harness store replaces the row. Otherwise
harness is the event's runner and step_ref is the event's attempt_id, a
random id per provider attempt. There is one row per provider request actually
sent, retries included, matching astrocyte's read of that runner's durable step
log. A step with no durable record stays a live-only row with
source: "runner".
sourceisfilefor a step astrocyte read from a harness store or a runner's durable log, andrunnerfor a step known so far only from a runner's bus event.harnessis the producer:claude-code,codex,gemini,kimi,omp,opencode,pi, or the runner's module id (broca,thalamus-harness,thalamus).- Required:
cursor,harness,source,step_ref,session,occurred_at_ms,prompt,verdict,prompt_dropped. Steps without a time are not in the feed. - Optional, omitted when unknown and never zero for unknown:
run_id(runner steps),provider,model,input,cache_read,cache_write,cache_write_5m,cache_write_1h,output,reasoning,finish,context_limit,request_body_len,retention,request_size. outputincludesreasoning, which is a breakdown inside it, as in section 4.1, andcache_writeis the sum of every cache lifetime.cache_write_5mandcache_write_1hare present only when the source split them.prompt,retention,verdict(cold_start,stable,warning,bust,full_bust,unknown),prompt_droppedandrequest_size(smaller,not_smaller) follow the rules in section 4.1 exactly. They are computed over the step's whole session in time order. The thresholds 0.95 and 0.80 and the 15,000-token prompt drop are part of this contract's version.- Rows never carry prompt text, tool names or error text.
Refusals: malformed (undecodable, or an unknown member), invalid_request
(limit is 0, or after is not a cursor astrocyte issued), range_too_large
(limit above 1,000), cursor_expired (after is older than the feed holds;
detail {"through": "<newest cursor>"}: reload per-session history with
usage.cache.sessions and usage.cache.steps, then read from through), and
not_permitted.
- The feed is the
step_feedtable (see the data model). After each commit that adds steps or changes a step's stored detail, astrocyte recomputes the served rows of each affected session, from the earliest changed step onward, and appends every row that differs from the newest row stored for itsharnessandstep_ref. That append is its own transaction, run after the commit that recorded the steps; subscribers are told only once it has committed. A cursor iscfollowed by the row's feed position. - A step that happened more than 24 hours before it is committed (a first
read of old history) is not added to the feed;
usage.cache.stepsserves it. Rows become eligible for deletion 24 hours after they were committed; each feed append deletes up to 1,000 of them, so a cursor stays readable until the deletion reaches it. - Runner rows come from the
step.finished.v1events of the runners the bus naming table grants astrocyte; today those arebroca,thalamus-harnessandthalamus. Astrocyte follows each runner's own subject,ck.<account>.live.<runner>.step.finished.v1, with a plain subscription and no durable. The subscriptions share the quota reader's single ckbus credential and NATS connection, and they run whether or not insula is configured. A row is appended as soon as the event arrives, and subscribers are told once it is committed. Runner rows are display data only: nothing from an event reaches the spend ledger. - A runner row's
prompt,retention,verdict,prompt_droppedandrequest_sizeare computed by the same rules as a file row's, over the session's recorded steps and its other runner rows.run_id, andcache_write_5mandcache_write_1hwhen the event split them, are sent only on runner rows.outputincludesreasoningunless the event says reasoning is outside it. - When the feed's newest row for the identity is a file row, astrocyte drops the runner event, because the durable read is the step's record. When it is the same runner row, the event is a duplicate and is also dropped. A later file row for the same identity replaces a runner row. An event that does not decode, or whose subject names a different runner than its body, is counted and dropped. So is a step that finished more than 24 hours ago. If the bus is unavailable or refuses the subscription, astrocyte logs it once and retries, and no new runner rows arrive; runner rows already appended stay served until a feed append prunes them. Quota events are not affected.
- The phone's device label is the route's bind identity
harness(fed-phone:<fingerprint>), as Callosum stamps it; the principal must bereserved:callosumas well. - A subscriber's queue holds 64 cursors. A subscriber whose notices are held
up for longer than that ends
lagged. - The serve helper always answers a cancelled request; a cancelled
subscription's last frame is the error
cancelled, which the daemon may drop because the caller asked to stop.
Tests: a_notice_is_sent_only_after_its_steps_commit,
reads_page_through_the_feed_in_commit_order,
a_verdict_changed_by_a_late_earlier_step_is_sent_again,
a_read_from_beyond_the_window_is_refused_cursor_expired,
the_feed_is_refused_to_simulators_computers_and_other_modules,
a_subscriber_that_falls_behind_its_queue_ends_lagged,
a_drain_ends_every_step_subscription_before_goodbye,
two_subscribers_both_receive_a_notice,
each_runners_event_is_served_as_a_runner_row,
a_file_row_for_the_step_supersedes_the_runner_row,
a_runner_event_after_the_file_row_leaves_the_file_row_newest,
an_undecodable_event_is_counted_and_dropped,
a_runner_rows_verdict_matches_the_file_row_that_replaces_it,
a_runner_row_wakes_its_sessions_filtered_subscription,
bus_reader_without_insula_serves_runner_step_events.
watermark.get tells a module how far astrocyte has durably read the logs
that module writes, so the module can erase logs that have been recorded.
Astrocyte declares the capability in the manifest's capabilities.provides.
The names and wire types are the watermark module of
cortexkit-role-llm-runner (0.7.1, contract §9.15).
- The request is
{}; any member is refusedinvalid_params. - Astrocyte answers for the principal the daemon attested when the route was bound, never for a module the request names. It forgets that principal when the route goes or the connection ends.
- A module principal maps to one reader source. Today the only row is
thalamus-harness→opencode-thalamus(the THALAMUS sessions contract §4). - Reply
{"watermark_ms": <u64>}, the source's stored watermark, read on the read-only connection;{"watermark_ms": null}(present, not omitted) before the source's first complete read pass. - Refusal
not_a_read_source: the caller is not in that table (a local app'sDirectprincipal, any other module), or its source is excluded inharness_ingest(named inexclude, orenabled: false).
Tests: watermark_get_answers_thalamus_harness_as_the_crate_vectors_pin,
watermark_get_refuses_every_caller_astrocyte_does_not_read,
watermark_get_refuses_thalamus_while_its_source_is_excluded,
the_bind_principal_is_forgotten_when_the_route_or_connection_goes,
manifest_provides_the_log_read_watermark_capability.
These reads serve machine-wide session, BROCA run and BROCA flow costs from the
ledger, never from client-side pricing. They share the live feed's admission
check: Principal::Direct and reserved callosum routes with a fed-phone:
identity harness. Every other caller is refused not_permitted before operation-specific parameters are decoded.
Each reply is the response frame's result, read from one SQLite snapshot.
Keys are closed tagged objects:
{"kind":"session","harness":h,"session":s}:(producer, session), not the ledger's optional hosting-harness attribution. Harnesses areopencode(both versions),claude-code,codex,pi,gemini,goose,omp,kimi.{"kind":"run","harness":"broca","run_id":r}: BROCArun_ref, verbatim.{"kind":"flow","harness":"broca","flow_id":f}: BROCA batchflow_ref.
Run rows also carry session when any fact of the run records its label, even
if that fact is outside the requested range. It is absent for unlabelled runs;
no label is fabricated and no run is dropped. Flow rows carry work_class when
recorded; no additional flow-launcher identity is inferred.
usage.runs.list request: {from_ms, to_ms, harness?, limit?, after?}.
The half-open range must satisfy from_ms < to_ms and span at most 31 days.
limit defaults to 50 and must be 1–200. harness filters the key's harness.
after is the opaque next from a previous page with the same range/filter.
Reply: {rows, totals:{rows, steps, tokens, cost}, next?}. Range totals are on
every page and cover the entire filtered range. Each row is
{key, first_at_ms, last_at_ms, steps, tokens, cost, pricing_unit, models};
models are {provider, model, steps, tokens, cost} splits of exactly the same
figures. Missing provider/model identity is null. Every figure counts only
facts with occurred_at_ms in the range. Undated facts are not listed.
Rows sort by newest last_at_ms, then canonical key; the cursor resumes after
that pair. next is absent on the last page. A concurrently active key may move
to the first page: restart paging on a feed notice rather than merging snapshots.
usage.runs.detail request: {key, limit?, after?}. limit defaults to 200
and must be 1–1000. Reply: {key, pricing_unit, items, totals:{tokens,cost}, next?}.
Totals cover the entire key, not just the page or the list's time range. Items
sort oldest first, with undated (occurred_at_ms: null) items first and stable
ledger order for ties. Harness items are
{step_ref, occurred_at_ms, provider, model, measurements}: step_ref is the
verbatim producer reference also used by the feed for step-based harnesses.
Goose's reference identifies its accumulated session event, not a feed call. BROCA items instead carry
segment_ref, the fact id; it is not a feed step id.
Pricing units are step for ordinary harnesses, run_segment for BROCA, and
session_total for Goose. Goose's detail has one item for its accumulated
session event. Segment/session-total costs belong to the run/session as a whole;
clients must not divide them over feed calls.
Tokens have optional input, output, reasoning, cache_read, and
cache_write integer members. Missing is not zero: a role is absent when no
included event reports an exact whole-token quantity. Exact decimal encodings
are normalized (100 × 10^-1 contributes 10). Genuinely fractional quantities
are omitted from integer totals without rounding and preserved exactly in detail. output includes reasoning, as on the feed;
reasoning is its breakdown. Stored detail roles remain disjoint.
Cost is {priced, covered_measurements, unpriced_measurements, unpriced_by_reason, pending_measurements}. priced maps currency to signed
integer nanos and includes only current priced charges; it is {} if none.
Covered/unpriced/pending counts are never folded into money. Reasons are the
ledger's closed set, including no_rate, no_catalog_coverage, rate_tiered,
rate_inherited, and stated_zero_requires_policy. A measurement with no charge
head is pending. All-zero counts plus empty priced means no measurements.
Each detail measurement is {role, unit, quantity, state, amount_nanos?, currency?, reason?}. quantity is {units, exponent} (decimal ledger quantity),
or null when unreported. State is priced, covered, unpriced, or pending.
Only priced measurements carry amount/currency, and only unpriced measurements
carry reason. Current means no charge row supersedes it, so repricing replaces
figures rather than adding historical charges. List money excludes adjustments.
List refusals: malformed, invalid_range, range_too_large, invalid_request
(bad limit, harness or cursor/request mismatch), not_permitted. Detail refusals:
malformed, invalid_request, not_found (no fact has that key), not_permitted.
Pricing can trail ingest by a pass; pending counts expose that lag. No new watch
is added: reread on existing step-feed notices. Repricing alone does not produce
a feed notice; clients needing to observe resolved prices must reread separately.
List totals use rebuildable key/model/hour cells maintained by the existing hourly dirty triggers, revision fence, bounded reducer, atomic publication and audit. Clean complete hours use cells; dirty/missing/partial hours use indexed base ranges in the same snapshot. Detail pages use key/time indexes with LIMIT; whole-key totals combine clean cells and dirty-hour base reductions.
Synthetic vectors and SHA-256 checksums are published under
crates/astrocyte-module/test-vectors/usage-runs-v1/. Tests regenerate frame bodies
through the real dispatch and the serving path's shared permission refusal.
| Peer | Route | Calls | On failure |
|---|---|---|---|
| fusiform (price catalog) | tool_provider, module fusiform |
catalog.status, catalog.get, plan.prices as {"name", "arguments"} tool calls |
A failed pricing call writes no head; the event stays queued and a later pass retries it. Recompute refuses. Route death and module_reloading are retried once within the call's deadline. |
| insula (the quota module, QTA) | management_surface, module insula |
usage.get with empty params; events_get with event_key and event_name: "quota_sampled" |
Invalid or missing event bodies owe a snapshot. A failed snapshot records the attempt and keeps the last windows. Capacity is advisory and never blocks spend. |
| ckbus | Attested management_surface, module ckbus |
ckbus.credential, ckbus.credential_renew, ckbus.nonce_sign |
Retain per-tick insula polling and retry with exponential backoff (250 ms to 30 s). Never fail the module or change broker topology. |
Fusiform and insula use the connection files named in the
configuration. The ckbus route uses the module's daemon
connection file and the daemon-validated consumer_identity shared with HELLO,
not the un-attested peer clients. With no fusiform connection, events are
stored unpriced with reason catalog_unconfigured; with no QTA connection,
capacity is disabled.
Insula notices use ck.{bus_account}.event.insula.quota_sampled.v1, with no body.
Astrocyte binds its pre-existing pull durable from cortexkit-bus-naming's consumer
table, fetches in batches of 32 (one-second expiry), and hashes the exact compact
result.body bytes against both reply and notice digests. It checks the body's
provider, optional account, epoch and sequence against the notice. In-sequence
readings update only that account (or unlabeled provider-current readings); they
have no complete-provider retirement authority. First notices, new epochs, gaps,
missing bodies or failed validation record a snapshot obligation before ack.
Duplicates do not write again. Progress acks protect pending writes; final acks
follow commit or recorded recovery. A restart begins with a snapshot owed.
Healthy attachment, a current credential and a recent successful pull suppress
per-tick snapshots. The backstop is the earlier of ten minutes and the existing
capacity freshness horizon minus one cadence: normally 90 seconds. Event commits
refresh capacity sooner; empty pulls never do. Any bus failure restores per-tick
polling. Transitions are logged; health reply shapes, freshness rules, the
multiplier and budget.verdict are unchanged.
broca, the coding harnesses and thalamus-harness are not called. astrocyte reads their stores and files directly; see harness readers and the Claude workers contract.
budget.verdictenforces no cap. Callers must not readallowas approval of a budget.- A timed-out write has an unknown outcome. Clients must not retry a write
blindly;
spend.correction.ingestis the one write with a retry key. - Served writes carry no operator identity. The caller is whoever subc admitted to the route.