ResearchAgent Telemetry

Token & Cost Telemetry

Status: Partially implemented (Phase 3, Agent Orchestration Program)

Implementation note: Both halves of the design have a landed Capsule slice. The account-quota side shipped with the Capsule usage overlay (a Capsule-local Turso account_usage_snapshots cache feeding the focused status signal + provider overlay). The token-spend side shipped in #532: the in-container token monitor reads each agent's provider logs (Claude/Codex/Kimi JSONL, Amp thread JSON, OpenCode via the async turso reader), recomputes per-session totals idempotently each poll, fills cost from the provider stream or the static pricing table, and the daemon polls it on a self-throttled 30s/60s cadence; totals are exposed as TokenUsageSummary over the control channel and printed by jackin-capsule token-usage <id>. That code (crates/jackin-usage/src/token_monitor.rs, re-exported by crates/jackin-capsule/src/lib.rs) is the source of truth; this page tracks only the remaining work below.

Adapter hardening still open: each provider reader currently globs all of that provider's session logs in the container and attributes them to the polling session, so two concurrent same-provider agents see the union of both — per-session file correlation (by cwd / session metadata) is the remaining accuracy work, and the field/path assumptions want validation against real provider output (Implementation sequence steps 2–3). Host-global telemetry still open: per-instance usage_samples storage substrate, workspace/session spend summaries, the shared account/* and usage/* daemon API + jackin usage --json CLI, and the console/Desktop spend badges. The quota-window cards and richer model catalog were deliberately dropped from the Capsule slice (they duplicate the shipped account-quota plane).

Problem

jackin has no idea how much an agent has cost the operator. Token usage, cache reads/writes, dollar cost — all of it is invisible. For a developer running one Claude session this is fine; for a team running several parallel agents on different projects, it's expensive ignorance:

  • The operator can't tell which workspace burned 10× more tokens than the others.
  • The autonomous queue (Phase 4) can't enforce a per-task or per-day budget.
  • The cost model can't be optimized — operators don't see when prompt caching is saving them money vs. when it isn't.

multicode tracks this per-workspace and renders it in their TUI. CodexBar and OpenUsage show the adjacent desktop-account version of the same problem: operators do not only care about per-session tokens, they care about subscription windows, reset times, weekly caps, provider health, and whether starting one more agent will burn through an account limit.

Why It Matters

  • It's the simplest cost-awareness primitive that doesn't require a vendor-specific billing integration.
  • It makes declarative resource limits feel finished — operators see RAM/CPU usage, but cost is the dimension they most care about for AI agents.
  • It enables future budget gates ("don't queue another task if this workspace exceeded $X today") without those gates having to reinvent the underlying tracking.

Inspiration in multicode

Sources:

multicode's usage_aggregation_service (see lib/src/services/usage_aggregation_service.rs) parses the OpenCode message history, sums input/output tokens per session, multiplies by a synthetic per-model cost table, and exposes:

  • usage_total_tokens: u64
  • usage_total_cost: f64
  • usage_cpu_percent: u16 (CPU)
  • usage_ram_bytes: u64 (RAM)
  • oom_kill_count: u64

These live on the workspace snapshot (transient — lost across TUI restarts). The TUI renders cost and tokens as columns in the workspace table.

The cost table is hardcoded per model. Re-using their per-1000-token prices and approach is fine; the more interesting work is making jackin pricing pluggable across runtimes.

Inspiration in CodexBar and OpenUsage

Sources:

CodexBar and OpenUsage are better references for account-limit telemetry than for jackin session telemetry. Both are menu-bar-first tools that collect provider usage from existing local state or provider APIs, then render quota bars, reset countdowns, and stale/error states. CodexBar is broader and deeper on local/provider source handling: it supports Claude, Codex, Amp, Kimi, OpenCode, and many providers outside jackin; exposes configurable refresh cadences; renders reset windows; ships a CLI for local cost scans; and treats browser cookies / API keys / OAuth / local CLI state as provider-specific source strategies. OpenUsage is the cleaner product reference: a lightweight menu-bar panel, automatic refresh, global shortcut, local HTTP API on 127.0.0.1, proxy support, and an explicit plugin direction for adding providers.

