Skip to content

Latest commit

 

History

History
810 lines (702 loc) · 45.8 KB

module-surface.md

File metadata and controls

810 lines (702 loc) · 45.8 KB

Module surface

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.

1. Transport

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>}. code is a closed string from this document; message is for people and is not a stable interface.
  • A body that does not decode as {method, params} is refused invalid_request.
  • A reply larger than the daemon delivers (4 MiB, counting the 21-byte frame header) is replaced by a refusal frame_too_large whose detail is {"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_unsupported before 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.

2. Operations

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).

3. Write operations

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.

3.1 spend.event.ingest

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.

3.2 spend.correction.ingest

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.

3.3 spend.chargeability.settle

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.

3.4 spend.statement.import

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.

3.5 spend.subscription.set and spend.subscription.close

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.

3.6 spend.event.reprice and spend.basis.correct

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.

3.7 spend.multiplier.recompute

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.

4. Read operations

Reads never write. A refusal on a read is one of the codes below.

  • budget.verdict: decision (always allow), consumed_nanos (current heads plus adjustments, by currency, filtered by the optional fields), est_cost_nanos (echoed), limit_nanos and remaining_nanos (always null), 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, and hierarchy_resolution (unavailable) when grouped by project. grouping names are provider, model, account, auth_selection, charge_basis, work_class, project; grain is day, week or month; timezone is 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 supplies role and unit; its charges are identified by pricing_generation within that measurement. There is no charge_id field. supersedes is 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 (empty event_id), not_found.
  • spend.reconcile: statement, lines, summary. Refusals: malformed, statement_not_found; a store failure is store_error.
  • spend.multiplier.report: latest contains 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. series keeps every head in the requested day range, including both legacy pooled and plan-specific heads. Refusal: invalid_range (from_ms negative or not before to_ms). Test: multiplier_report_refuses_a_malformed_range. Rows add plan, routing_eligible, eligibility_reasons, clean_hours_share, saturated_share, gap_share, unpriced_share, unconfigured_accounts and source_backlog, plus unattributed_event_share, zero_measurement_events, missing_quantity_events and the derived unknown_quantity_measurements count. 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_diagnostics adds 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 optional plan; it is required when configured plans differ. Replies include the selected plan and routing_eligible: true. Eligible heads must end no more than two days before to_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.

4.1 Cache diagnostics

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, and occurred_at_ms. BROCA's step-log reader will use the event's attempt_id, a random id per provider attempt, as described in section 4.2.
  • input, cache_read, cache_write, output and reasoning, each omitted when the reader did not store it. cache_write sums every cache lifetime Claude Code reports.
  • output includes reasoning, the fleet convention: reasoning is a breakdown inside output. The ledger stores the two disjoint, and a served row adds them: output is 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, finish and context_limit. finish comes from OpenCode's finish, Pi's stopReason, or the stored stop reason of Claude Code and Oh My Pi. context_limit comes from Codex.
  • request_body_len is optional and is not populated by the current readers. On a prompt drop, request_size is present when both adjacent steps report body lengths: smaller when the current body is shorter, otherwise not_smaller.
  • prompt, which is input plus cache read plus cache write.
  • retention, verdict and prompt_dropped. retention is 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 stable with no retention. Otherwise, expected is that step's cache read plus its growth, which is its cache write when positive and otherwise its uncached input. retention is this step's cache read divided by expected.
  • Codex reports no cache writes, so its retention is the cache read divided by the cache read plus the uncached input. When that sum is zero, the verdict is unknown.
  • A cache read of zero is full_bust. Otherwise, a retention of at least 0.95 is stable, at least 0.80 is warning, and anything lower is bust.
  • prompt_dropped is true when prompt is 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:

  • harness and session.
  • 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 is bust or full_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.

4.2 Live step feed (contract v1)

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.

usage.steps.subscribe

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).

usage.steps.read

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"
}
  • rows are oldest first. next is the cursor to pass as after for the following page, or null when the reply reached through. through is the newest committed cursor.
  • A row's identity is harness plus step_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".

  • source is file for a step astrocyte read from a harness store or a runner's durable log, and runner for a step known so far only from a runner's bus event.
  • harness is 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.
  • output includes reasoning, which is a breakdown inside it, as in section 4.1, and cache_write is the sum of every cache lifetime. cache_write_5m and cache_write_1h are present only when the source split them.
  • prompt, retention, verdict (cold_start, stable, warning, bust, full_bust, unknown), prompt_dropped and request_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.

How astrocyte serves the feed

  • The feed is the step_feed table (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 its harness and step_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 is c followed 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.steps serves 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.v1 events of the runners the bus naming table grants astrocyte; today those are broca, thalamus-harness and thalamus. 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_dropped and request_size are computed by the same rules as a file row's, over the session's recorded steps and its other runner rows. run_id, and cache_write_5m and cache_write_1h when the event split them, are sent only on runner rows. output includes reasoning unless 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 be reserved:callosum as 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.

4.3 Read watermark (log-read-watermark/v1)

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 refused invalid_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's Direct principal, any other module), or its source is excluded in harness_ingest (named in exclude, or enabled: 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.

4.4 Runs cost reads (usage-runs-v1)

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 are opencode (both versions), claude-code, codex, pi, gemini, goose, omp, kimi.
  • {"kind":"run","harness":"broca","run_id":r}: BROCA run_ref, verbatim.
  • {"kind":"flow","harness":"broca","flow_id":f}: BROCA batch flow_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.

5. Calls astrocyte makes

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.

6. Not guaranteed

  • budget.verdict enforces no cap. Callers must not read allow as approval of a budget.
  • A timed-out write has an unknown outcome. Clients must not retry a write blindly; spend.correction.ingest is the one write with a retry key.
  • Served writes carry no operator identity. The caller is whoever subc admitted to the route.