Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions docs/agents/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,118 @@ hash state can contain raw trailing transcript bytes. They retain staged parsing
but publish projected messages and tool metadata without staged output. Late
result updates use the same projection as newly inserted messages.

### Rate-limit snapshots

`rate_limit_snapshots` is a SQLite-only vendor-data table, in the same
category as `cursor_usage_events` and the four Codex incremental-import
tables above: it is out of scope for the SQLite/PostgreSQL/DuckDB parity
rule below. It is vendor-keyed (`vendor`, `'codex'` today) from the start
so a future vendor can add rows without a schema change or a migration:
`account_id`, `account_label`, `scope_label`, and `details` are reserved
for a vendor whose rate-limit source has that shape, and stay `''` on
every Codex row -- verified against `~/.codex/sessions` rollouts: no
account id, user id, email, or org field appears in `session_meta` or
`token_count` payloads, so a Codex snapshot's identity omits an account
entirely (see `docs/internal/session-format-sources.md`). An
`account_id` filter scopes vendors that have
accounts, so both `LatestRateLimitSnapshots` and
`RateLimitSnapshotHistory` match a nonempty `account_id` against
`(account_id = '' OR account_id = ?)` rather than a bare equality,
letting an account-less vendor's rows (Codex today) pass through
instead of being excluded by someone else's account filter.

It stores each `rate_limits` observation a Codex `token_count` event
carries beside `info.last_token_usage` (one row per rate-limit window).
It is written with an upsert against a unique `dedup_key` (source
session id + observed timestamp + limit id + window kind + `ordinal`),
never a delete-then-reinsert, so both a full parse (which sees the
whole transcript every time) and an incremental parse (which only sees
the appended tail) can write to it without duplicating or losing rows.
A `dedup_key` collision against a row whose `session_id` is already
NULL (a resync copy for a session absent from the destination -- see
`CopyRateLimitSnapshotsFrom`) reattaches that row to the incoming
session and refreshes its other fields instead of being ignored, since
the copy preserves `dedup_key` unchanged and the session reappearing
later (a fresh parse reproducing the identical key) would otherwise
collide with, and lose to, the stale detached row forever. A collision
against a row that already has a session attached is still a no-op.
`ordinal` is the source `token_count` event's 0-based
position among every `token_count` event in the rollout file, assigned
by the parser (`ParsedRateLimitSnapshot.Ordinal`) and stable across a
full parse, an incremental tail parse resuming from a cached or
reseeded cursor, and any later re-parse of the same file: without it,
two distinct `token_count` events landing on the same observed-at
second would produce the same `dedup_key`, and the second event's row
would be silently dropped by `INSERT OR IGNORE` instead of persisted.
It also participates in `observation_key` for the same reason -- two
such events would otherwise merge their sibling windows into one
`LatestRateLimitSnapshots` bucket. A single malformed
observation (e.g. one missing `limit_id`) is skipped rather than failing
the whole write, so it cannot take down the rest of the batch or the
session ingestion it rode in on. `session_id` is nullable (`ON DELETE SET
NULL`) so a row survives its source session being deleted. `resets_at` is
also nullable: Codex can report a window with no reset time, and that is
kept distinct from a window that resets at the unix epoch all the way
through the Go types and the API response (an absent field, not `0`), so
the Usage page can tell "no known reset time" apart from "resets right
now". A full resync copies existing rows into the replacement archive
the same way model pricing is copied (`CopyRateLimitSnapshotsFrom`), but
only after orphaned sessions are restored: `session_id` is a foreign
key, and copying before restoration could violate it for a snapshot
belonging to an orphaned session, aborting the whole copy. The copy also
NULLs `session_id` for any row whose session does not exist in the
destination even after restoration -- a session resync intentionally
does not restore, such as one superseded by a reparse under a different
id, or one excluded as parser-excluded -- rather than copying that id
unchanged and violating the same foreign key; `dedup_key` is preserved
from the source unchanged either way. The copy also skips any row whose
session was rebuilt by the resync's own reparse rather than merely
restored: it copies a row only when `session_id` is NULL, absent from
the destination's `sessions` table, or one of the ids the orphan copy
restored without reparsing, so a rebuilt session's superseded rows
cannot resurrect on top of its fresh, current ones.
`LatestRateLimitSnapshots` (the
`/current` endpoint) resolves "latest" per (vendor, machine, account_id,
limit_id) bucket -- one level above window_kind -- and returns every
window belonging to that bucket's single newest observation. Ranking
each window_kind independently instead would let a window that stops
being reported (e.g. a session moving from primary+secondary to
primary-only) keep surfacing its last-known row forever, since no newer
row for that window_kind ever arrives to supersede it. `plan_type` is
deliberately not part of this bucket, or of any other window identity in
this codebase: Codex reports it as a label that can flip between
`"pro"` and empty for the same window from one observation to the next,
not a stable identity component, so partitioning on it would let a stale
plan-keyed bucket coexist alongside the newest observation instead of
being superseded by it, and (on the history/frontend side) would split
one window's history across two chart series or silently drop half of
it. `plan_type` and `limit_name` are instead resolved independently as
the latest non-empty value ever observed for the bucket -- so a bucket
whose newest observation happens to omit one or both labels still
displays the last value seen for it rather than blanking the card --
and are otherwise pure display labels carried on each row, never a
grouping key. `RateLimitSnapshotHistory` still returns every row over
time regardless of the current-snapshot grouping, and its query,
downsampling, and frontend cache key never filter or key by `plan_type`
either. A window's identity, for both the current-snapshot and history
paths, is (vendor, machine, account_id, limit_id, window_kind) -- the
same fields `RateLimitCardIdentity` on the frontend groups by. PostgreSQL
and DuckDB implement the read-side
`Store` methods as no-ops returning an empty result, so the Usage page's
rate-limits section is simply hidden when either backend is the active
read store. `LatestRateLimitSnapshots` and `RateLimitSnapshotHistory` both
probe once (cached per `*DB`) whether `rate_limit_snapshots` exists and
return an empty result instead of erroring when it does not, since
`OpenReadOnly` tolerates an older, otherwise-compatible archive that
predates the table. A write that replaces a session's messages wholesale
(an authoritative reparse superseding a fallback parsed at
`parser.DataVersionNeedsRetry`, or any other full delete-and-reinsert)
must delete that session's `rate_limit_snapshots` rows in the same
transaction before inserting the new set via
`InsertRateLimitSnapshotsReplacingSession`, while a normal incremental
parse keeps appending through `InsertRateLimitSnapshots` without
deleting.