Useful ideas:

  • Track account quota snapshots separately from per-session token samples. A Claude weekly limit, Codex session window, Amp free-tier usage bucket, or OpenCode workspace subscription window is not the same entity as a single assistant-message token sample.
  • Store reset-window metadata: provider, account label, plan/source, usage amount, limit amount when known, reset time, stale/error state, and provenance.
  • Keep refresh state resilient: background refresh, stale-but-usable cached values, throttled errors when prior data exists, and a clear repair/login state when no data can be trusted.
  • The daemon's account/* and usage/* endpoints (specified below) are the local read API; resist the temptation to ship a second always-on HTTP server.
  • Treat provider adapters as a small, typed set for the built-in jackin runtimes (claude, codex, amp, kimi, opencode) instead of inheriting CodexBar/OpenUsage's generic provider marketplace shape.
  • Prefer runtime-owned credentials or credentials forwarded by jackin over host-wide browser-cookie scraping. Cookie import can exist as an explicit diagnostic or fallback, but it must not become the primary path for account state jackin already launched.
  • Preserve the host-mutation rule: reading host-side auth/config is allowed when surfaced; writing provider config, Keychain entries, browser profiles, or host dotfiles is not part of telemetry refresh.

What not to copy:

  • Do not build a generic subscription tracker. jackin should monitor the accounts and runtimes it launches, plus explicit operator opt-ins, not every AI provider installed on the host.
  • Do not make hidden browser-cookie scraping the happy path. It increases permissions, fragility, and trust cost.
  • Do not expose broad host filesystem scans. Saved workspaces, running role containers, known agent auth locations, and explicit provider settings are enough.

Better accuracy references

CodexBar and OpenUsage remain useful UI references, but they are not the reliability baseline. The better roadmap inputs are:

  • Tokemonhttps://github.com/richyparr/tokemon and https://www.tokemon.ai/. Best reference for Claude-specific accuracy: OAuth / provider usage endpoint as primary truth for 5-hour and rolling-week quota, JSONL logs as fallback for local token/cost estimation, multi-profile support, burn-rate projection, warning thresholds, and a terminal statusline export. The main caution is scope: it is Claude-first and small-community, so jackin should borrow the source-priority model rather than treat the implementation as canonical.
  • Token Trackerhttps://github.com/mm7894215/TokenTracker and https://www.tokentracker.cc/. Best reference for local-first project attribution across many agent CLIs: hooks where available, passive log readers where hooks are not available, normalized buckets keyed by source/model/time, local dashboard, menu bar app, widgets, status --json, and explicit "token counts only, never prompts" positioning. The main caution is that auto-installing hooks into host tool configs would violate the jackin host-mutation rule unless the operator explicitly opts in; jackin already owns role launch and can install container-side hooks without touching host configs.
  • Brimhttps://getbrim.tech/. Best reference for trust-minimized UX: narrow provider scope, native menu bar rings, manual refresh for server quota, local log reads for token/cost breakdown, no background network traffic unless requested. The main caution is that manual refresh alone is too passive for the jackin daemon-backed status bar; jackin should use a bounded cache TTL with a visible freshness label.
  • MeterBarhttps://github.com/shipshitdev/meterbar.app and https://meterbar.app/. Useful reference for a simple native account dashboard, Keychain-local credentials, CLI JSON output, and provider API source mapping. The caution is that direct host credential-file access must be surfaced and scoped; jackin should prefer daemon-owned account connectors and 1Password / Keychain / existing forwarded auth sources over app-level ad hoc reads.
  • Control Towerhttps://github.com/krishcdbry/ControlTower and https://control-tower.dev/. Useful as a product comparison for multi-account, alerts, usage history, quiet hours, and provider detail pages, but the public project appears much less mature than Tokemon, Token Tracker, or CodexBar. Treat it as UI comparison only.

jackin needs two truth channels, not one:

  1. Quota availability comes from the provider's server-side usage/quota source whenever available. This is the number that answers "will this account hit a subscription or session limit?" Local JSONL token totals are not authoritative for this question because providers can apply hidden weighting, cache accounting, model-specific quotas, or server-side resets.
  2. Workspace/project cost comes from jackin-owned session attribution: runtime hooks, structured transcripts, local logs, and Capsule/session metadata. This is the number that answers "which workspace burned the tokens?" Provider quota pages usually cannot answer that at jackin workspace granularity.

Any UI that mixes these numbers must label them separately: Account availability and Workspace spend. A single percentage without provenance is a bug.

A small usage adapter per runtime, parallel to (and consuming the same input stream as) the agent runtime status and tag protocol parsers. Output samples land in the persistent storage layer's usage_samples table.

Sample event

pub struct UsageSample {
    pub at: SystemTime,                 // wall-clock at sample emission; SQL stores epoch seconds (occurred_at)
    pub session_id: Option<i64>,        // Capsule session emitting the sample, when known
    pub workspace: Option<String>,      // jackin workspace name for attribution
    pub provider: Agent,                // canonical runtime enum from src/agent/mod.rs
    pub model: String,                  // must match a key in the pricing table; example: "claude-opus-4-7"
    pub token_input: u64,               // delta per assistant message (not cumulative)
    pub token_output: u64,
    pub token_cache_read: u64,
    pub token_cache_write: u64,
    pub cost_usd_micros: u64,           // computed from (provider, model, tokens) at sample time; never recomputed
    pub cost_source: CostSource,        // ExplicitUsd | PriceTable | Unpriced
}

pub enum CostSource { ExplicitUsd, PriceTable, Unpriced }

Per-instance DB rows mirror the value fields; the SQL row adds a surrogate id INTEGER PRIMARY KEY, stores at as occurred_at INTEGER (epoch seconds), carries cost_source TEXT, and may carry a source_hash dedupe key for log scanners or runtime streams. The hash is derived from source identity and event bytes so refreshes can be idempotent without storing prompts, completions, or raw log text. instance is implicit in the DB file path and is not stored per-row.

cost_usd_micros is integer to avoid floating-point summation drift across thousands of samples; renderers divide by 1e6 for display. cost_source is load-bearing provenance: provider-reported USD fields are ExplicitUsd, verified model price tables are PriceTable, and unknown/Amp pass-through rows without explicit USD stay Unpriced instead of receiving a synthetic zero. The delta per assistant message rule matters: Claude stream-json emits cumulative chunks, Codex app-server emits per-event deltas — adapters convert to deltas before emitting UsageSample, so SQL aggregates are simple sums.

model: String is loose by intent in V1: the pricing table is the closed set, and an unknown model is logged with no cost_usd_micros plus cost_source = Unpriced rather than rejected. Once the pricing table stabilizes, the field should tighten to a Model newtype whose constructor normalizes against the pricing table and rejects unknown variants — that is the right encapsulation but is a later tightening, not a V1 blocker.

Account quota snapshots

The snapshot splits into three concerns — identity, quota numbers, cache metadata — so the type prevents a renderer from showing quota without acknowledging freshness:

pub struct AccountUsageSnapshot {
    pub provider: Agent,                  // canonical runtime enum
    pub account_label: String,            // operator-facing, never a raw token
    pub quota: AccountQuota,
    pub cache: SnapshotCacheMeta,
}

pub struct AccountQuota {
    pub window: UsageWindow,              // Session, Daily, Weekly, Monthly, Credits
    pub usage: Option<QuotaUsage>,        // None when status is NeedsLogin / Unsupported / Error
    pub resets_at: Option<SystemTime>,    // next provider reset boundary; orthogonal to usage
}

pub struct QuotaUsage {
    pub used: UsageAmount,                // current consumption; always present when QuotaUsage is Some
    pub limit: Option<UsageAmount>,       // cap; None for providers that report used-only (e.g. credits remaining)
}