## Archive Content Policy

`archive_content` (`internal/config.ArchiveContent`) narrows what the SQLite
Expand Down
24 changes: 24 additions & 0 deletions docs/internal/session-format-sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,30 @@ add an archived or maintained mirror without replacing the original identity.
[LiteLLM's Bedrock row](https://github.com/BerriAI/litellm/blob/fbed17d567a62b14b8fc7d9ef13c5cd61a8d1ae0/model_prices_and_context_window.json).
Remove that supplemental row when the shared snapshot includes it.

- **Rate limits (2026-09-09):** `token_count` events carry a `rate_limits`
object beside `info.last_token_usage`, confirmed against the pinned
[protocol types](https://github.com/openai/codex/blob/406dc9239492aff6d295cca5eebe2a548548d42f/codex-rs/protocol/src/protocol.rs):
a `RateLimitSnapshot` (`limit_id`, `limit_name`, `primary`/`secondary`
`RateLimitWindow`s, `credits`, `plan_type`, `rate_limit_reached_type`,
`individual_limit`, `spend_control_reached`) nested in `TokenCountEvent`.
Each `RateLimitWindow` is `{used_percent: f64, window_minutes: Option<i64>,
resets_at: Option<i64>}`; `CreditsSnapshot` is `{has_credits: bool,
unlimited: bool, balance: Option<String>}`. `rate_limits` itself, and both
windows independently, are nullable — observed as null for events with no
rate-limit data and for limit ids (e.g. `premium`) that report credits only.
Cross-checked against a local corpus of 218,803 `token_count` events from
2026-09 rollouts: 216,394 carried only a `primary` window, 2,408 carried
both `primary` and `secondary` (e.g. a 5h window plus a 7-day window), and
1 carried neither. No account, user, or organization identifier field
appears anywhere in this protocol file, nor in any `session_meta` or
`token_count` payload across that corpus (also grepped for `account_id`,
`chatgpt_account`, `account_email`, `user_email`, and `org_id`; every hit
was inside transcript content, not a real identity field) — so
`rate_limit_snapshots` keys a Codex snapshot by (machine, limit_id,
window_kind) with an `account_id` column reserved for a future
Codex release that starts reporting one. See
`internal/db/schema.sql` and `docs/token-usage.md`.

- **Agentsview:** `internal/parser/codex.go` and
`internal/parser/codex_provider.go`; usage is taken from the last-turn
counters rather than repeatedly counting cumulative totals. Fork and
Expand Down
42 changes: 42 additions & 0 deletions docs/token-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,48 @@ cache creation without earning the reads back.

![Cache efficiency panel](/docs/assets/generated/screenshots/usage-cache-efficiency.png)

### Rate Limits

When the archive contains sessions from a vendor whose rate limits agentsview
tracks (Codex CLI today), the Usage page adds a **Rate limits** section below
the summary cards, grouped by vendor and then by account (Codex reports no
account identity, so its group is keyed by machine instead): one card per
rate-limit window (Codex reports up to two — a short `primary` window and a
longer `secondary` window, e.g. 5 hours and 7 days), showing how much of the
window is used, when it resets, the plan type, and the current credit
balance, plus a small history chart of used-percent over the selected date
range, plotted on a real time axis so the spacing between points reflects how
much time actually elapsed. The section is hidden entirely for archives with
no rate-limit data.

Codex CLI writes a `rate_limits` object into its `token_count` events;
agentsview persists each observation into a vendor-keyed
`rate_limit_snapshots` table (SQLite only — see `docs/agents/storage.md`) and
serves it over:

```http
GET /api/v1/rate-limits/current
GET /api/v1/rate-limits/history
```

Both accept `vendor`, `account_id`, and `machine` filters (`agent` is a
deprecated alias for `vendor`); `history` additionally accepts `limit_id`,
`window`, `since`, `until`, and `max_points` (default 500), but not
`plan_type`: a window's identity for history purposes is (vendor,
machine, account_id, limit_id, window_kind), and the returned series
(and its downsampling) covers the whole window regardless of the
`plan_type` label each observation happens to carry. A `history`
request spanning more observations than `max_points` is downsampled: the
range is divided into `max_points` equal-width time buckets and only the
most recently observed row in each bucket is kept, so a wide date range
does not grow the response (or the chart's point count) unboundedly.
Codex rollouts do not currently report a
stable per-account identifier, so Codex snapshots are grouped by machine,
`limit_id`, and window kind rather than by account — see the Codex
entry in `docs/internal/session-format-sources.md` for the evidence and
`internal/db/schema.sql` for the exact column set, including the columns
reserved for a future account-keyed vendor.

The dashboard reads from the same `model_pricing` table that backs the CLI
commands below, so the numbers line up exactly with what
`agentsview usage daily` prints.
Expand Down
16 changes: 15 additions & 1 deletion frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2038,5 +2038,19 @@
"settings_tool_result_images_keep": "Keep",
"settings_tool_result_images_drop": "Drop",
"settings_tool_result_images_hint": "Stored results keep the policy they were written under, and switching back to Keep cannot restore payloads already removed. Run agentsview db strip --images to project existing rows.",
"settings_tool_result_images_restart_notice": "Restart the AgentsView daemon to apply this change to newly ingested sessions."
"settings_tool_result_images_restart_notice": "Restart the AgentsView daemon to apply this change to newly ingested sessions.",
"rate_limits_section_title": "Rate limits",
"rate_limits_plan_label": "Plan",
"rate_limits_credits_label": "Credits",
"rate_limits_credits_unlimited": "Unlimited",
"rate_limits_resets_in": "Resets in {value}",
"rate_limits_resets_now": "Resets now",
"rate_limits_used_percent_aria": "Rate limit usage",
"rate_limits_history_aria": "Rate limit usage over time",
"rate_limits_window_weekly": "Weekly limit",
"rate_limits_window_session": "Session limit",
"rate_limits_window_generic": "{duration} limit",
"rate_limits_card_header": "{window} [{name}]",
"rate_limits_vendor_codex": "Codex",
"rate_limits_resets_unknown": "Reset time unknown"
}
16 changes: 15 additions & 1 deletion frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -2037,5 +2037,19 @@
"settings_tool_result_images_keep": "Conserver",
"settings_tool_result_images_drop": "Supprimer",
"settings_tool_result_images_hint": "Les résultats déjà stockés gardent la règle appliquée lors de leur écriture, et revenir à Conserver ne restaure pas les données déjà supprimées. Lancez agentsview db strip --images pour traiter les lignes existantes.",
"settings_tool_result_images_restart_notice": "Redémarrez le démon AgentsView pour appliquer ce changement aux nouvelles sessions ingérées."
"settings_tool_result_images_restart_notice": "Redémarrez le démon AgentsView pour appliquer ce changement aux nouvelles sessions ingérées.",
"rate_limits_section_title": "Limites de débit",
"rate_limits_plan_label": "Forfait",
"rate_limits_credits_label": "Crédits",
"rate_limits_credits_unlimited": "Illimité",
"rate_limits_resets_in": "Réinitialisation dans {value}",
"rate_limits_resets_now": "Réinitialisation immédiate",
"rate_limits_used_percent_aria": "Utilisation de la limite de débit",
"rate_limits_history_aria": "Historique d'utilisation de la limite de débit",
"rate_limits_window_weekly": "Limite hebdomadaire",
"rate_limits_window_session": "Limite de session",
"rate_limits_window_generic": "Limite {duration}",
"rate_limits_card_header": "{window} [{name}]",
"rate_limits_vendor_codex": "Codex",
"rate_limits_resets_unknown": "Heure de réinitialisation inconnue"
}
16 changes: 15 additions & 1 deletion frontend/messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -2038,5 +2038,19 @@
"settings_tool_result_images_keep": "保持",
"settings_tool_result_images_drop": "破棄",
"settings_tool_result_images_hint": "保存済みの結果は書き込み時のポリシーを保ち、保持に戻しても削除済みのデータは復元できません。既存の行は agentsview db strip --images で処理してください。",
"settings_tool_result_images_restart_notice": "この変更を新しく取り込むセッションに適用するには、AgentsView デーモンを再起動してください。"
"settings_tool_result_images_restart_notice": "この変更を新しく取り込むセッションに適用するには、AgentsView デーモンを再起動してください。",
"rate_limits_section_title": "レート制限",
"rate_limits_plan_label": "プラン",
"rate_limits_credits_label": "クレジット",
"rate_limits_credits_unlimited": "無制限",
"rate_limits_resets_in": "{value}後にリセット",
"rate_limits_resets_now": "今すぐリセット",
"rate_limits_used_percent_aria": "レート制限の使用状況",
"rate_limits_history_aria": "レート制限の使用状況の推移",
"rate_limits_window_weekly": "週間上限",
"rate_limits_window_session": "セッション上限",
"rate_limits_window_generic": "{duration}上限",
"rate_limits_card_header": "{window} [{name}]",
"rate_limits_vendor_codex": "Codex",
"rate_limits_resets_unknown": "リセット時刻不明"
}
16 changes: 15 additions & 1 deletion frontend/messages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -1973,5 +1973,19 @@
"settings_tool_result_images_keep": "유지",
"settings_tool_result_images_drop": "삭제",
"settings_tool_result_images_hint": "이미 저장된 결과는 기록될 당시의 정책을 유지하며, 유지로 되돌려도 이미 삭제된 데이터는 복원되지 않습니다. 기존 행은 agentsview db strip --images 로 처리합니다.",
"settings_tool_result_images_restart_notice": "이 변경을 새로 수집되는 세션에 적용하려면 AgentsView 데몬을 다시 시작하세요."
"settings_tool_result_images_restart_notice": "이 변경을 새로 수집되는 세션에 적용하려면 AgentsView 데몬을 다시 시작하세요.",
"rate_limits_section_title": "속도 제한",
"rate_limits_plan_label": "요금제",
"rate_limits_credits_label": "크레딧",
"rate_limits_credits_unlimited": "무제한",
"rate_limits_resets_in": "{value} 후 초기화",
"rate_limits_resets_now": "지금 초기화",
"rate_limits_used_percent_aria": "속도 제한 사용량",
"rate_limits_history_aria": "속도 제한 사용량 추이",
"rate_limits_window_weekly": "주간 한도",
"rate_limits_window_session": "세션 한도",
"rate_limits_window_generic": "{duration} 한도",
"rate_limits_card_header": "{window} [{name}]",
"rate_limits_vendor_codex": "Codex",
"rate_limits_resets_unknown": "초기화 시간 알 수 없음"
}
16 changes: 15 additions & 1 deletion frontend/messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1973,5 +1973,19 @@
"settings_tool_result_images_keep": "保留",
"settings_tool_result_images_drop": "丢弃",
"settings_tool_result_images_hint": "已存储的结果保持写入时的策略,改回「保留」也无法恢复已删除的数据。运行 agentsview db strip --images 可处理已有记录。",
"settings_tool_result_images_restart_notice": "重启 AgentsView 守护进程后,此更改才会应用到新采集的会话。"
"settings_tool_result_images_restart_notice": "重启 AgentsView 守护进程后,此更改才会应用到新采集的会话。",
"rate_limits_section_title": "速率限制",
"rate_limits_plan_label": "套餐",
"rate_limits_credits_label": "余额",
"rate_limits_credits_unlimited": "不限量",
"rate_limits_resets_in": "{value} 后重置",
"rate_limits_resets_now": "现在重置",
"rate_limits_used_percent_aria": "速率限制使用情况",
"rate_limits_history_aria": "速率限制使用趋势",
"rate_limits_window_weekly": "每周限额",
"rate_limits_window_session": "会话限额",
"rate_limits_window_generic": "{duration} 限额",
"rate_limits_card_header": "{window} [{name}]",
"rate_limits_vendor_codex": "Codex",
"rate_limits_resets_unknown": "重置时间未知"
}
Loading