pub struct SnapshotCacheMeta {
    pub fetched_at: SystemTime,           // when the daemon read this snapshot
    pub expires_at: Option<SystemTime>,   // when the daemon must refresh; None if provider doesn't say
    pub source: UsageSource,
    pub confidence: UsageConfidence,
    pub status: UsageSnapshotStatus,
    pub last_error: Option<UsageRefreshError>,
}

Per-message samples answer "what did this agent session cost?" Account snapshots answer "can I safely start more work on this account before the next reset?" The two models stay separate so renderers can mix them without confusing readers about provenance.

confidence answers "how much do we trust this number?" — provenance. status answers "is this snapshot useful right now?" — lifecycle. The two axes are intentionally separate; a snapshot can be provider_authoritative + stale (we have a trusted number that's past TTL) or local_estimate + fresh (best-effort number we just computed). The split into quota (the numbers) and cache (the metadata) means a renderer cannot present quota without cache in scope, so the source / confidence / status / freshness labels are always reachable from the same struct.

Supporting types:

pub enum UsageSource { RuntimeApi, LocalLog, CliAuth, BrowserCookie, Manual }
pub enum UsageConfidence { ProviderAuthoritative, LocalEstimate, Fallback }
pub enum UsageWindow { Session, Daily, Weekly, Monthly, Credits }
pub enum UsageSnapshotStatus { Fresh, Stale, NeedsLogin, Unsupported, Error }

pub struct UsageAmount {
    pub amount: u64,
    pub unit: &'static str,             // "tokens" | "messages" | "requests" | "usd_micros"
}

pub struct UsageRefreshError {
    pub kind: RefreshErrorKind,         // Network | Auth | RateLimited | Schema | Other
    pub at: SystemTime,                 // attempt timestamp
    pub message: String,                // operator-facing diagnostic, never contains secrets
}

Constructors so adapters cannot assemble inconsistent (source, confidence, status) triples:

impl AccountUsageSnapshot {
    pub fn from_provider_api(provider: Agent, label: String, quota: AccountQuota, fetched_at: SystemTime, expires_at: Option<SystemTime>) -> Self;  // (RuntimeApi, ProviderAuthoritative, Fresh)
    pub fn from_local_log(provider: Agent, label: String, quota: AccountQuota, fetched_at: SystemTime) -> Self;                                     // (LocalLog,   LocalEstimate,        Fresh)
    pub fn from_browser_cookie(provider: Agent, label: String, quota: AccountQuota, fetched_at: SystemTime) -> Self;                                // (BrowserCookie, Fallback,           Fresh)
    pub fn unsupported(provider: Agent, label: String) -> Self;                                                                                     // (Manual, Fallback, Unsupported); quota.usage = None
    pub fn needs_login(provider: Agent, label: String, source: UsageSource) -> Self;                                                                // (source, mapped-confidence, NeedsLogin); quota.usage = None
    pub fn errored(prior: &Self, error: UsageRefreshError) -> Self;                                                                                 // preserves prior quota; status = Stale, last_error = Some(error)
}

Constructor callers cannot stamp (BrowserCookie, ProviderAuthoritative, Fresh) or other illegal triples; field-by-field assembly stays available only inside the usage::snapshot module that owns the writer.

Cross-field invariants the daemon writer must uphold (none are expressible in the type system; failing them is a bug):

  • quota.usage.unwrap().used.unit == quota.usage.unwrap().limit.unwrap().unit whenever limit is Some (renderers compare amounts on the same scale).
  • cache.last_error.is_some() ⇔ cache.status ∈ { Error, Stale } (and only when the stale state came from a failed refresh).
  • (source, confidence) defaults to (RuntimeApi → ProviderAuthoritative), (LocalLog | CliAuth → LocalEstimate), (BrowserCookie | Manual → Fallback); adapters override only when a runtime_api response is demonstrably stale.

SQL flattens this nested shape: UsageAmount becomes <field>_amount INTEGER + <field>_unit TEXT column pairs; window: UsageWindow serializes as window_kind TEXT; UsageRefreshError serializes to last_error TEXT as JSON; the quota / cache split is conceptual only (one row holds both halves; see the daemon-global account cache schema). account_key_hash is a write-time hash of the provider-stable account identity (OAuth sub, provider user id, or salted hash of the canonical login email — not the editable account_label display string); it lives in the SQL row but not in the value struct, so consumers cannot stamp inconsistent hashes and a label rename does not produce a duplicate snapshot row.

Source priority by runtime

V1 should treat source selection as a deterministic matrix, not as a best-effort scrape. Each adapter returns both a normalized snapshot and a UsageSource / confidence pair so renderers can explain why a number should or should not be trusted.

RuntimeAccount availability priorityWorkspace spend priorityV1 confidence call
codexcodex app-server JSON-RPC (account/read, account/rateLimits/read) or OAuth-backed usage endpoint when daemon-owned credentials are available; optional web dashboard only for explicit extrasCODEX_HOME / ~/.codex session JSONL, including archived sessions, plus runtime events when jackin launches CodexStrong V1 candidate because a non-interactive CLI RPC source exists and local logs are structured enough for attribution
claudeThe console usage endpoint Tokemon reverse-engineered (no Anthropic-published public quota API exists at the time of writing — endpoint is undocumented and may shift; jackin treats it like a scrape, throttles, and degrades to local-estimate when it 4xx/5xxs); Claude CLI /usage probe only as a repair/diagnostic fallback; cookie-backed web API only after explicit operator opt-inClaude JSONL project logs and stream-json usage payloads, deduped by message/request identity where streaming chunks are cumulativeStrong V1 candidate for workspace spend; account-availability confidence drops to local_estimate whenever the undocumented endpoint is unreachable
ampOne-shot amp --no-color usage under daemon timeout, with explicit browser-cookie settings scrape fallback only if the operator opts in outside CapsuleAmp thread/API usage events when available from the launched runtime; local transcript fallback only if structured token counts existCapsule bridge can show Amp Free and individual credits from the CLI; the persistent account cache still needs scheduled refresh/backoff
kimiKimi For Coding billing endpoint from KIMI_AUTH_TOKEN / local Kimi Code OAuth token; cookie/JWT web state can be a fallback only if explicitly enabledKimi local logs or runtime hooks if they expose per-turn token usageCapsule bridge can show weekly and rate-limit buckets; the persistent account cache still needs scheduled refresh/backoff
opencodeProvider-specific web dashboard or configured provider account state, not a generic OpenCode account guessOpenCode message history, following multicode's local aggregation approach where compatibleWorkspace spend can arrive before quota; account availability may be unsupported per backing provider

Host-owned telemetry cache

The jackin daemon owns a host-side telemetry cache so every container and UI surface reads the same account state instead of each role container polling providers independently. The cache is a small persistent store under the daemon's data directory and is keyed by account identity, provider, source, window, and workspace/session attribution where applicable.

Rules:

  • The daemon is the only component that refreshes provider quota snapshots by default. Containers can request a refresh through the daemon, but they do not hold independent polling loops.
  • Each snapshot carries fetched_at, expires_at, source, confidence, and last_error. UI surfaces must show stale data as stale rather than silently hiding it.
  • Containers receive read-only access through the Capsule/daemon control channel, not by bind-mounting writable host cache directories into every container. A narrow read-only materialized file at /jackin/run/usage/accounts.json is acceptable for agents that can only read files; the daemon writes the file with tmp+rename atomicity (write to accounts.json.tmp in the same directory, fsync, then rename(2) over accounts.json) so concurrent readers always see a complete document. The payload carries an embedded fetched_at epoch-seconds field per snapshot so readers do not depend on file mtime; a missing file means "no snapshots yet" and is distinct from a stale-but-present file.
  • Cache TTL is provider-specific. Server-side quota endpoints get conservative refresh intervals and backoff on errors; local session samples can update on session events.
  • The cache stores tokens, costs, percentages, reset times, source metadata, and opaque account labels. It never stores prompts, completions, raw conversation text, provider cookies, or bearer tokens.
  • If an account requires authentication, the daemon requests it through existing jackin auth mechanisms: forwarded CLI auth, 1Password references, macOS Keychain integration, or an explicit host-bridge approval. Failed auth is a first-class needs_login / needs_secret state.

Host-side effects

The host-side write this roadmap authorizes is the daemon telemetry cache itself, stored under jackin-owned host state and surfaced through the launch/daemon summary before the feature ships. Telemetry refresh may read existing host-side auth/config sources when the operator has enabled that source, but it must not write provider config, Keychain entries, browser profiles, Git config, shell dotfiles, or any user repository as part of normal refresh.

Any repair action that would change host state, such as connecting a provider account, adding a Keychain item, or enabling a browser-cookie fallback, must be an explicit operator action rather than a background refresh side effect. Containers only receive read-only views through the daemon/Capsule channel or daemon-materialized files under /jackin/run/usage/; they do not own or mutate the host cache.

Storage shape

Per-message samples land in the per-instance usage_samples table; account snapshots land in the daemon-global account_usage_snapshots table. Both schemas live in the persistent storage layer — see usage samples for the per-instance table and daemon-global account cache for the snapshot table. account_key_hash is a stable hash of the provider/account identity so raw email/token values never become primary keys; operator-facing labels stay display-only.

Daemon API contract

Daemon-owned account usage endpoints land before any UI polish. The naming follows the existing daemon convention (daemon/hello, session/list, event/subscribe) — slash-namespaced, two segments:

  • account/list: returns every known account snapshot with freshness, confidence, and active workspace/session references.
  • account/refresh { provider?, account? }: schedules a refresh and returns the last cached snapshot immediately; does not block the UI on provider network calls.
  • usage/workspace { workspace, window }: returns local token/cost attribution from per-instance samples.
  • usage/session { instance, session_id, window }: returns the per-session token/cost summary when the session is still known.
  • usage/focused: active account quota plus session/workspace spend for the currently focused Capsule pane.

These can live in the daemon protocol first. Capsule should consume a narrow read path later: focused provider/account summary in, no provider polling out. If a file bridge is needed for agents that can only read files, the daemon writes a read-only materialized JSON file under /jackin/run/usage/ inside the container; containers never write back to the host telemetry cache.

Capsule-local implementation note: the jackin-capsule control channel now has UsageFocused / UsageRefreshFocused and UsageAccountList message variants. These read the Capsule daemon cache and /jackin/state/usage/telemetry.db. The Capsule daemon persists account quota buckets only. It no longer persists local-log samples, token-bearing runtime-stream JSON samples, workspace summaries, session summaries, or invented price-table estimates. These APIs are not yet the host-global daemon API or final account/list / account/refresh / usage/* CLI surface.

Implementation sequence

  1. Storage substrate. Land the per-instance usage_samples table and daemon-global account_usage_snapshots cache with schema tests and no UI.
  2. Codex account adapter. Use codex app-server / OAuth-backed reads for account availability, bounded with timeouts and launch-failure backoff. Add local Codex JSONL scanner for workspace spend.
  3. Claude account adapter. Use OAuth/provider usage first and local JSONL/stream-json for spend. CLI PTY probing is user-initiated repair or diagnostics, not a background loop.
  4. Shared daemon endpoints. Expose account list, refresh, workspace summary, and session summary; include source, confidence, freshness, and last-error fields in every response.
  5. CLI. Ship jackin usage --json and a compact human view before the Desktop/Capsule work so the data contract can be tested without a native UI.
  6. Console and Desktop surfaces. Add compact badges, then account/detail panels. Badges must carry both axes — UsageConfidence for provenance and UsageSnapshotStatus for lifecycle — before shipping percentage-only displays.
  7. Capsule focused signal. The Capsule/control/TUI bridge has landed: Capsule can request a normalized focused usage view, render a compact focused-provider status label, and open the read-only prefix u overlay from a daemon-owned account snapshot cache. Provider-authoritative quota adapters and scheduled refresh/backoff for the in-container overlay have shipped. The remaining work is to replace the Capsule-local account cache with the final host-global account snapshot service described above. Rendering contract lives in Capsule usage overlay.
  8. Amp/Kimi/OpenCode research adapters. Add provider rows only after each runtime has a non-interactive source contract or an explicit fallback UX. Until then, render honest unsupported/local-estimate states.

Surface contract

Every interactive jackin surface that can launch or focus an agent exposes the active account's quota state inline, with details one action away. The contract:

  • Compact by default: provider plus remaining percent (Claude 80%, Codex 18%) or state (Amp login, stale); use a glyph/ring when text crowds the surface. Prefer "remaining" over "used" language — Claude 18% left is safer than Claude 82% when the operator is moving quickly.
  • Two rows, labeled: when both numbers exist, render Account availability (provider-authoritative) and Workspace spend (local attribution) separately. Never combine into one gauge.
  • Confidence and status visible: every rendered number carries both labels — UsageConfidence for provenance, UsageSnapshotStatus for lifecycle (variants declared once in the snapshot type definition above). Kimi unsupported beats a missing badge. A provider_authoritative + stale snapshot is not healthier than a local_estimate + fresh one; render both axes.
  • Color plus text: pair warning colors with glyphs or labels because the Capsule and console may run in low-color terminals.
  • Focused-agent aware: when the operator is attached to one agent, the visible signal follows that agent's account, not a global average.
  • Repair stays explicit: separate "read-only auth source missing" from "jackin wants to write host state". The latter requires explicit operator approval under the host-side effects rule.

Surfaces consuming the cache:

  • Desktop app. Accounts view is the full inspection surface: every account, provider, source, current availability, reset time, confidence, last refresh, active workspaces, repair actions. Running Agents view shows focused-account quota and workspace/session spend inline.
  • macOS status bar. Most-constrained active account by default — see the most-constrained ordering rule below. Provider glyph plus remaining percent or stale / login state. Menu expands to all accounts with reset timers, source labels, and repair actions.
  • jackin console Accounts / Usage. Lists every known account with source, current availability, reset time, last refresh, and which workspaces currently use it. Workspace and running-agent rows carry compact badges for the attached account.
  • Capsule multiplexer. Focused-tab provider only — see Capsule usage overlay for the rendering contract (status-line layout, prefix u, modal contents, daemon-disconnected and needs-login fallbacks).
  • CLI / automation. jackin usage exposes account, workspace, and session views with --json, including freshness and provenance so scripts do not mistake local estimates for provider-enforced limits.

The contract must answer, for the focused agent: which built-in-runtime accounts jackin knows about, which one this agent is using, how much quota remains, when it resets, how much this workspace/session burned today/this week/lifetime, and which confidence + status pair the displayed limit carries.

Most-constrained ordering

Compare accounts across providers in this fixed priority chain (first hit wins):

  1. status == NeedsLogin or Error — show this first regardless of any percentage; one broken account is the highest-risk surface state.
  2. status == Stale AND age > 2× provider TTL — the snapshot is too old to trust.
  3. status == Fresh AND used.is_some() AND limit.is_some() AND used.unit == limit.unit — rank by remaining percentage = 1 - used.amount / limit.amount. Lowest remaining wins.
  4. status == Fresh AND resets_at.is_some() AND no usable percentage — rank by earliest resets_at. Soonest reset wins.
  5. Tie-break: provider alphabetical (Agent::slug()).

The status bar never compares percentages across different UsageAmount.unit values (a tokens-remaining percentage is not the same scale as a messages-remaining percentage); same-unit percentage comparison is the only legitimate use of step 3.

Adapter shape

Each adapter consumes the runtime's structured output (same input stream as the agent runtime status and tag protocol parsers), resolves source priorities from the matrix above, and emits UsageSample for per-message attribution plus AccountUsageSnapshot for account quota. The adapter knows its runtime; the consumer doesn't.

Hidden constraint not captured in the matrix:

  • Codex: keep bare TUI /status probing out of background refresh; it can trigger interactive auth and break the session.

Pricing table

# ~/.config/jackin/pricing.toml (or section of operator config)

[pricing.models."claude-opus-4-7"]
input_per_million = 15.00
output_per_million = 75.00
cache_read_per_million = 1.50
cache_write_per_million = 18.75

[pricing.models."gpt-5-codex"]
input_per_million = 5.00
output_per_million = 15.00

Operator-overridable; jackin ships a default table for current models. Stale prices are a documentation issue, not a runtime issue — the table is just a multiplier.

When a sample arrives for a model not in the table, jackin records the tokens, leaves cost as null, and prints a one-line warning at next console open ("no pricing for model X — tokens tracked but cost unknown").

Console rendering

The per-agent table (when console resource panel is open) gains a "Cost (today)" column showing the running cost since 00:00 local time. A second Tokens (1h) column may surface the rolling-1-hour rate; defer if cell width gets tight.

A jackin usage <selector> CLI command dumps a structured summary for scripting:

$ jackin usage the-architect --json
{
  "instance": "jackin-the-architect",
  "today_cost_usd": 4.12,
  "today_tokens": { "input": 124000, "output": 38500, ... },
  "lifetime_cost_usd": 67.40
}

Scope (V1)

  • Per-runtime usage adapter consuming the runtime's structured output stream.
  • Per-provider account snapshot adapter for the built-in jackin runtimes, starting with Claude and Codex where local sources are most mature.
  • UsageSample events appended to the storage layer's usage_samples table.
  • AccountUsageSnapshot events appended to storage and exposed through the daemon for the Desktop Agent Hub, jackin console, Capsule multiplexer, and CLI.
  • Host-owned telemetry cache with TTL, stale/error states, and provider-specific refresh backoff.
  • Pricing table with operator override; ship sensible defaults.
  • Console "Cost (today)" column when the resource panel is open.
  • Compact quota badges on Desktop running-agent rows, jackin console workspace/agent rows, and macOS status item.
  • Console / Desktop account panel showing all accounts, quota windows, reset time, workspace attribution, and stale/login-needed state.
  • Capsule multiplexer quota signal — Capsule's rendering contract lives in Capsule usage overlay.
  • jackin usage <selector> CLI with --json and human-readable formatting.
  • Cost computed at sample time and stored, not recomputed on render — preserves history when prices change.

Defer

  • Budget gating ("kill the agent or refuse queue dispatch when daily cost exceeds $X"). Useful but waits — it requires the queue to exist first.
  • Multi-currency. USD only in V1; the internal representation is micros-of-USD, conversion is a render-time concern.
  • Org-level aggregation across operators. Out of scope.
  • Generic provider marketplace / third-party plugin API. Useful later, but first-class support should stay scoped to the built-in jackin runtimes.
  • Browser-cookie import as the primary source for supported runtime accounts. Keep it explicit and fallback-only.
  • Token-savings reporting from prompt caching. Tracked as a column; not separately summarized in V1.
  • Per-tool-call cost attribution (which tool call cost the most). Defer; needs richer event extraction than V1's per-message sampling.

Open Questions

  • What's the canonical input for the Claude usage adapter? Claude Code's stream-json transcript exposes tokens per message; the question is whether jackin attaches as a sidecar consumer or intercepts the runtime's stdout. The latter risks interfering with the operator's own view of the agent. Recommended default: stream- json sidecar, gated behind the same opt-in as the status adapter.
  • Default pricing table source. Hand-maintained in this repo, or fetched from an Anthropic/OpenAI-published source at build time? Recommended: hand-maintained for V1; revisit if it stales out fast.
  • Daily cost reset boundary. Local midnight, UTC midnight, or rolling 24-hour? Recommended: local midnight for the "today" column; CLI exposes any window.

See Also

On this page