From aee868e65ad85384f52ec69130a57a80b8abd786 Mon Sep 17 00:00:00 2001 From: stv1024 Date: Fri, 16 Oct 2026 14:45:04 +0800 Subject: [PATCH 1/6] wip: usage-scope semantics, honest unknowns, and pricing cleanup Experimental checkpoint of usage/cost reporting accuracy work: - shared: TurnRecord.tokenScope (turn/session/unknown), nullable token and cost fields, UsageSnapshot for late cumulative/unknown observations; v1 history stays readable - semantics: only verified harness input conventions; no model-name fallback, freshInput stays unknown when counts are inconsistent - pricing: no private gateway seed prices or fixed FX; full model identity plus explicit aliases, provider scopes no longer match model text, ambiguous entries surface pricingWarning instead of guessing - tracker/turns/read-model: late idle usage refreshes a snapshot card instead of inventing turn deltas; a falling session-cost counter is not treated as a session reset - UI (panel/pill/sidebar/overview/timeline): disclose incomplete usage, partial reference estimates and unknown-cost turns; session token snapshot in pill; old timeline cards label missing scope metadata - tests: new server/read-model.test.ts; 66 tests pass, tsc clean --- AGENTS.md | 14 +- CHANGELOG.md | 10 ++ OVERVIEW.md | 17 +- README.md | 65 ++++---- client/panel.tsx | 60 ++++--- client/pill.tsx | 46 ++++-- client/sidebar.tsx | 2 +- client/timeline.tsx | 5 +- client/ui.tsx | 50 +++--- docs/compatibility.md | 29 ++++ docs/paseo-usage-api.md | 31 +++- images/screenshot-overview.png | Bin 82306 -> 50395 bytes images/screenshot-panel.png | Bin 47644 -> 29172 bytes images/screenshot-pill.jpg | Bin 37405 -> 2428 bytes server/pricing.test.ts | 110 +++++++++---- server/pricing.ts | 275 ++++++++++++--------------------- server/read-model.test.ts | 36 +++++ server/read-model.ts | 24 ++- server/tracker.ts | 91 +++++------ server/turns.test.ts | 21 +++ server/turns.ts | 28 ++-- shared/aggregate.test.ts | 14 +- shared/aggregate.ts | 12 +- shared/ledger.ts | 54 +++++-- shared/pricing.test.ts | 37 +++-- shared/pricing.ts | 83 +++++----- shared/semantics.test.ts | 53 +++---- shared/semantics.ts | 86 +++++------ shared/timeline.ts | 3 +- 29 files changed, 743 insertions(+), 513 deletions(-) create mode 100644 server/read-model.test.ts diff --git a/AGENTS.md b/AGENTS.md index f17fc52..56dd6e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,8 +29,8 @@ paseo agent archive # 测完归档,别 - 服务端 contribute 拿到 `handle/on/before`,`paseo` 在 RPC 和生命周期回调中提供。`before(agent.session_open)` 启动订阅,`on(agent.turn_started)` 覆盖 reload 后已有会话;客户端 ensure 尽早连接已有会话。**新建 agent 在 session_open 时尚未进入目录,不能在此 refresh 新 agent。** - **wire 层 `agent_stream` 没有 `usage_updated`**:轮中 usage 走 `agent_update` upsert 快照(`lastUsage`/`activeTurn`),且必须先 `paseo.agents.list({ subscribe: {} })` 才会推送。turn 生命周期(started/completed/failed/canceled + turnId + 轮末 usage)走 `agents.ref(id).timeline.subscribe`。 - usage 语义(2026-09-04 实测):Claude 轮末 usage 是**逐轮**汇总、`totalCostUsd` 是**会话累计**(所以记录里存 delta + raw 两份);Codex 每次模型请求报一次 `last` 值,多请求轮靠去重求和。快照会把上一轮旧 usage 回放进新一轮——tracker 用 agent 级 `lastObservation` 基线挡掉。 -- **Paseo 丢弃 `cache_creation_input_tokens`**(2026-09-05 从 app.asar 的 `providers/claude/agent.js` `buildResultUsage` ~1424 行验证):`cachedInputTokens` 只映射 `cache_read_input_tokens`,cache write 只用于上下文窗口内部估算、不进 `AgentUsage`。后果:用户输入和工具结果(几乎全是 cache write,计费 1.25× input 价)在 IN 和 CACHE 两列都不可见,IN 通常只有零头(个位数~几十);`costUsd` 仍准确(上游总额含 write)。pricing.ts 的分项估算按牌价直算,总额与估算的差额单列为 `otherUsd`(COST 列下方显示 `+$x.xx`)——不再 rescale 摊进分项。token 数本身的缺口需上游 Paseo 加字段,插件侧无法拿到。 -- **input 语义因 provider 而异**(2026-09-05 从落库记录实测):codex/OpenAI 系的 `inputTokens` **含** cached(如 input 213818 / cached 213252,真实新增仅 566);Claude 的 input **不含** cache read。`semantics.ts` 维护按 provider(回退 model)匹配的语义表,归一化到"input = 未命中缓存的新输入"(Anthropic 式,信息不丢失)。磁盘 ledger.jsonl 永远存上游原始值,归一化只在读取/服务层做(store 的 summary、tracker 的 handleSync 和 inFlightFor),历史记录自动被正确重解读。新接 provider:跑一轮真实 turn → 看落库记录判断 input 是否含 cached → 往 SEMANTICS_TABLE 加一行 + 测试。未知 provider 原样透传,不猜。 +- **Paseo 0.11.2 仍丢弃 cache creation**:Claude `buildResultUsage` 只映射普通输入、cache read、输出与 SDK 报告费用。缺失不等于零;命中率必须有完整分母。`costUsd` 只是运行器报告费用,不保证等于网关实际扣款;分项估算残差不得反推 write token。 +- **输入口径只按已验证 harness,不按模型回退**:Codex input 包含 read/write;Claude 和 OMP input 已与缓存分开。`usageSemantics(provider)` 不接受 model。RPC 的 IN 是减掉 read 和已知 write 的未缓存输入,write 缺失时 Codex IN 仍可能含未分类写入;未知 provider 原样显示并排除轮次合计。磁盘原始值不重写。 - **turnId 不是全局唯一的**(2026-09-07 从 ledger.jsonl 实测):Paseo 的 turnId 形如 `foreground-turn-N`,agent/daemon 会话重启后从 1 重新计数。任何以 `agentId:turnId` 当唯一键的地方都会跨会话冲突——曾导致面板 React key 重复、渲染出幽灵重复行。现在记录 id 拼入 `endedAt`(aggregate.ts),面板行 key 用 `seq`(兼容旧格式的历史记录)。新代码需要唯一键时同样别只用 turnId。 - **终止事件后快照可能回放 activeTurn**(同日实测,曾造成 3ms 内同一 turn 落库两条):turn_completed 之后紧跟的 agent_update 快照仍带着刚结束的 turn,noteUsage 若据此重开 open turn,下个终止事件就会重复落库。tracker 用 `lastClosedTurnId` 挡掉(新轮保留旧 ID 以屏蔽迟到事件;provider session 确认改变时重置)。改 turn 生命周期逻辑时注意 timeline 事件流和快照流是两条异步通道,顺序无保证。 @@ -48,8 +48,8 @@ paseo agent archive # 测完归档,别 - `server/journal.ts` 保存 open turn、session/cost 基线及 pending records;先写 journal 再 append JSONL,open.key 是稳定 UUID。结束 hook 无 usage,短暂等待 timeline 终值后按已观测数据结算;清理时完成已知的结束 hook。 - `server/read-model.ts` 按 store/pricing revision 缓存归一化行与汇总;`client/data.ts` 共享 panel/pill 查询并合并未变化的 records。RPC 的空 records 配合相同 recordsRevision 表示未变化,不表示历史清空。 - `server/catalog.ts` 复用完整 agent 目录并订阅分页 workspace 目录;总览不再每次查询只取前 200 个 agent。 -- `providerSemantics` 只按已验证 harness 匹配:Claude whole-turn + session cost;Codex request observations;未知取最新观察且标 partial,原始费用保留但不推断累计差值。model fallback 仅用于 input/cache 口径。 -- 新 Codex 记录的 requests 保留原始逐请求 token,用于逐请求价格档位;旧记录没有请求明细,只能近似。pricing.json 最后档必须无上限,档位递增;运行时重新读取无效文件会保留上一份有效价格。 +- `providerSemantics` 区分 Claude turn、Codex request、OMP session/branch 和 unknown;`tokenScope` 在读取旧账本时按已验证 harness 补足解释。session/unknown 只作为快照显示,不做轮差、不计入轮次或日期范围合计、不估算单轮费用。`ledger.sync.usageSnapshot` 保留迟到的闭轮/idle 观察,不能据此重开一轮;无 epoch/终值归属时不猜测真实轮增量。 +- Codex `requests` 用于逐请求价格档位;缺字段时整个对应分项保持未知,原请求仍保留。价格匹配保留完整 ID,标准 Claude 纯数字版本仅兼容点号/连字符,其他差异使用显式 aliases。旧的 namespace/context/provider 折叠只用于提示迁移,不能选价;可能的旧覆盖价失配或多义匹配阻止参考价回退,并通过 pricingWarning/pricingWarningTurns 在 UI 提示。provider scope 仅匹配 provider,配置读回后自动清除已解决的提示。用户文件不重写,无内部价格种子或固定汇率。 - Timeline 使用 append + addTimelineRenderer:先落盘、每轮一次、稳定 ID、最多重试一次。不要高频更新或全历史回填;append 会改变 agent.updatedAt,且 plugin rows 不保证 daemon 重启后保留。JSONL 是权威账本。 ## 0.9.1 适配约束 @@ -64,17 +64,17 @@ paseo agent archive # 测完归档,别 ## 0.11 适配约束 - **发布代码禁止 import `@getpaseo/client` / `@getpaseo/protocol`**(含 type-only)。宿主编译插件时会解析 type-only import,但只提供 SDK specifier、zod、react、react-native、@tanstack/react-query、@types/node。git 安装不跑 `npm install`(只跑 manifest `build`),npm 安装不装 devDependencies,所以其他类型依赖会让安装失败(0.11.1 实测 `Could not resolve type dependency`)。客户端类型从 `client/paseo-types.ts` 取,服务端从 `server/paseo-types.ts` 取(都由宿主 `PaseoApi` 推导)。`shared/` 不能 import `/client` 或 `/server` SDK,用结构类型和泛型。测试文件不进 bundle,可以继续用 devDependencies。 -- 改动依赖或 import 后,用无 `node_modules` 的副本验证:复制源码到临时目录、改 manifest id、让 contribute 提前返回,`paseo plugin add <目录>`,看到 "Plugin ready" 后 `paseo plugin remove` 并删除目录。 +- 改动依赖或 import 后,用无 `node_modules` 的副本验证:复制源码到临时目录、改 manifest id、让两端 contribute 提前返回 `() => {}`(宿主要求 cleanup 函数,裸 return 会被拒绝),`paseo plugin add <目录>`,看到 "Plugin ready" 后 `paseo plugin remove` 并删除目录。 - 总览是 `addScreen`(id `ledger-overview` 不变),`range` 放在 screen params 里(`today`/`7d`/`30d`/`all`,缺省 all)。screen props 没有 setParams;切换范围用 `client.openScreen`(宿主 router.push,会产生历史记录),标题函数随 params 变化。 - 侧边栏是 `addSidebarFooterItem` + `SidebarRow`,trailing 显示今日费用。"今天"按客户端本地午夜计算,以 ISO `since` 传给 `ledger.overview`;服务端按 `Date.parse(endedAt)` 过滤 read-model 行,不在服务端猜时区。 - `addSurface` / `addSidebarItem` / `openSurface` 已弃用,不要再用。不接 `registerUsageSource`(那是订阅额度窗口,不是逐轮 usage;用户已决定不放进 Usage 面板)。 -- 0.11.1 仍未公开 cache-write 和 provider 内部子代理 usage。 +- 0.11.2 的 cache-write、Codex 内部子代理 usage、OMP final-drain/branch epoch 缺口仍在。OMP 统计已包含部分子代理,不能无条件再叠加。徽标使用 Session token snapshot,不以 unknown 代替已知快照;未知 scope 不求 token 总数。历史 Timeline 缺少 scope 元数据时只声明元数据缺失,不声明账本排除。更新后重载客户端以使用新字段。 ## 截图 发布用截图不能出现真实 agent 标题、workspace 或项目名。用独立 demo daemon 和合成数据: -- 新建 home 目录,config 只写 `daemon.listen`(换端口)、`relay.enabled: false`、`cors.allowedOrigins`(静态服务的 origin)、`pluginsEnabled: true`。**不要**复制 `~/.paseo/config.json`(含 provider 密钥)。`paseo daemon run --home ` 前台运行,用 `paseo daemon stop --home ` 只停它,不碰主 daemon。 +- 新建 home 目录,config 只写 `daemon.listen`(换端口)、`daemon.relay.enabled: false`、`daemon.cors.allowedOrigins`(静态服务 origin)、`pluginsEnabled: true`。0.11.2 的 relay/cors 在 daemon 下,不在根级。**不要**复制 `~/.paseo/config.json`(含 provider 密钥)。`paseo daemon run --home ` 前台运行,用 `paseo daemon stop --home ` 只停它,不碰主 daemon。演示 OMP 需在隔离配置中启用,并用 provider.env.PI_CODING_AGENT_DIR 指向临时目录,避免继承用户的 disabledProviders。 - `paseo project create --home `、`paseo workspace create --path --isolation local --title --home ` 建项目和 workspace。agent 记录直接写 `agents//.json`(daemon 启动时读),账本写 `plugins/token-ledger/ledger.jsonl`,然后 `paseo plugin add <仓库目录> --home `。 - Web 客户端在桌面安装目录的 `resources/app-dist`。静态服务时往 index.html 注入 `globalThis.__PASEO_INITIAL_DAEMON_CONNECTION__={listen:"127.0.0.1:",useTls:false}`,用 puppeteer-core 截图。总览可直接打开 `/h//plugin/token-ledger/surface/ledger-overview?param.range=7d`。 - 截完停 demo daemon,删除 demo 目录。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab508e..94037ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ ## Unreleased - Add an overview screenshot (demo data, with the sidebar footer row) to the README and as the first manifest media image. +- Fix harness input semantics: OMP exclusive input is no longer reduced again by a GPT-model fallback; unverified providers are never inferred from model names. +- Separate per-turn usage from OMP current-session/branch totals and unknown-scope snapshots, including historical v1 records. Snapshots remain visible but are excluded from turn/date-range sums and estimates. Late idle usage updates refresh a separate latest-snapshot card, not an invented turn delta. +- Preserve unknown counts and cache rates rather than displaying zero. Only show a cache hit rate with a complete prompt denominator; keep partial request fields incomplete. +- Disclose incomplete usage, partial reference estimates and unknown-cost turns in the panel, pill, sidebar and overview. Runtime-reported amounts are not verified bills. Keep cost cells within narrow panel widths. +- Remove private gateway seed prices and fixed currency conversion. Start new price files empty without rewriting existing custom files. Match full model identities and explicit aliases; provider scopes no longer match model text, and ambiguous overrides are unpriced. +- Do not guess cache-read/write rates from ordinary-input rates, select tiers with an unknown prompt size, or interpret a falling session-cost counter as proof of a new session. +- Follow up upstream with public synthetic evidence for cache writes, request/branch/descendant attribution and OMP final-drain ordering; see [usage API requirements](docs/paseo-usage-api.md). Missing upstream data is not restored by this change. +- Preserve standard Claude dotted/hyphenated numeric-version aliases for custom prices. Block reference-price fallback and surface a pricing.json migration warning for ambiguous entries or matches that previously depended on unsafe namespace/context/provider folding; valid explicit aliases clear it without rewriting the file. +- Distinguish session token snapshots, unknown-scope snapshots, waiting for usage and no reported usage in the composer pill. Live per-turn token observations remain visible even without a price. +- Label old Timeline cards as missing scope metadata rather than falsely claiming they were excluded from ledger totals. Refresh the support matrix and screenshots with isolated synthetic data. ## v0.7.0 (2026-10-09) — Paseo 0.11 screens and sidebar diff --git a/OVERVIEW.md b/OVERVIEW.md index 5a13647..479cb3c 100644 --- a/OVERVIEW.md +++ b/OVERVIEW.md @@ -2,7 +2,9 @@ TokenLedger records the tokens and cost of each agent turn and keeps the history The agent panel shows session totals, the turn in progress with its context window, and one row per finished turn with status, time, duration, input, cache and output tokens, cost, and a data quality mark. The pill shows the session cost and context use, with an activity icon while a turn runs, and opens the panel. The sidebar row opens the overview, which groups agents by workspace and filters to today, the last 7 days, the last 30 days, or all time. Day ranges start at local midnight on the device that shows them. After each finished turn, the plugin can add one short usage summary to the timeline. -Token counts come from the provider and are never invented. Claude reports a total for each turn, which is marked exact. Codex reports each model request, so the plugin adds the requests and marks the total as approximate. Other providers show the latest report, marked partial. When the provider reports a cost, that cost is shown. Otherwise the plugin estimates the cost, marked with ≈, from your prices in `pricing.json`, then OpenRouter reference prices, then a small built-in price table. +Token counts come from the provider. Claude terminal aggregates and observed Codex requests contribute to turn subtotals. OMP current-session/branch totals and unverified-harness observations remain visible as snapshots but are excluded from turn sums and prices. A separate latest-snapshot card retains late updates without inventing turn attribution. Missing counts/rates display as unknown; cache hit rates require a complete prompt denominator. Partial estimates and unknown-cost turns are explicit in the panel, pill and overview. Runtime-reported costs take priority over your reference prices, OpenRouter references and built-in prices, but are not verified bills. + +The pill distinguishes known session snapshots (`Session ≈4.12M tok`), unknown-scope snapshots, waiting for usage and no reported usage. Old timeline cards without scope metadata are labelled as legacy; their ledger inclusion is not guessed from the card. The plugin settings page on each host has four options: @@ -11,11 +13,14 @@ The plugin settings page on each host has four options: - **Built-in fallback prices** controls estimates when no custom or OpenRouter price matches a model. - **Refresh frequency** selects normal or relaxed polling. Live updates still refresh the views immediately. -The ledger is a JSON Lines file in the plugin data folder on the daemon host, `~/.paseo/plugins/token-ledger/` (or the same path under `PASEO_HOME`). It keeps the last 2,000 turns. Each record holds timestamps, agent ID, provider, model, token counts, cost, status, duration, and quality. Prompts, responses, tool arguments, file paths, and credentials are not recorded. A `pricing.json` file in the same folder holds your own prices per million tokens. History does not sync between hosts. +The ledger is a JSON Lines file on the daemon host, `~/.paseo/plugins/token-ledger/` (or under `PASEO_HOME`), retaining 2,000 records. It stores observed usage, timestamps, identity, scope and quality, not prompts, responses, tool arguments or credentials. Older records remain readable and are reinterpreted without rewriting them. `pricing.json` starts empty and preserves your existing prices. Full model IDs and explicit aliases prevent unrelated routes from sharing a guessed price. History does not sync between hosts. + +Standard Claude numeric-version spellings such as `5.5` and `5-5` retain custom-price compatibility. Other old lossy matches or ambiguous entries show a pricing configuration warning and block reference-price fallback until an explicit alias/provider scope is reviewed. Existing files are not rewritten, and valid edits clear the warning automatically. Known limits: -- Paseo does not report cache write tokens, so Claude cache writes are missing from the token columns. The reported cost stays correct, and the difference shows as a residual under the cost. -- Paseo does not report the usage of subagents that run inside a provider, so totals can be lower than the provider bill. -- Usage during a time when the plugin or daemon is offline cannot be recovered. Affected turns are marked as a gap or as unknown. -- Estimated costs use reference prices and can differ from your actual contract. +- Missing cache writes are not zero; setting prices cannot recover omitted token counts. A reported-cost residual does not prove a cache-write charge. +- Child coverage differs by harness. Codex child usage can be omitted; OMP already includes some child usage. Blindly adding children can double-count. +- Session snapshots cannot safely become per-turn deltas without branch epochs and final usage attribution; they are not included in date-range totals. +- Offline observations and missing request identities cannot be reconstructed. Affected usage stays incomplete or unknown. +- Runtime and reference prices can differ from actual contracts. No private gateway rate table or fixed exchange rate is built in. diff --git a/README.md b/README.md index 7b5e990..a56bc8c 100644 --- a/README.md +++ b/README.md @@ -13,21 +13,21 @@ Per-turn LLM token usage and cost for [Paseo](https://paseo.sh) agents — an ag

- TokenLedger per-agent panel: session summary (turns, input/cache/output tokens, cost), context-window bar, and a per-turn history table + TokenLedger demo panel: session snapshot shown separately from unknown per-turn totals

## What it shows **Per-agent panel** (workspace/explorer, bound to one session — the header names the agent and model it is tracking): -- **Session summary** — total turns, input / cache / output tokens, and reported or estimated cost. Estimated totals use `≈`; missing-price turns are counted explicitly. -- **Turn in progress** — elapsed time, running token counts and estimated cost when available, and a context-window bar. When idle, the last known context-window usage is still shown. -- **Turn history** — one row per finished turn: status, time, duration, tokens, cost, and a data-quality mark. +- **Session summary** — known per-turn input / cache / output subtotals and runtime-reported or reference cost. Missing counts stay `–`; partial estimates and unknown-cost turns are counted explicitly. +- **Turn in progress** — elapsed time, observed token counts and cost when attributable to the turn, plus a context-window bar. Idle context usage remains visible. +- **Turn history** — one row per finished turn with accounting scope and coverage. OMP session/branch totals and unverified-harness observations are snapshots, not turn usage, and are excluded from totals. A separate latest-snapshot card also reflects late idle updates. -**Composer pill** — every live agent gets a compact pill next to its composer (`$0.43 · ctx 47%`, with an activity icon while a turn is running). The total includes the current turn's estimated cost when available and uses `≈` whenever estimates contribute. Pressing it opens that agent's panel. With split views, each session carries its own pill, so the session↔usage binding is always visible. +**Composer pill** — shows attributable current-turn totals when available. For OMP, `Session ≈4.12M tok` means the known tokens in the latest session/branch snapshot, not a per-turn total; `≈` marks missing components. Unknown-scope snapshots are named without summing potentially overlapping counts. No data shows **Waiting for usage** while running or **No usage reported** when idle. Reference estimates, partial costs and pricing configuration warnings remain explicit. Pressing the pill opens that agent's panel.

- TokenLedger composer pill showing cost and context usage next to the composer; pressing it opens the panel + TokenLedger demo pill showing a session token snapshot and context usage

**Sidebar** — a **TokenLedger** row in the sidebar footer shows today's cost (or tokens when no price is known), with an activity icon while a turn is running. "Today" starts at local midnight on the device that shows it. @@ -35,10 +35,10 @@ Per-turn LLM token usage and cost for [Paseo](https://paseo.sh) agents — an ag **Overview** (sidebar footer → TokenLedger, or the command center) — all sessions in one place: grand totals, then per-agent rows grouped by workspace with live-turn indicator, last activity, cost/tokens, and turn count. Pick **Today**, **Last 7 days**, **Last 30 days**, or **All time**; the range is part of the screen link and its title. A range lists agents with turns that ended in it, plus agents with a turn in flight. Tapping a row jumps to that agent.

- TokenLedger overview on Last 7 days with demo data: range buttons, totals for all sessions, and agent rows grouped by workspace; the TokenLedger row in the sidebar footer shows today's cost + TokenLedger Today overview with synthetic data, excluded snapshots and a custom-price migration warning

-**Timeline** — each newly finished turn gets one passive usage summary after its ledger record is saved. It uses the same normalized counts, cost source and quality marks as the panel. No polling or message replacement is involved. +**Timeline** — each newly finished turn gets one passive usage summary after its ledger record is saved. Older cards without scope metadata say **legacy scope metadata unavailable**; they do not claim exclusion from totals. The ledger remains authoritative. No polling or message replacement is involved. ## Compatibility @@ -46,6 +46,8 @@ TokenLedger **v0.7.0** requires **Paseo 0.11.0 or newer**, on both the daemon an Paseo 0.9.1–0.10.x users should stay on `v0.6.3`; Paseo 0.8 users on `v0.5.1`; Paseo 0.7 users on `v0.3.1`. Existing `ledger.jsonl` records remain readable; no migration or deletion is required. See the [v0.6.0 release notes](docs/releases/v0.6.0.md) for the 0.9.1 migration details. +The unreleased accounting fixes were also exercised on Paseo 0.11.2. After updating, reload/reopen the client so it uses the new nullable summary fields, scope metadata and pricing warnings. Existing JSONL records are reinterpreted on read, not rewritten. The screenshots above use synthetic data on an isolated host. + ## Install Install from the [Paseo plugin registry](https://paseo.sh/plugins/stv1024/token-ledger) (Paseo 0.11.0 or newer): @@ -102,20 +104,23 @@ All three switches default to on, and refresh frequency defaults to normal. A "turn" is one user message through to `turn_completed`, `turn_failed`, or `turn_canceled` — potentially spanning many model calls. Token counts always come from the provider; TokenLedger never invents missing usage: -| Situation | Value used | Quality | +| Situation | Value used | Accounting scope | | --- | --- | --- | -| Provider reports a whole-turn aggregate at turn end (Claude) | used as-is | `exact` | -| Provider reports per-request usage during the turn (Codex) | distinct observations summed | `partial` (shown as `≈`) | -| Only a single mid-turn observation | that observation | `partial` | -| Nothing reported | nothing shown | `unavailable` | +| Whole-turn terminal aggregate (Claude) | reported counts | turn; coverage can still be incomplete | +| Per-request observations (Codex) | observed requests summed | turn; approximate without stable request IDs | +| Current-session/branch aggregate (OMP) | latest reported snapshot | session; excluded from per-turn totals and prices | +| Unverified harness | latest reported snapshot | unknown; excluded from per-turn totals and prices | +| Nothing reported | unknown, not zero | unavailable | + +Counting conventions are selected by verified harness/provider identity, never by the model name. OMP input is already exclusive of cache; Codex input is inclusive. “IN” is uncached input after subtracting cache reads and known writes; if writes are omitted, Codex IN may still include unclassified writes. Unknown provider values stay as reported, without guessed normalization. -Cost uses the first available source: upstream reported cost, local override, OpenRouter reference pricing, then the small built-in fallback table. Claude reports cumulative session cost, so its per-turn delta is attributed to the turn. For unverified harnesses, raw cost is retained in `sessionCostUsd` but is not treated as a known per-turn charge. Model names do not establish aggregation or cost scope. +For attributable turn costs, the order is runtime-reported cost, local override, OpenRouter reference, then built-in fallback. Claude session-cost deltas use the known baseline; a lower counter alone does not prove a session reset. A runtime's reported cost can itself be an estimate: it is not a verified invoice, and custom reference pricing does not replace it. Residuals do not identify missing tokens or billing categories. -Estimated costs are marked `≈`. OpenRouter prices are references and may differ from the actual route or contract. New Codex records retain observed request counts, so tiered prices are chosen separately for each request. Historical records without request detail use the aggregate prompt as an approximate fallback. +Estimated costs are marked `≈`; missing components/rates or uncertain request coverage add `partial`. Summaries expose unknown-cost turns rather than presenting the known subtotal as a complete bill. Codex request observations are priced separately; legacy records without requests use the aggregate as an explicitly partial estimate. A tiered price cannot be selected when the complete prompt size is unknown. Per-request routed model identities are not exposed, so reference pricing uses the recorded selected model, not a claim of the actual billed model. -Paseo 0.9.1 still does not expose cache-write token counts through its public usage API. TokenLedger can retain and price them separately when supplied, but the current Claude and Codex adapters omit them. Missing counts stay unknown; historical writes are not inferred. Claude's reported total can include charges absent from the token counts, producing a positive residual under COST. Codex input includes cache reads (and cache writes when supplied); the UI separates known cache counts while the ledger retains raw counts. +Paseo 0.11.2 still omits cache-write counts from the public Claude, Codex and OMP usage paths. Missing counts remain unknown; setting a price cannot recover them. Cache hit rate is read tokens divided by the complete prompt size: Codex's inclusive input supplies that denominator, while Claude/OMP need input + read + write. Without a complete denominator the panel shows `?`, not a near-100% rate. If input/cache counts are inconsistent, the plugin does not clamp them into a fabricated zero. -Provider-internal subagents are not ordinary Paseo catalog agents. The public plugin API does not expose their usage; the Codex adapter discards child usage notifications. Session and overview totals therefore **may omit descendant usage**. TokenLedger uses only public Paseo APIs; it does not inspect provider logs or gateway billing endpoints. The [upstream API requirements](docs/paseo-usage-api.md) describe the missing fields, lineage, stable request IDs, and accounting scopes needed to close these gaps without double billing. +Provider-internal subagents are not ordinary Paseo catalog agents. The Codex adapter discards child usage notifications; OMP session stats already include some task-child usage. Neither just traversing the catalog nor blindly adding children fixes accounting. OMP's branch-view totals can decrease, and its final usage can arrive after completion or be superseded by the next turn. Without epochs and turn attribution the plugin does not invent per-turn deltas: cumulative history is shown as snapshots and late observations refresh the separate latest-snapshot card. These snapshots do not enter date-range totals. TokenLedger uses only public Paseo APIs, not provider logs or gateway billing endpoints. See the [upstream API requirements](docs/paseo-usage-api.md). Identical consecutive usage snapshots cannot be distinguished from separate equal-size model calls, so Codex totals remain `partial`. Open-turn observations, session identity and billing baselines are checkpointed locally, and graceful plugin reloads preserve them. A crash can lose observations since the last checkpoint; events missed while the plugin was offline cannot be reconstructed. Interrupted recovered turns are marked as partial/unavailable rather than claiming an exact offline outcome. `exact` describes the reported turn aggregate, not proof that every descendant or billing category was included. @@ -125,7 +130,7 @@ After a subscription error or reconnect, the tracker refreshes the affected agen ### Pricing overrides -On first use, TokenLedger creates `~/.paseo/plugins/token-ledger/pricing.json` (or under `PASEO_HOME`). It is a USD-per-million-token table and has highest priority among estimates. Entries can be scoped by provider substring and can contain prompt-size tiers. Changes to the file are picked up on the next usage request after a 30-second recheck interval; a reload also applies them immediately: +On first use, TokenLedger creates an **empty** `~/.paseo/plugins/token-ledger/pricing.json` (or under `PASEO_HOME`). No internal gateway prices or currency conversion are shipped. Existing files are preserved. Overrides are USD per million tokens and have highest priority among estimates. Provider substring scopes match only the provider, not the model string. Full model IDs are matched case-insensitively, with one safe spelling equivalence: standard Claude numeric versions such as `claude-opus-5.5` and `claude-opus-5-5` match each other. Namespaces, context/mode suffixes, arbitrary model punctuation and model arrays are not collapsed. Add explicit `aliases` for other verified identities. Scoped matches take priority over global ones; ambiguous matches are unpriced. Valid edits take effect after the next 30-second recheck, or a reload: ```json { @@ -135,6 +140,7 @@ On first use, TokenLedger creates `~/.paseo/plugins/token-ledger/pricing.json` ( { "providers": ["my-provider"], "model": "my-model", + "aliases": ["my-provider/my-model"], "tiers": [ { "input": 1.5, "cacheRead": 0.15, "cacheWrite": 1.875, "output": 6 } ] @@ -143,11 +149,13 @@ On first use, TokenLedger creates `~/.paseo/plugins/token-ledger/pricing.json` ( } ``` -`cacheWrite` is an optional, independent USD/MTok rate; the example is illustrative, not a universal write multiplier. Old files remain valid: when a write count is known but its price is omitted, it uses the ordinary input rate as an estimate. An explicit zero is a free write rate. Missing write counts cannot be recovered by setting a price. Prompt-size tiers include ordinary input, cache reads, and known writes; reported total cost stays authoritative and is never increased by adding the estimated components again. +**Upgrading old custom prices:** if a model would only have matched through the old namespace/context/provider folding, TokenLedger blocks OpenRouter/built-in fallback and shows **price config**. Review that model's full ID and provider scope, then add a verified alias or correct the entry; the warning clears after the valid configuration is re-read. The plugin does not edit your file or silently replace an intended override with a reference rate. Runtime-reported costs remain visible, but their reference breakdown can still require review. + +`cacheRead` and `cacheWrite` are optional independent USD/MTok rates. Missing rates are unknown; an explicit zero is free. A measured zero token count costs zero even when its rate is unknown. Missing rates no longer fall back to ordinary-input pricing. The example's write multiplier is illustrative, not universal. Tiers use the full prompt count when available. Runtime-reported totals are never increased by adding the estimated components again. OpenRouter's public model catalog, including `input_cache_write` when provided, is cached locally for 24 hours in `openrouter-pricing.json`. Local/cache prices are served before network refresh completes. If refresh fails, the last cache remains active, with retries no more often than every five minutes while the plugin is in use. -Built-in Claude rates were checked against [Anthropic's pricing table](https://platform.claude.com/docs/en/about-claude/pricing) on 2026-09-23. Opus 5.5 standard rates are input $4, cache read $0.20, output $20 per million tokens; the cache-write reference is $5 for the 5-minute lifetime. Other lifetimes, Fast Mode, discounts and gateway contracts can differ. The public usage API does not identify cache lifetimes or the billed service tier. Unknown model versions and explicit Fast/Batch suffixes do not receive a guessed built-in price. Configure a matching override when needed; no new TokenRouter contract prices are assumed. +Built-in Claude rates were checked against [Anthropic's pricing table](https://platform.claude.com/docs/en/about-claude/pricing) on 2026-09-23. Opus 5.5 standard rates are input $4, cache read $0.20, output $20 per million tokens; the cache-write reference is $5 for the 5-minute lifetime. Other lifetimes, Fast Mode, discounts and gateway contracts can differ. The public usage API does not identify cache lifetimes or the billed service tier. Unknown model versions and explicit Fast/Batch suffixes do not receive a guessed built-in price. Configure a matching override when needed; private contract prices are never assumed. ## Data & privacy @@ -160,19 +168,20 @@ Built-in Claude rates were checked against [Anthropic's pricing table](https://p ## Limitations -- Paseo 0.9.1 does not expose cache-write token counts; Claude cache writes are invisible in token columns (reported cost stays accurate, the gap shows as a `+$x.xx` residual). -- Usage of provider-internal subagents is not exposed by Paseo, so session and overview totals may omit it. -- Codex totals are summed per request and marked `partial` (`≈`); estimated costs use reference prices and may differ from your actual contract. -- Events missed while the plugin or daemon was offline cannot be reconstructed; affected turns are marked `gap` / `unknown`. -- Desktop and mobile UI were verified less thoroughly than the daemon side. +- Paseo 0.11.2 omits cache-write counts on current Claude/Codex/OMP usage paths. Missing writes are unknown, and may make the cache-rate denominator unavailable. A reported-cost residual is not proof of missing token charges. +- Child coverage differs by harness: Codex child usage can be omitted, while OMP already includes some child usage. Blindly adding children can double-count. +- OMP session/branch snapshots have no reliable per-turn boundary. They stay separate from per-turn and date-range sums; unknown providers are not priced by guessing semantics from model names. +- Codex request observations and reference estimates remain partial. Runtime-reported costs are not verified invoices. +- Events missed offline cannot be reconstructed; affected turns retain gap/unknown status. Mobile-device testing is more limited than desktop/web verification. ## Support matrix | Provider | Tokens | Cost | | --- | --- | --- | -| Claude / claude-* variants | per-turn aggregate (`exact`) | reported when available; estimate fallback | -| Codex / codex-* variants | summed per-request (`partial`) | estimate fallback when absent | -| Other providers (OpenCode, Pi, ACP harnesses) | latest observation, `partial`; `unavailable` when silent | raw cost retained; estimate when model pricing matches | +| Claude / claude-* variants | per-turn terminal aggregate; missing categories stay incomplete | runtime-reported when attributable; reference fallback | +| Codex / codex-* variants | summed request observations (`partial`) | partial reference estimate with known prices | +| OMP / omp-* variants | latest session/branch snapshot, excluded from turn totals | raw session cost displayed separately; no guessed turn cost | +| Other providers (OpenCode, Pi, ACP harnesses) | latest unknown-scope snapshot; no guessed aggregation | raw cost retained separately; no turn estimate | ## Development diff --git a/client/panel.tsx b/client/panel.tsx index 072e07b..d6ad903 100644 --- a/client/panel.tsx +++ b/client/panel.tsx @@ -58,7 +58,7 @@ function CtxBar({ ctx, theme }: { ctx: Ctx; theme: PluginTheme }) { function InFlightCard({ inFlight, seq, theme }: { inFlight: InFlight; seq: number; theme: PluginTheme }) { const elapsed = useElapsed(inFlight.startedAt); const tokens = tokensLine(inFlight.input, inFlight.cached, inFlight.output, inFlight.cacheWrite); - const pct = fmtPct(cacheRatio(inFlight.input, inFlight.cached, inFlight.cacheWrite)); + const pct = fmtPct(cacheRatio(inFlight.cached, inFlight.promptTokens)); const ctx = inFlight.ctxUsed !== null && inFlight.ctxMax !== null ? { used: inFlight.ctxUsed, max: inFlight.ctxMax } : null; return ( {tokens} - {pct ? ` (${pct} cached)` : ""} - {inFlight.modelCalls > 0 ? ` · ${inFlight.modelCalls} call${inFlight.modelCalls > 1 ? "s" : ""}` : ""} + {pct ? ` (${pct} cached)` : ' (cache rate unknown)'} + {inFlight.modelCalls > 0 ? ` · ${inFlight.modelCalls} observation${inFlight.modelCalls > 1 ? 's' : ''}` : ''} ) : ( tokens & cost reported at turn end )} + {inFlight.tokenScope !== 'turn' ? + {inFlight.tokenScope} snapshot, not current-turn usage. Excluded from totals. + : null} {inFlight.effectiveCostUsd !== null ? ( - ≈{fmtCost(inFlight.effectiveCostUsd)} current turn + {inFlight.costSource === 'reported' ? '' : '≈'}{fmtCost(inFlight.effectiveCostUsd)} {inFlight.partialCost ? 'partial estimate' : 'current turn'} ) : null} + {inFlight.pricingWarning ? {inFlight.pricingWarning} : null} {ctx ? : null} {inFlight.usageGap ? Connection interrupted · usage may be incomplete. : null} @@ -115,9 +119,9 @@ function TurnCell({ theme: PluginTheme; }) { return ( - + {top} - + {bottom} @@ -128,16 +132,16 @@ function RecordRow({ record, theme, dense }: { record: TurnRow; theme: PluginThe const cols = turnColumns(dense); const hasUsage = record.input !== null || record.cached !== null || record.output !== null || record.cacheWrite != null; const split = record.costBreakdown; - const pct = fmtPct(cacheRatio(record.input, record.cached, record.cacheWrite)); + const pct = fmtPct(cacheRatio(record.cached, record.promptTokens)); const cacheCost = split ? fmtCostSmall(split.cacheUsd) : "–"; - const costDetail = + const costDetail = record.pricingWarning ? 'price config' : record.costSource === "override" ? "override" : record.costSource === "openrouter" ? "OR ref" : record.costSource === "builtin" ? "list" - : fmtResidual(split?.otherUsd ?? null); + : record.tokenScope !== 'turn' ? 'not turn' : `runtime ${fmtResidual(split?.otherUsd ?? null).trim()}`; const seqWidth = dense ? 20 : 24; return ( {fmtDuration(record.durationMs)} - {record.usageGap ? ' · gap' : record.quality === "partial" ? " ≈" : ""} + {record.tokenScope !== 'turn' ? ` · ${record.tokenScope} snapshot` : record.usageGap ? ' · gap' : record.incompleteUsage ? ' · incomplete' : ''} {hasUsage ? ( @@ -187,14 +191,12 @@ function RecordRow({ record, theme, dense }: { record: TurnRow; theme: PluginThe - {record.cacheWrite != null ? ( - - ) : null} + @@ -229,6 +231,7 @@ function RecordRow({ record, theme, dense }: { record: TurnRow; theme: PluginThe export function TokenLedgerPanel({ theme, layout, agentId }: PluginAgentPanelProps) { const { dense, setWidth } = useDenseLayout(layout.compact); const { data, error } = useLedger(agentId); + const pricingWarnings = [...new Set(data?.records.flatMap((record) => record.pricingWarning ? [record.pricingWarning] : []) ?? [])]; // Make the panel↔agent binding visible: which session is this ledger for? const agentLabel = useAgent(agentId, (agent) => { const name = agent.title ?? agentId.slice(0, 8); @@ -269,8 +272,24 @@ export function TokenLedgerPanel({ theme, layout, agentId }: PluginAgentPanelPro + {pricingWarnings.map((warning) => {warning})} {!data.inFlight && data.ctx ? : null} + {data.usageSnapshot ? + + Latest {data.usageSnapshot.scope === 'session' ? 'session / branch' : 'unknown-scope'} snapshot + + + {tokensLine(data.usageSnapshot.input, data.usageSnapshot.cached, data.usageSnapshot.output, data.usageSnapshot.cacheWrite)} + + {data.usageSnapshot.reportedCostUsd !== null ? + Runtime-reported cost: {fmtCost(data.usageSnapshot.reportedCostUsd)} · not a verified bill + : null} + + Not per-turn usage; excluded from totals. Late updates refresh this snapshot, not closed turns. + {data.usageSnapshot.observedAt ? ` Observed ${fmtTime(data.usageSnapshot.observedAt)}.` : ''} + + : null} {data.inFlight ? : null} @@ -286,10 +305,13 @@ export function TokenLedgerPanel({ theme, layout, agentId }: PluginAgentPanelPro // restarts (turnId reuse), which made React render ghost rows. ))} + + IN is uncached input; it may include unclassified writes. “?” means the cache-rate denominator is unknown. + Session/unknown snapshots are observations at settlement, not final turn totals. + {data.records.some((record) => fmtResidual(record.costBreakdown?.otherUsd ?? null).trim() !== "") ? ( - ± under COST: difference between reported cost and the token-based estimate, including charges - for usage Paseo does not expose. + ± under COST: runtime-reported cost minus known component estimates. A residual does not identify missing tokens or actual charges. ) : null} {data.records.some((record) => record.costSource && record.costSource !== "reported") ? ( diff --git a/client/pill.tsx b/client/pill.tsx index efc785d..d761ccb 100644 --- a/client/pill.tsx +++ b/client/pill.tsx @@ -5,23 +5,47 @@ import { useLedger } from "./data.ts"; import { useEffect } from "react"; import { subscribeAgents } from "../shared/agents.ts"; import { ledgerEnsure, type SyncResult } from "../shared/ledger.ts"; -import { fmtCost, fmtTokens } from "./ui.tsx"; +import { costOrTokens, fmtTokens } from "./ui.tsx"; import type { PanelPlacement } from "./layout.ts"; -function pillLabel({ summary, inFlight, ctx }: SyncResult): string { - const combinedCost = summary.effectiveCostUsd === null && inFlight?.effectiveCostUsd == null - ? null - : (summary.effectiveCostUsd ?? 0) + (inFlight?.effectiveCostUsd ?? 0); - const cost = fmtCost(combinedCost); - const estimated = summary.estimatedTurns > 0 || inFlight?.effectiveCostUsd != null; - const parts = [cost - ? `${estimated ? "≈" : ""}${cost}` - : `${fmtTokens(summary.input + summary.cached + (summary.cacheWrite ?? 0) + summary.output)} tok`]; +function pillLabel({ summary, inFlight, ctx, usageSnapshot }: SyncResult): string { + let label: string; + const snapshotHasTokens = usageSnapshot && (usageSnapshot.input !== null || usageSnapshot.cached !== null + || usageSnapshot.cacheWrite !== null || usageSnapshot.output !== null); + if (usageSnapshot && (snapshotHasTokens || usageSnapshot.reportedCostUsd !== null)) { + if (usageSnapshot.scope === 'session' && snapshotHasTokens) { + const partial = usageSnapshot.input === null || usageSnapshot.cached === null + || usageSnapshot.cacheWrite === null || usageSnapshot.output === null; + const total = (usageSnapshot.input ?? 0) + (usageSnapshot.cached ?? 0) + + (usageSnapshot.cacheWrite ?? 0) + (usageSnapshot.output ?? 0); + label = `Session ${partial ? '≈' : ''}${fmtTokens(total)} tok`; + } else label = usageSnapshot.scope === 'session' ? 'Session snapshot' : 'Usage snapshot · scope unknown'; + } else { + const live = inFlight?.tokenScope === 'turn' ? inFlight : null; + const liveCost = live?.effectiveCostUsd; + const combined = { ...summary, + effectiveCostUsd: summary.effectiveCostUsd === null && liveCost == null ? null + : (summary.effectiveCostUsd ?? 0) + (liveCost ?? 0), + estimatedTurns: summary.estimatedTurns + (liveCost != null && live?.costSource !== 'reported' ? 1 : 0), + partialCostTurns: summary.partialCostTurns + (liveCost != null && live?.partialCost ? 1 : 0), + unpricedTurns: summary.unpricedTurns + (live && liveCost == null ? 1 : 0), + pricingWarningTurns: summary.pricingWarningTurns + (live?.pricingWarning ? 1 : 0), + incompleteTurns: summary.incompleteTurns + (live ? 1 : 0), + }; + if (live) for (const field of ['input', 'cached', 'cacheWrite', 'output'] as const) { + if (live[field] != null) combined[field] = (combined[field] ?? 0) + live[field]; + } + const hasUsage = combined.effectiveCostUsd !== null || combined.input !== null || combined.cached !== null + || combined.cacheWrite != null || combined.output !== null; + label = hasUsage || combined.pricingWarningTurns > 0 ? costOrTokens(combined) + : inFlight ? 'Waiting for usage' : 'No usage reported'; + } + const parts = [label]; const liveCtx = inFlight?.ctxUsed != null && inFlight.ctxMax != null ? { used: inFlight.ctxUsed, max: inFlight.ctxMax } : ctx; if (liveCtx && liveCtx.max > 0) parts.push(`ctx ${Math.round(liveCtx.used / liveCtx.max * 100)}%`); - return parts.join(" · "); + return parts.join(' · '); } export function contributePills(client: PluginClientContext, placement: PanelPlacement): () => Promise { diff --git a/client/sidebar.tsx b/client/sidebar.tsx index 1f11f74..284feed 100644 --- a/client/sidebar.tsx +++ b/client/sidebar.tsx @@ -9,7 +9,7 @@ import { costOrTokens } from "./ui.tsx"; export function LedgerSidebarItem({ theme, currentScreen, openScreen }: PluginSidebarItemProps) { const { data } = useOverview(useRangeStart("today")); const working = !!data?.groups.some((group) => group.agents.some((agent) => agent.active)); - const label = !data ? null : data.totals.turns === 0 ? "$0.00" : costOrTokens(data.totals); + const label = data ? costOrTokens(data.totals) : null; return ( = 0.01 ? `$${value.toFixed(2)}` : `$${value.toFixed(4)}`; } -/** Cost when priced, else total tokens. "≈" marks a cost that includes estimates. */ +/** Compact totals always disclose omitted costs or incomplete token coverage. */ export function costOrTokens(summary: Summary): string { const cost = fmtCost(summary.effectiveCostUsd); - if (cost) return `${summary.estimatedTurns > 0 ? "≈" : ""}${cost}`; - return `${fmtTokens(summary.input + summary.cached + (summary.cacheWrite ?? 0) + summary.output)} tok`; + const unknown = summary.pricingWarningTurns > 0 ? ' · price config' + : summary.unpricedTurns > 0 ? ` · ${summary.unpricedTurns} unknown` : ''; + if (cost) return `${summary.estimatedTurns > 0 ? '≈' : ''}${cost}${summary.partialCostTurns > 0 ? ' partial' : ''}${unknown}`; + if (summary.input === null && summary.cached === null && summary.output === null && summary.cacheWrite == null) { + return summary.unmeasuredTurns > 0 ? `turn usage unavailable${unknown}` : `no usage reported${unknown}`; + } + const total = (summary.input ?? 0) + (summary.cached ?? 0) + (summary.cacheWrite ?? 0) + (summary.output ?? 0); + return `${fmtTokens(total)} tok${summary.incompleteTurns > 0 ? ' partial' : ''}${unknown}`; } /** Compact cost for per-component cells; "–" when unknown. */ @@ -29,12 +35,7 @@ export function fmtCostSmall(value: number | null): string { return `$${value.toPrecision(1)}`; } -/** - * Signed cost residual (reported total minus list-price component estimate) - * for the COST cell; blank when unknown or negligible. On Claude turns a - * positive residual is mostly cache writes, which upstream never reports as - * tokens. - */ +/** Reported runtime cost minus known component estimates, not missing-token attribution. */ export function fmtResidual(value: number | null): string { if (value === null || Math.abs(value) < 0.005) return " "; return `${value > 0 ? "+" : "−"}${fmtCostSmall(Math.abs(value))}`; @@ -72,7 +73,7 @@ export function statusColor(status: TurnRecord["status"], theme: PluginTheme): s export function tokensLine(input: number | null, cached: number | null, output: number | null, cacheWrite?: number | null): string | null { if (input === null && cached === null && output === null && cacheWrite == null) return null; - return `in ${fmtTokens(input)} · cache ${fmtTokens(cached)}${cacheWrite != null ? ` · write ${fmtTokens(cacheWrite)}` : ''} · out ${fmtTokens(output)}`; + return `in ${fmtTokens(input)} · cache ${fmtTokens(cached)} · write ${fmtTokens(cacheWrite ?? null)} · out ${fmtTokens(output)}`; } /** Column widths for the TURNS table; shared by the header and data rows. */ @@ -120,16 +121,25 @@ export function Stat({ label, value, theme, dense }: { label: string; value: str export function SummaryRow({ summary, theme, dense }: { summary: Summary; theme: PluginTheme; dense?: boolean }) { const cost = fmtCost(summary.effectiveCostUsd); - const costValue = cost ? `${summary.estimatedTurns > 0 ? "≈" : ""}${cost}` : null; - const costLabel = summary.unpricedTurns > 0 ? `cost · ${summary.unpricedTurns} unknown` : summary.estimatedTurns > 0 ? "cost · estimated" : "cost"; + const costValue = cost ? `${summary.estimatedTurns > 0 ? '≈' : ''}${cost}` : 'unknown'; + const costLabel = ['known cost', summary.partialCostTurns > 0 ? `${summary.partialCostTurns} partial` : null, + summary.unpricedTurns > 0 ? `${summary.unpricedTurns} unknown` : null].filter(Boolean).join(' · '); return ( - - - - - {summary.cacheWrite != null ? : null} - - {costValue ? : null} + + + + + + + + + + {summary.incompleteTurns > 0 ? + {summary.incompleteTurns} turn(s) with incomplete usage{summary.unmeasuredTurns > 0 ? ` · ${summary.unmeasuredTurns} snapshot(s) excluded from totals` : ''}. + : null} + {summary.pricingWarningTurns > 0 ? + {summary.pricingWarningTurns} turn(s) need pricing.json review. Reference-price fallback is blocked for ambiguous or unsafe old matches. + : null} ); } @@ -137,7 +147,7 @@ export function SummaryRow({ summary, theme, dense }: { summary: Summary; theme: export function UsageCoverageNote({ theme }: { theme: PluginTheme }) { return ( - Totals reflect usage available from Paseo; subagent usage and cache writes may be missing. + Known usage only. “–” means unknown, not zero. Runtime-reported and reference costs are not verified bills; descendant usage may be missing. ); } diff --git a/docs/compatibility.md b/docs/compatibility.md index f152bad..837d42f 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -46,6 +46,7 @@ exclude beta builds. | 0.10.2 | Installed desktop/daemon, published SDK, typecheck, 65 tests, plugin reload, real Claude/Codex turns on 2026-09-30 | Compatible with v0.6.3 | | 0.11.0 | Published SDK (plugin dist identical to 0.11.1), typecheck and 65 tests on 2026-10-09 | Minimum for v0.7.0; no installed host tested | | 0.11.1 | Installed desktop/daemon, published SDK, typecheck, 65 tests, dependency-free compile probe, plugin reload, real Claude/Codex turns, manual sidebar and overview check on 2026-10-09 | Supported by v0.7.0 | +| 0.11.2 | Unreleased accounting fixes: pinned-SDK typecheck, 66 tests, installed-plugin reload, replay of existing Claude/OMP records, real host Web UI overview/pill/panel and live OMP snapshots on 2026-10-14 | Harness scopes and incomplete coverage verified; missing upstream data remains unavailable | The 0.11.1 investigation found: @@ -75,3 +76,31 @@ This review checked client types, bundle compilation, and the desktop sidebar row and overview ranges by hand; mobile UI was not checked. It does not certify other providers or future Paseo releases. Test agents were archived after verification. + +The 0.11.2 accounting review found two plugin errors: model-based input +normalization subtracted OMP cache twice, and session/branch snapshots were +added as turn counts. Historical snapshots are now excluded from turn totals, +while retaining their raw facts and showing late updates separately. Cache +hit rates require a complete denominator; costs disclose unknown/partial +coverage. Existing local prices and JSONL files were not rewritten. + +The UI check used the installed app-dist against the running daemon in an +isolated Chromium, including the narrow ledger panel. Local-network browser +policy was disabled only for that disposable verification process; document +assets were served locally at the daemon origin without changing its CORS or +auth configuration. No mobile device or new paid model probe was used. + +A copy without `node_modules` also compiled and reached "Plugin ready" on +0.11.2 under a disposable plugin ID. Its contributions returned cleanup +functions without registering tracking/UI; the probe was then removed. + +The local-trial follow-up retained 66 passing tests and added upgrade cases +for dotted/hyphenated Claude custom prices, unsafe legacy-match warnings, +reported-cost preservation and warning removal after a corrected alias. +The actual Timeline renderer was also exercised with legacy and scoped data: +legacy cards no longer assert exclusion. Native Text/View were represented +as leaf components for that render-branch smoke, not a full Timeline UI run. +An isolated 0.11.2 daemon with synthetic data verified the overview warning, +the `Session ≈4.12M tok` pill and its snapshot panel, and supplied the refreshed +screenshots. No model prompt was sent. The production plugin was reloaded +for local trial only; no package version was published. diff --git a/docs/paseo-usage-api.md b/docs/paseo-usage-api.md index ef313a1..49e975f 100644 --- a/docs/paseo-usage-api.md +++ b/docs/paseo-usage-api.md @@ -5,6 +5,8 @@ Status: submitted as follow-up comments in existing upstream Discussions on - [Cache creation counts — discussion #4623](https://github.com/getpaseo/paseo/discussions/4623#discussioncomment-18550901). - [Descendant usage and attribution — discussion #4660](https://github.com/getpaseo/paseo/discussions/4660#discussioncomment-18550906). +- [2026-10-14 follow-up: OMP cache creation and input conventions](https://github.com/getpaseo/paseo/discussions/4623#discussioncomment-18924605). +- [2026-10-14 follow-up: final drain, branch epochs and attribution](https://github.com/getpaseo/paseo/discussions/4660#discussioncomment-18924607). Paseo's [contribution policy](https://github.com/getpaseo/paseo/blob/b2ce2bcb83dd40bd91c32098c59221f0b52d7ffd/CONTRIBUTING.md) directs missing extension points to Discussions. Existing threads covered both @@ -32,6 +34,27 @@ Rechecked on 2026-09-30 against the installed Paseo 0.10.2 provider source and published SDK: the same cache-creation and Codex child-usage gaps remain. See the [compatibility review](compatibility.md) for the scope of this verification. +Rechecked on 2026-10-14 against installed 0.11.2, separately from public `main` +at `cafa04d3fbdbc7e4a3fe8f4d4bea80f15cd4a1d0`. The follow-ups above were posted +as `stv1024` and read back through GitHub; submission does not imply acceptance. +The public OMP usage poller was exercised in memory with synthetic statistics +and controlled promises, not by running the full upstream daemon: + +- OMP maps `getSessionStats()` input/read/output/cost but drops `cacheWrite`. + The installed OMP statistics are already cache-exclusive and aggregate the + current session/branch, including some task-child usage. +- Completion can precede final usage; starting another turn can invalidate + the pending final sample. Public snapshots do not preserve its turn identity. + Require terminal drain or attributable late corrections, plus branch/reset + epochs; subtracting adjacent snapshots is not a reliable per-turn contract. +- Context-only OMP stats become zero token/cost values in the current mapper. + The plugin cannot distinguish those fabricated zeros from measured zero. +- Claude cache creation and Codex child usage remain omitted. Merged + [#6703](https://github.com/getpaseo/paseo/pull/6703) and + [#6106](https://github.com/getpaseo/paseo/pull/6106) improve context accounting, + not the missing billing/usage contract. They are not duplicate bug reports. + + ## 1. Preserve cache creation usage `@getpaseo/protocol/dist/agent-types.d.ts`, `AgentUsage`, currently exposes @@ -137,6 +160,8 @@ history/subscription. Persist identities before acknowledging ingestion, price each request with its own model, and select one accounting scope for each total. Keep the current snapshot path as an explicitly partial fallback. -Until then, TokenLedger's ordinary snapshot/terminal tracking remains active. -Historical missing counts and descendant charges cannot be reconstructed from -the current public API, and this change does not claim to restore them. +Until then, verified turn/request tracking remains active. OMP session/branch +and unknown-scope observations are displayed separately and excluded from +per-turn costs and sums; late observations refresh the latest-snapshot card. +Coverage and unknown prices are explicit. Historical missing counts and +descendant charges cannot be reconstructed from the current public API. diff --git a/images/screenshot-overview.png b/images/screenshot-overview.png index 947cc2a9e8e1bd3407968c0b3111a040af475e3e..034ec569160875d82c7c973bb14fe225dd1272ab 100644 GIT binary patch literal 50395 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π9zAGyO_N^FnGwbbFtY?8) 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 diff --git a/images/screenshot-panel.png b/images/screenshot-panel.png index d2f193c0d893ef4994796b9ee9fdb0c02ff583b4..21c833a6d047c8c73064e98f08a7e2abfdf77843 100644 GIT binary patch literal 29172 zcmbTdV{~O-x9%Go6}#f3V%v6BY}>Y-if!ArS+Q*!687FhD>+@Dk#}3P3=>{6Ih;1W+K~PdxvUbACUdFc%V%(**)@ z%S?*plNzx55rQuzfO?)E7$}6^*T+}2P`)##q^YI(N2P?>I&ftuR`g!zcTS&-R%WIa zZsrD9-)P^a2yUi#0s)0Mo^(QPiaudnrRrQn%)SY9g~4&TLhv723b3FNsZ-D$1B@SP zqxkM$t@E4ffG4l}iTjHwkd7UM>-ULzbS562j|V5d&H3qh-Iu)xy6*cTIwUXwZm4kD z05?ojJ+K@4U%gO!`f`3qg9Euh#J@uYfJJ{G27*Kq{Dc6<6ySz~q5t1rv^GK^FEFHV z7#@uj1pthp>%j}B5VA5yP!Mm?>S-jRMD`y@ULGb`Lyt~iL}6y ziD*~ZEuLa+XgV8+Tjz>K5JMNjO+ zuK|&n#l9VI7^mg2VG(7*1hdkg6=W_+}w z`r^fi#s!hUx}?WZ6CbOlej*`;o;wjHB(+W;0>(arW-b}sfU!Q`n9a_TNTA^^Ap}93HOX}I za%1MSA>o@_4vzX)US<fc$wTZAM@Gq`M@pU@EY`m|*WfpENb78X?R)b-XHmz6C4^M>!_T}Plnp(X28K6|&n+@{TGjcUpc#!9QbL01+pFk(_Re>4~NmEOs zil?a1i}QTx#2UKUIs_50vw5wbi8EKFS7uz)E~X6@K)QtqDMq|W>b4eauXNZZj7e7~ zbHpPPQwhQDTke)uJLZl|c?$h9H#c!7G{^DFihF)Bt%-cc&mk52H4A3^=92+Ttzy9R#EMZBp^WoLdgq#W3)jzLLD zWfp3%5}57@sukIM+H?)kUfkKiO(SfUBS#cuy=8++^!;3j#ZvsROKbid#<#I)Wnlz( z9|qdxTOiJQHewIXyo^0lqN>hN*x<%<%X+iPNn6=%d2SAwQ7?t>MvIhmX~-(${4r}k zSRcrfTt28Z;R0<9g>JaoYE74Sz!JAshMdMOs+{XHz8a@>lZ^ppu%+~F^{cUmpAgA4 zMvK^ynZHHE4=G;pp3(7S3*?{4tC<*<9yALmhGV6ic8oEy&N``mhp5bZ&n zpsa~-$eoi<{dt-i8%9pmQ9Qx^~MJD~;|8C~&@*B`Q>bT9d}eaU=D-i@5HrQn5hk3Y4^EBDFRVg8?<=SF1@Tx&-wI@y=uF85F(WQ;@Mk=UKP!^S`9wVqDiQLc%Zdg$Rt<3T9 z4=B-+*azp~J92<|c#hXAK?qWkBdG5BUv=f-5THj8sM?n{^C6qRor&H48`oIbvds%d z?sQqHwW~0T$otJ`sHDOnq4{eXu%2AZPeIMUjXh9w8}y`boFTpZ9k&wn?YF%+&Gadmr}C{M8(+{Ti0QSvZ-Y5O zj+n2_fxKhWl6h|G!T8@0Lm-;)eoQUfzCF3Lt<|AZ=|FO?NN<9N_{Jh&{`1 z1=4kY-MZ?L`2kiYxKTb6lf(DLF7TtU{TFR7V%O*B?Jc>jRd6T|n1CdxYTmz>ylDqe zmmJ>k@VyD$j=qgF>#ZNylPfK$1QP$#uAgKt7yjPe+_mNg6D)#6|E~tG`+W@h|MD0n zI8b&b%$&(Ynv!Q1a`#ptN^Hz=1)3_ftAnGGi-N&tYZdrNY5U*&PIxi(2 z$!)tz+dHv82CG{F?|4E3vi(y!P|ePc|eitO%OYamfdV&FBjM<_r@O

+popj$@X+xCMp}LGa`C5y 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

PnY=wc|WYT3qHFx25Dr3 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 F`!5nu`2+v} diff --git a/images/screenshot-pill.jpg b/images/screenshot-pill.jpg index 3b1c3fd319aef10188b598402cddc7068030c745..10aace48469f4d69ef8ce16fa3947f12e8ccd15f 100644 GIT binary patch literal 2428 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 DmTy0z literal 37405 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 ArT_o{ diff --git a/server/pricing.test.ts b/server/pricing.test.ts index 9100d41..fe42f89 100644 --- a/server/pricing.test.ts +++ b/server/pricing.test.ts @@ -1,36 +1,78 @@ -import assert from "node:assert/strict"; -import { test } from "node:test"; -import { tokenRouterDefaultPricing } from "./pricing.ts"; +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { finalizeTurn } from '../shared/aggregate.ts'; -test("TokenRouter quota prices are converted to USD per million tokens", () => { - assert.deepEqual(tokenRouterDefaultPricing("gpt-5.6-sol", 100_000), { - input: 5.000222, - cacheRead: 0.499948, - output: 29.999852, - }); -}); - -test("TokenRouter aliases normalize dots, hyphens, vendor prefixes, and 1m suffixes", () => { - assert.deepEqual(tokenRouterDefaultPricing("tokenrouter/claude-opus-4-6[1m]", 100_000), { - input: 5.000222, - cacheRead: 0.499948, - output: 25.000369, - }); -}); - -test("TokenRouter tiered pricing selects the prompt-size tier", () => { - assert.deepEqual(tokenRouterDefaultPricing("gpt-6-astra", 272_000), { - input: 9.999705, - cacheRead: 0.999897, - output: 50, - }); - assert.deepEqual(tokenRouterDefaultPricing("gpt-6-astra", 272_001), { - input: 20.000148, - cacheRead: 1.999793, - output: 75.000369, - }); -}); - -test("unknown TokenRouter model has no invented price", () => { - assert.equal(tokenRouterDefaultPricing("future-model", 100), null); +test('pricing preserves model identity, provider scopes, explicit aliases and unknown tiers', async () => { + const home = await mkdtemp(join(tmpdir(), 'token-ledger-identities-')); + const oldHome = process.env.PASEO_HOME; + process.env.PASEO_HOME = home; + const dir = join(home, 'plugins', 'token-ledger'); + await mkdir(dir, { recursive: true }); + const prices = [ + { model: 'vendor-a/model-x', aliases: ['shared-alias'], tiers: [{ input: 1, cacheRead: 0, output: 0 }] }, + { model: 'vendor-b/model-x', aliases: ['shared-alias', 'explicit-alias'], tiers: [{ input: 10, cacheRead: 0, output: 0 }] }, + { model: 'acme/model-x', providers: ['acme'], tiers: [{ input: 99, cacheRead: 0, output: 0 }] }, + { model: 'tiered', tiers: [{ upToInputTokens: 100, input: 1, cacheRead: 0, output: 0 }, { input: 10, cacheRead: 0, output: 0 }] }, + { model: 'claude-opus-5.5', tiers: [{ input: 100, cacheRead: 0, cacheWrite: 0, output: 0 }] }, + { model: 'gateway/claude-sonnet-4-6', tiers: [{ input: 50, cacheRead: 0, cacheWrite: 0, output: 0 }] }, + { model: 'claude-haiku-4-5[1m]', tiers: [{ input: 20, cacheRead: 0, cacheWrite: 0, output: 0 }] }, + ]; + await writeFile(join(dir, 'pricing.json'), JSON.stringify({ version: 1, currency: 'USD', prices })); + await writeFile(join(dir, 'openrouter-pricing.json'), JSON.stringify({ fetchedAt: new Date().toISOString(), prices: [] })); + const originalNow = Date.now; + try { + // Module-load boundary: pricing captures PASEO_HOME; static import would target the real ledger. + const { ensurePricing, estimateUsageCost, enrichTurn } = await import('./pricing.ts'); + const { summaryFromRows } = await import('./read-model.ts'); + await ensurePricing(); + const usage = { provider: 'claude', model: 'vendor-b/model-x', input: 1_000_000, cached: 0, cacheWrite: 0, output: 0 }; + assert.equal(estimateUsageCost(usage).effectiveCostUsd, 10); + assert.equal(estimateUsageCost({ ...usage, model: 'vendor-a/model-x' }).effectiveCostUsd, 1); + assert.equal(estimateUsageCost({ ...usage, model: 'explicit-alias' }).effectiveCostUsd, 10); + assert.equal(estimateUsageCost({ ...usage, model: 'shared-alias' }).effectiveCostUsd, null); + assert.equal(estimateUsageCost({ ...usage, model: 'acme/model-x' }).effectiveCostUsd, null); + assert.equal(estimateUsageCost({ ...usage, provider: 'claude-acme', model: 'acme/model-x' }).effectiveCostUsd, 99); + assert.equal(estimateUsageCost({ ...usage, model: 'tiered', input: 80, cacheWrite: undefined }).effectiveCostUsd, null); + assert.equal(estimateUsageCost({ ...usage, model: 'tiered', input: 80 }).effectiveCostUsd, 0.00008); + assert.equal(estimateUsageCost({ ...usage, provider: 'omp' }).effectiveCostUsd, null); + assert.equal(estimateUsageCost({ ...usage, provider: 'unknown' }).effectiveCostUsd, null); + const dotted = estimateUsageCost({ ...usage, model: 'claude-opus-5.5' }); + const dashed = estimateUsageCost({ ...usage, model: 'claude-opus-5-5' }); + assert.equal(dotted.effectiveCostUsd, 100); + assert.equal(dashed.effectiveCostUsd, 100); + assert.equal(dashed.costSource, 'override'); + assert.equal(dashed.pricingWarning, null); + for (const model of ['claude-sonnet-4-6', 'claude-haiku-4-5', 'shared-alias', 'acme/model-x']) { + const result = estimateUsageCost({ ...usage, model }); + assert.equal(result.effectiveCostUsd, null); + assert.equal(result.costSource, null); + assert.ok(result.pricingWarning != null); + } + assert.equal(estimateUsageCost({ ...usage, model: 'claude-opus-4' }).effectiveCostUsd, 15); + const record = finalizeTurn({ agentId: 'a', turnId: 't', provider: 'claude', model: 'claude-sonnet-4-6', + startedAt: null, endedAt: '2026-10-14T00:00:00Z', status: 'completed', observations: [], prevSessionCostUsd: 0, + finalUsage: { inputTokens: 1_000_000, cachedInputTokens: 0, cacheWriteInputTokens: 0, outputTokens: 0 } }); + const blocked = enrichTurn(record, 1); + assert.equal(summaryFromRows([blocked]).pricingWarningTurns, 1); + // Reported costs remain visible even when reference breakdown pricing needs review. + const reported = enrichTurn({ ...record, costUsd: 7 }, 1); + assert.equal(reported.effectiveCostUsd, 7); + assert.ok(reported.pricingWarning != null); + const repaired = prices.map((entry) => entry.model === 'gateway/claude-sonnet-4-6' + ? { ...entry, aliases: ['claude-sonnet-4-6'] } : entry); + await writeFile(join(dir, 'pricing.json'), JSON.stringify({ version: 1, currency: 'USD', prices: repaired })); + Date.now = () => originalNow() + 31_000; + await ensurePricing(); + const fixed = enrichTurn(record, 1); + assert.equal(fixed.effectiveCostUsd, 50); + assert.equal(fixed.pricingWarning, null); + assert.equal(summaryFromRows([fixed]).pricingWarningTurns, 0); + } finally { + Date.now = originalNow; + if (oldHome === undefined) delete process.env.PASEO_HOME; else process.env.PASEO_HOME = oldHome; + await rm(home, { recursive: true, force: true }); + } }); diff --git a/server/pricing.ts b/server/pricing.ts index 01e41e2..1dd084a 100644 --- a/server/pricing.ts +++ b/server/pricing.ts @@ -3,12 +3,12 @@ import { homedir } from "node:os"; import { join } from "node:path"; import { z } from "zod"; import type { CostSource, TurnRecord, TurnRow } from "../shared/ledger.ts"; -import { canonicalModel, costBreakdownWithPricing, modelPricing, type ModelPricing } from "../shared/pricing.ts"; -import { freshInput, usageSemantics } from "../shared/semantics.ts"; +import { canonicalModel, costBreakdownWithPricing, knownCost, modelPricing, partialCost, type CostBreakdown, type ModelPricing } from "../shared/pricing.ts"; +import { freshInput, promptTokens, tokenScope, usageSemantics } from "../shared/semantics.ts"; import { getPreferences, preferencesRevision } from './preferences.ts'; type PriceTier = ModelPricing & { upToInputTokens?: number }; -type PriceEntry = { providers?: string[]; model: string; tiers: PriceTier[] }; +type PriceEntry = { providers?: string[]; model: string; aliases?: string[]; tiers: PriceTier[] }; export const PriceFileSchema = z.object({ version: z.literal(1), @@ -18,11 +18,12 @@ export const PriceFileSchema = z.object({ z.object({ providers: z.array(z.string()).optional(), model: z.string(), + aliases: z.array(z.string().min(1)).optional(), tiers: z.array( z.object({ upToInputTokens: z.number().int().positive().optional(), input: z.number().nonnegative(), - cacheRead: z.number().nonnegative(), + cacheRead: z.number().finite().nonnegative().optional(), cacheWrite: z.number().finite().nonnegative().optional(), output: z.number().nonnegative(), }), @@ -38,142 +39,24 @@ const DATA_DIR = join(process.env.PASEO_HOME ?? join(homedir(), ".paseo"), "plug const PRICE_FILE = join(DATA_DIR, "pricing.json"); const OPENROUTER_CACHE_FILE = join(DATA_DIR, "openrouter-pricing.json"); const OPENROUTER_MAX_AGE_MS = 24 * 60 * 60 * 1000; -const TOKENROUTER_QUOTA_PER_USD = 13.5414; // 1 quota = CNY 0.5 at CNY 6.7707 / USD -/* model|max prompt tokens|input quota|cache-read quota|output quota. Blank max = final tier. */ -const TOKENROUTER_ROWS = ` -claude-fable-5||135.41|13.54|677.07 -claude-haiku-4.5||13.54|1.35|67.71 -claude-opus-4.5||67.71|6.77|338.54 -claude-opus-4.6||67.71|6.77|338.54 -claude-opus-4.6-hq||67.71|6.77|338.54 -claude-opus-4.7||67.71|6.77|338.54 -claude-opus-4.8||67.71|6.77|338.54 -claude-opus-5||67.71|6.77|338.54 -claude-sonnet-4.5||40.62|4.06|203.12 -claude-sonnet-4.6||40.62|4.06|203.12 -claude-sonnet-5||27.08|2.71|135.41 -deepseek-v3.2||2|0.4|3 -deepseek-v4-flash||3|0.1|9 -deepseek-v4-flash-vision-exp||3|0.1|9 -deepseek-v4-pro||9|0.3|27 -gemini-3-flash-preview||6.77|0.68|40.62 -gemini-3.1-flash-lite||3.39|0.34|20.31 -gemini-3.1-pro-preview|200000|27.08|2.71|162.5 -gemini-3.1-pro-preview||54.17|5.42|243.75 -gemini-3.5-flash||20.31|2.03|121.87 -gemini-3.7-flash||10.16|1.02|50.78 -glm-5|32000|4|1|18 -glm-5||6|1.5|22 -glm-5-turbo|32000|5|1.2|22 -glm-5-turbo||7|1.8|26 -glm-5.1|32000|6|1.3|24 -glm-5.1||8|2|28 -glm-5.2||8|2|28 -glm-5.3||8|2|28 -glm-5.3-flash||0.8|0.23|2.8 -glm-5v-turbo|32000|5|1.2|22 -glm-5v-turbo||7|1.8|26 -google.gemini-2.5-pro|200000|16.93|1.69|135.41 -google.gemini-2.5-pro||33.85|3.39|203.12 -gpt-4o-mini||2.03|1.02|8.12 -gpt-5-mini||3.39|0.34|27.08 -gpt-5.4||33.85|3.39|203.12 -gpt-5.4-mini||10.16|1.02|60.94 -gpt-5.5||67.71|6.77|406.24 -gpt-5.6-luna||13.54|1.35|81.25 -gpt-5.6-sol||67.71|6.77|406.24 -gpt-5.6-terra||33.85|3.39|203.12 -gpt-6-astra|272000|135.41|13.54|677.07 -gpt-6-astra||270.83|27.08|1015.61 -hy3||1|0.25|4 -hy3-preview|15999|1.2|0.4|4 -hy3-preview|31999|1.6|0.6|6.4 -hy3-preview||2|0.8|8 -hy4-preview||6|0.3|18 -kimi-k2.5||4|0.7|21 -kimi-k2.6||6.5|1.1|27 -kimi-k2.7-code||6.43|1.29|27.08 -kimi-k2.7-code-highspeed||12.86|2.57|54.17 -kimi-k3||20|2|100 -mimo-v2.5-pro||3|0.03|6 -minimax-m2.5||2.1|0.21|8.4 -minimax-m2.7||2.1|0.42|8.4 -minimax-m3|512000|2.1|0.42|8.4 -minimax-m3||4.2|0.84|16.8 -qwen3.5-35b-a3b||0.95|0.95|6.77 -qwen3.5-flash|128000|0.2|0.02|2 -qwen3.5-flash|256000|0.8|0.08|8 -qwen3.5-flash||1.2|0.12|12 -qwen3.5-plus|128000|0.8|0.08|4.8 -qwen3.5-plus|256000|2|0.2|12 -qwen3.5-plus||4|0.4|24 -qwen3.6-27b||1.9|1.9|13.47 -qwen3.8-max-0902||12|1.5|36 -xai.grok-3|199999|16.93|2.71|33.85 -xai.grok-3||33.85|5.42|67.71 -xai.grok-4|199999|16.93|2.71|33.85 -xai.grok-4||33.85|5.42|67.71 -xai.grok-4.20-0309-non-reasoning|199999|16.93|2.71|33.85 -xai.grok-4.20-0309-non-reasoning||33.85|5.42|67.71 -xai.grok-4.20-0309-reasoning|199999|16.93|2.71|33.85 -xai.grok-4.20-0309-reasoning||33.85|5.42|67.71 -xai.grok-4.20-multi-agent|199999|16.93|2.71|33.85 -xai.grok-4.20-multi-agent||33.85|5.42|67.71 -xai.grok-4.20-multi-agent-0309|199999|16.93|2.71|33.85 -xai.grok-4.20-multi-agent-0309||33.85|5.42|67.71 -xai.grok-4.20-non-reasoning|199999|16.93|2.71|33.85 -xai.grok-4.20-non-reasoning||33.85|5.42|67.71 -xai.grok-4.20-reasoning|199999|16.93|2.71|33.85 -xai.grok-4.20-reasoning||33.85|5.42|67.71 -xai.grok-4.3|199999|16.93|2.71|33.85 -xai.grok-4.3||33.85|5.42|67.71 -xai.grok-4.6|199999|27.08|6.77|81.25 -xai.grok-4.6||54.17|13.54|162.5`; - -function seedPrices(): PriceEntry[] { - const usd = (quota: string) => Number((Number(quota) / TOKENROUTER_QUOTA_PER_USD).toFixed(6)); - const grouped = new Map(); - for (const line of TOKENROUTER_ROWS.trim().split("\n")) { - const [model, max, input, cacheRead, output] = line.split("|"); - let entry = grouped.get(model); - if (!entry) { - entry = { providers: ["tokenrouter", "deepseek-harness"], model, tiers: [] }; - grouped.set(model, entry); - } - entry.tiers.push({ - ...(max ? { upToInputTokens: Number(max) } : {}), - input: usd(input), - cacheRead: usd(cacheRead), - output: usd(output), - }); - } - return [...grouped.values()]; -} - -function providerMatches(entry: PriceEntry, provider: string | null, model: string | null): boolean { +function providerMatches(entry: PriceEntry, provider: string | null): boolean { if (!entry.providers?.length) return true; - const haystack = `${provider ?? ""} ${model ?? ""}`.toLowerCase(); - return entry.providers.some((part) => haystack.includes(part.toLowerCase())); + return entry.providers.some((part) => (provider ?? '').toLowerCase().includes(part.toLowerCase())); } -function chooseTier(entry: PriceEntry, promptTokens: number): PriceTier { - return entry.tiers.find((tier) => tier.upToInputTokens === undefined || promptTokens <= tier.upToInputTokens) ?? entry.tiers.at(-1)!; +function chooseTier(entry: PriceEntry, prompt: number | null): PriceTier | null { + if (entry.tiers.length === 1) return entry.tiers[0]; + if (prompt === null) return null; + return entry.tiers.find((tier) => tier.upToInputTokens === undefined || prompt <= tier.upToInputTokens) ?? null; } -/** Exposed for deterministic verification of the shipped TokenRouter seed table. */ -export function tokenRouterDefaultPricing(model: string, promptTokens: number): ModelPricing | null { - const entry = seedPrices().find((candidate) => canonicalModel(candidate.model) === canonicalModel(model)); - if (!entry) return null; - const { input, cacheRead, output } = chooseTier(entry, promptTokens); - return { input, cacheRead, output }; -} -type OpenRouterPrice = { id: string; prompt: number; completion: number; cacheRead: number; cacheWrite?: number }; +type OpenRouterPrice = { id: string; prompt: number; completion: number; cacheRead?: number; cacheWrite?: number }; const OpenRouterCacheSchema = z.object({ fetchedAt: z.string().datetime(), prices: z.array(z.object({ id: z.string(), prompt: z.number().finite().nonnegative(), - completion: z.number().finite().nonnegative(), cacheRead: z.number().finite().nonnegative(), + completion: z.number().finite().nonnegative(), cacheRead: z.number().finite().nonnegative().optional(), cacheWrite: z.number().finite().nonnegative().optional() })), }); @@ -188,14 +71,17 @@ async function loadOverrides(): Promise { try { overridePrices = PriceFileSchema.parse(JSON.parse(await readFile(PRICE_FILE, "utf8"))).prices; } catch (error) { - const seeded = { version: 1, currency: "USD", updatedAt: "2026-09-14", prices: seedPrices() } as const; - overridePrices = seeded.prices.map((entry) => ({ ...entry, tiers: [...entry.tiers] })); + if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) { + console.error('token-ledger: invalid pricing.json; custom estimates unavailable', error); + return; + } + // Community installations start with no account-specific pricing policy. + const empty = { version: 1, currency: 'USD', prices: [] }; try { - await writeFile(PRICE_FILE, `${JSON.stringify(seeded, null, 2)}\n`, { encoding: "utf8", flag: "wx" }); - } catch { - if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) { - console.error("token-ledger: invalid pricing.json; using TokenRouter defaults", error); - } + await writeFile(PRICE_FILE, `${JSON.stringify(empty, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' }); + } catch (writeError) { + if (!(writeError instanceof Error && 'code' in writeError && writeError.code === 'EEXIST')) throw writeError; + overridePrices = PriceFileSchema.parse(JSON.parse(await readFile(PRICE_FILE, 'utf8'))).prices; } } } @@ -223,14 +109,15 @@ function refreshPrices(): void { const prompt = Number(item.pricing?.prompt); const completion = Number(item.pricing?.completion); if (!item.id || !Number.isFinite(prompt) || prompt < 0 || !Number.isFinite(completion) || completion < 0) return []; - const rawCache = Number(item.pricing?.input_cache_read); + const readPrice = item.pricing?.input_cache_read; + const rawCache = typeof readPrice === 'string' && readPrice.trim() !== '' ? Number(readPrice) : NaN; const writePrice = item.pricing?.input_cache_write; const rawWrite = typeof writePrice === 'string' && writePrice.trim() !== '' ? Number(writePrice) : NaN; return [{ id: item.id, prompt: prompt * 1_000_000, completion: completion * 1_000_000, - cacheRead: (Number.isFinite(rawCache) && rawCache >= 0 ? rawCache : prompt) * 1_000_000, + ...(Number.isFinite(rawCache) && rawCache >= 0 ? { cacheRead: rawCache * 1_000_000 } : {}), ...(Number.isFinite(rawWrite) && rawWrite >= 0 ? { cacheWrite: rawWrite * 1_000_000 } : {}), }]; }); @@ -270,11 +157,37 @@ function lookupOpenRouter(model: string): ModelPricing | null { return { input: price.prompt, cacheRead: price.cacheRead, cacheWrite: price.cacheWrite, output: price.completion }; } -function resolvePricing(provider: string | null, model: string | null, promptTokens: number): { pricing: ModelPricing; source: Exclude } | null { +/** Detect the old lossy matching rule only to warn; never select a price with it. */ +function legacyPriceKey(value: string): string { + let model = value.trim().toLowerCase(); + if (model.startsWith('[')) { + try { + const parsed: unknown = JSON.parse(model); + if (Array.isArray(parsed) && typeof parsed.at(-1) === 'string') model = parsed.at(-1); + } catch {} + } + return (model.split('/').at(-1) ?? model).replace(/\[1m\]$/, '').replace(/[._]/g, '-').replace(/-+/g, '-'); +} + + +function resolvePricing(provider: string | null, model: string | null, prompt: number | null): { pricing: ModelPricing; source: Exclude } | { warning: string } | null { if (!model) return null; const canonical = canonicalModel(model); - const override = overridePrices.find((entry) => canonicalModel(entry.model) === canonical && providerMatches(entry, provider, model)); - if (override) return { pricing: chooseTier(override, promptTokens), source: "override" }; + const matches = overridePrices.filter((entry) => + [entry.model, ...(entry.aliases ?? [])].some((id) => canonicalModel(id) === canonical) && providerMatches(entry, provider)); + const scoped = matches.filter((entry) => entry.providers?.length); + const candidates = scoped.length ? scoped : matches; + if (candidates.length > 1) return { warning: `Multiple custom prices match ${model}. Keep one matching entry in pricing.json; reference-price fallback is blocked.` }; + if (candidates.length === 1) { + const pricing = chooseTier(candidates[0], prompt); + return pricing ? { pricing, source: 'override' } : null; + } + const legacyKey = legacyPriceKey(model); + const formerScope = `${provider ?? ''} ${model}`.toLowerCase(); + const legacyMatch = overridePrices.some((entry) => + (!entry.providers?.length || entry.providers.some((scope) => formerScope.includes(scope.toLowerCase()))) + && [entry.model, ...(entry.aliases ?? [])].some((id) => legacyPriceKey(id) === legacyKey)); + if (legacyMatch) return { warning: `Review custom pricing for ${model}: add its full ID to model/aliases and check providers in pricing.json. The old name/provider folding is unsafe; reference-price fallback is blocked.` }; const openrouter = getPreferences().openRouterPricing ? lookupOpenRouter(model) : null; if (openrouter) return { pricing: openrouter, source: "openrouter" }; const builtin = getPreferences().builtinPricing ? modelPricing(model) : null; @@ -283,53 +196,63 @@ function resolvePricing(provider: string | null, model: string | null, promptTok type PriceableUsage = Pick & { requests?: TurnRecord['requests']; + tokenScope?: TurnRecord['tokenScope']; }; -/** Prices raw provider usage, including per-request tiers when observations are available. */ +/** Reference estimates only for usage with a verified per-turn accounting scope. */ export function estimateUsageCost(usage: PriceableUsage): { effectiveCostUsd: number | null; - costSource: Exclude | null; - costBreakdown: ReturnType; + costSource: Exclude | null; + costBreakdown: CostBreakdown | null; + partialCost: boolean; + pricingWarning: string | null; } { - const semantics = usageSemantics(usage.provider, usage.model); - const input = freshInput(semantics, usage.input, usage.cached, usage.cacheWrite); - const promptTokens = (input ?? 0) + (usage.cached ?? 0) + (usage.cacheWrite ?? 0); - const resolved = resolvePricing(usage.provider, usage.model, promptTokens); - let breakdown = resolved ? costBreakdownWithPricing({ ...usage, input, costUsd: null }, resolved.pricing) : null; - if (usage.requests?.length && resolved) { - const parts = usage.requests.map((request) => { - const fresh = freshInput(semantics, request.input, request.cached, request.cacheWrite); - const rate = resolvePricing(usage.provider, usage.model, (fresh ?? 0) + (request.cached ?? 0) + (request.cacheWrite ?? 0)); - return rate ? costBreakdownWithPricing({ ...request, input: fresh, costUsd: null }, rate.pricing) : null; - }); - if (parts.every((part) => part !== null)) { - const sum = parts.reduce((total, part) => ({ inUsd: total.inUsd + part!.inUsd, - cacheUsd: total.cacheUsd + part!.cacheUsd, cacheWriteUsd: total.cacheWriteUsd + (part!.cacheWriteUsd ?? 0), - outUsd: total.outUsd + part!.outUsd }), { inUsd: 0, cacheUsd: 0, cacheWriteUsd: 0, outUsd: 0 }); - const { cacheWriteUsd, ...known } = sum; - breakdown = { ...known, ...(parts.some((part) => part!.cacheWriteUsd !== undefined) ? { cacheWriteUsd } : {}), otherUsd: null }; + const unavailable = { effectiveCostUsd: null, costSource: null, costBreakdown: null, partialCost: true, pricingWarning: null }; + if (tokenScope(usage) !== 'turn' || !usageSemantics(usage.provider)) return unavailable; + const semantics = usageSemantics(usage.provider); + const requests = usage.requests?.length ? usage.requests : [usage]; + let breakdown: CostBreakdown | null = null; + let source: Exclude | null = null; + let incomplete = false; + let pricingWarning: string | null = null; + for (const request of requests) { + const resolved = resolvePricing(usage.provider, usage.model, promptTokens(usage.provider, request)); + if (!resolved) { incomplete = true; continue; } + if ('warning' in resolved) { pricingWarning ??= resolved.warning; incomplete = true; continue; } + const input = freshInput(semantics, request.input, request.cached, request.cacheWrite); + const part = costBreakdownWithPricing({ ...request, input, costUsd: null }, resolved.pricing); + if (!part) { incomplete = true; continue; } + source = resolved.source; + incomplete ||= partialCost(part); + if (!breakdown) breakdown = { ...part }; + else { + for (const field of ['inUsd', 'cacheUsd', 'cacheWriteUsd', 'outUsd'] as const) { + const value = part[field]; + if (value != null) breakdown[field] = (breakdown[field] ?? 0) + value; + } } } - return { - effectiveCostUsd: breakdown ? breakdown.inUsd + breakdown.cacheUsd + (breakdown.cacheWriteUsd ?? 0) + breakdown.outUsd : null, - costSource: resolved?.source ?? null, - costBreakdown: breakdown, - }; + return { effectiveCostUsd: knownCost(breakdown), costSource: source, costBreakdown: breakdown, partialCost: incomplete || !breakdown, pricingWarning }; } export function enrichTurn(record: TurnRecord, seq: number): TurnRow { - const input = freshInput(usageSemantics(record.provider, record.model), record.input, record.cached, record.cacheWrite); + const scope = tokenScope(record); + const input = freshInput(usageSemantics(record.provider), record.input, record.cached, record.cacheWrite); const estimated = estimateUsageCost(record); - const estimate = estimated.effectiveCostUsd; - const breakdown = estimated.costBreakdown && record.costUsd !== null - ? { ...estimated.costBreakdown, otherUsd: record.costUsd - estimate! } + const reported = scope === 'turn' ? record.costUsd : null; + const breakdown = estimated.costBreakdown && reported !== null + ? { ...estimated.costBreakdown, otherUsd: reported - estimated.effectiveCostUsd! } : estimated.costBreakdown; return { - ...record, - input, - seq, - effectiveCostUsd: record.costUsd ?? estimate, - costSource: record.costUsd !== null ? "reported" : estimated.costSource, + ...record, input, seq, tokenScope: scope, + promptTokens: promptTokens(record.provider, record), + incompleteUsage: scope !== 'turn' || record.quality !== 'exact' || !!record.usageGap + || input === null || record.cached === null || record.cacheWrite == null || record.output === null, + partialCost: reported === null && (estimated.partialCost || record.quality !== 'exact' || !!record.usageGap), + pricingWarning: estimated.pricingWarning, + costUsd: reported, + effectiveCostUsd: reported ?? estimated.effectiveCostUsd, + costSource: reported !== null ? 'reported' : estimated.costSource, costBreakdown: breakdown, }; } diff --git a/server/read-model.test.ts b/server/read-model.test.ts new file mode 100644 index 0000000..00bba99 --- /dev/null +++ b/server/read-model.test.ts @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { finalizeTurn } from '../shared/aggregate.ts'; +import { enrichTurn } from './pricing.ts'; +import { summaryFromRows } from './read-model.ts'; + +const base = { agentId: 'a', turnId: 't', provider: 'omp', model: 'gpt-model', startedAt: null, + endedAt: '2026-10-14T00:00:00Z', status: 'completed' as const, observations: [], prevSessionCostUsd: null }; + +test('historical cumulative snapshots remain visible without duplicating turn totals or prices', () => { + const first = finalizeTurn({ ...base, finalUsage: { inputTokens: 1000, cachedInputTokens: 4000, outputTokens: 100, totalCostUsd: 1 } }); + const next = finalizeTurn({ ...base, finalUsage: { inputTokens: 2000, cachedInputTokens: 9000, outputTokens: 200, totalCostUsd: 2 } }); + const rows = [first, next].map((r, i) => enrichTurn({ ...r, tokenScope: undefined, costUsd: 9 }, i + 1)); + assert.equal(rows[1].input, 2000); + assert.equal(rows[1].tokenScope, 'session'); + assert.equal(rows[1].effectiveCostUsd, null); + const summary = summaryFromRows(rows); + assert.equal(summary.input, null); + assert.equal(summary.cached, null); + assert.equal(summary.effectiveCostUsd, null); + assert.equal(summary.unmeasuredTurns, 2); + assert.equal(summary.unpricedTurns, 2); +}); + +test('known turn values and measured zero survive beside unknown scope without hiding coverage', () => { + const known = finalizeTurn({ ...base, provider: 'claude', finalUsage: { inputTokens: 0, cachedInputTokens: 10, cacheWriteInputTokens: 0, outputTokens: 0, totalCostUsd: 1 } }); + const unknown = finalizeTurn({ ...base, provider: 'custom', finalUsage: { inputTokens: 9000, outputTokens: 9000, totalCostUsd: 20 } }); + const summary = summaryFromRows([enrichTurn(known, 1), enrichTurn(unknown, 2)]); + assert.equal(summary.input, 0); + assert.equal(summary.cached, 10); + assert.equal(summary.output, 0); + assert.equal(summary.effectiveCostUsd, 1); + assert.equal(summary.unmeasuredTurns, 1); + assert.equal(summary.incompleteTurns, 1); + assert.equal(summary.unpricedTurns, 1); +}); diff --git a/server/read-model.ts b/server/read-model.ts index ff9287b..8dcabea 100644 --- a/server/read-model.ts +++ b/server/read-model.ts @@ -3,13 +3,17 @@ import { allRecords, storeRevision } from './store.ts'; import { enrichTurn, pricingRevision } from './pricing.ts'; export const EMPTY_SUMMARY: Summary = { turns: 0, - input: 0, - cached: 0, - output: 0, + input: null, + cached: null, + output: null, costUsd: null, effectiveCostUsd: null, estimatedTurns: 0, unpricedTurns: 0, + unmeasuredTurns: 0, + incompleteTurns: 0, + partialCostTurns: 0, + pricingWarningTurns: 0, }; /** Sums enriched rows, so a caller can summarize any subset of the read model. */ @@ -17,14 +21,22 @@ export function summaryFromRows(rows: readonly TurnRow[]): Summary { const summary: Summary = { ...EMPTY_SUMMARY }; for (const row of rows) { summary.turns += 1; - summary.input += row.input ?? 0; - summary.cached += row.cached ?? 0; + if (row.incompleteUsage) summary.incompleteTurns += 1; + if (row.pricingWarning) summary.pricingWarningTurns += 1; + if (row.tokenScope !== 'turn') { + summary.unmeasuredTurns += 1; + summary.unpricedTurns += 1; + continue; + } + if (row.input !== null) summary.input = (summary.input ?? 0) + row.input; + if (row.cached !== null) summary.cached = (summary.cached ?? 0) + row.cached; if (row.cacheWrite != null) summary.cacheWrite = (summary.cacheWrite ?? 0) + row.cacheWrite; - summary.output += row.output ?? 0; + if (row.output !== null) summary.output = (summary.output ?? 0) + row.output; if (row.costUsd !== null) summary.costUsd = (summary.costUsd ?? 0) + row.costUsd; if (row.effectiveCostUsd !== null) { summary.effectiveCostUsd = (summary.effectiveCostUsd ?? 0) + row.effectiveCostUsd; if (row.costSource !== "reported") summary.estimatedTurns += 1; + if (row.partialCost) summary.partialCostTurns += 1; } else { summary.unpricedTurns += 1; } diff --git a/server/tracker.ts b/server/tracker.ts index c2565d9..83012f4 100644 --- a/server/tracker.ts +++ b/server/tracker.ts @@ -4,9 +4,10 @@ import type { PluginLifecycleEvents } from "@getpaseo/plugin/server"; import { subscribeAgents } from "../shared/agents.ts"; import type { CatalogSubscription } from '../shared/catalog-subscription.ts'; import type { PaseoAgent, PaseoAgentTimelineEvent, PaseoApi } from "./paseo-types.ts"; -import type { AgentUsageRow, InFlight, OverviewResult, Summary, SyncResult, TurnRecord } from "../shared/ledger.ts"; -import { freshInput, usageSemantics, providerSemantics } from "../shared/semantics.ts"; -import { ensurePricing, estimateUsageCost } from "./pricing.ts"; +import type { AgentUsageRow, InFlight, OverviewResult, Summary, SyncResult, TurnRecord, UsageSnapshot } from "../shared/ledger.ts"; +import { freshInput, tokenScope, usageSemantics } from "../shared/semantics.ts"; +import { finalizeTurn } from '../shared/aggregate.ts'; +import { ensurePricing, enrichTurn } from "./pricing.ts"; import { allRecords, flushStore, @@ -249,47 +250,42 @@ export function stopTracker(): Promise { function inFlightFor(agentId: string): InFlight | null { const state = agents.get(agentId); const open = state?.open; - if (!open) return null; - const semantics = usageSemantics(open.provider, open.model); - let input: number | null = null; - let rawInput: number | null = null; - let cached: number | null = null; - let cacheWrite: number | null = null; - let output: number | null = null; - const observations = providerSemantics(open.provider).tokens === "request" ? open.observations : open.observations.slice(-1); - for (const observation of observations) { - const fresh = freshInput(semantics, observation.input, observation.cached, observation.cacheWrite); - if (observation.input !== null) rawInput = (rawInput ?? 0) + observation.input; - if (fresh !== null) input = (input ?? 0) + fresh; - if (observation.cached !== null) cached = (cached ?? 0) + observation.cached; - if (observation.cacheWrite != null) cacheWrite = (cacheWrite ?? 0) + observation.cacheWrite; - if (observation.output !== null) output = (output ?? 0) + observation.output; - } - const usage = open.lastUsage; - const estimated = estimateUsageCost({ - provider: open.provider, - model: open.model, - input: rawInput, - cached, - cacheWrite, - output, - ...(providerSemantics(open.provider).tokens === "request" - ? { requests: open.observations.map(({ cost, ...tokens }) => tokens) } - : {}), - }); + if (!state || !open) return null; + const record = finalizeTurn({ agentId, turnId: open.turnId, provider: open.provider, model: open.model, + startedAt: open.startedAt, endedAt: new Date().toISOString(), status: 'unknown', + observations: open.observations, finalUsage: null, prevSessionCostUsd: state.prevSessionCostUsd }); + if (open.usageGap) record.usageGap = true; + if (state.costBaselineUncertain) record.costUsd = null; + const row = enrichTurn(record, 1); return { ...(open.usageGap ? { usageGap: true } : {}), - turnId: open.turnId, - startedAt: open.startedAt, - modelCalls: open.observations.length, - input, - cached, - cacheWrite, - output, - effectiveCostUsd: estimated.effectiveCostUsd, - costSource: estimated.costSource, - ctxUsed: typeof usage?.contextWindowUsedTokens === "number" ? usage.contextWindowUsedTokens : null, - ctxMax: typeof usage?.contextWindowMaxTokens === "number" ? usage.contextWindowMaxTokens : null, + turnId: open.turnId, startedAt: open.startedAt, modelCalls: row.modelCalls, + tokenScope: row.tokenScope, promptTokens: row.promptTokens, partialCost: row.partialCost, + pricingWarning: row.pricingWarning, + input: row.input, cached: row.cached, cacheWrite: row.cacheWrite, output: row.output, + effectiveCostUsd: row.effectiveCostUsd, costSource: row.costSource, + ctxUsed: open.lastUsage?.contextWindowUsedTokens ?? null, + ctxMax: open.lastUsage?.contextWindowMaxTokens ?? null, + }; +} + +function usageSnapshotFor(agentId: string): UsageSnapshot | null { + const state = agents.get(agentId); + const previous = lastRecordForAgent(agentId); + const provider = state?.provider ?? previous?.provider ?? null; + const scope = tokenScope({ provider }); + if (scope === 'turn') return null; + const compatible = !state?.sessionId || state.sessionId === previous?.sessionId; + const observation = state?.lastObservation ?? (compatible && previous ? { + input: previous.input, cached: previous.cached, cacheWrite: previous.cacheWrite, + output: previous.output, cost: previous.sessionCostUsd, + } : null); + if (!observation) return null; + return { + scope, input: freshInput(usageSemantics(provider), observation.input, observation.cached, observation.cacheWrite), + cached: observation.cached, cacheWrite: observation.cacheWrite ?? null, output: observation.output, + reportedCostUsd: observation.cost, + observedAt: state?.lastObservedAt ?? previous?.endedAt ?? null, }; } @@ -311,6 +307,7 @@ export async function handleSync( records: input.knownRecordsRevision === recordsRevision ? [] : (group?.rows.slice(0, input.limit ?? 50) ?? []), summary, ctx: agents.get(input.agentId)?.lastCtx ?? null, + usageSnapshot: usageSnapshotFor(input.agentId), }; } @@ -340,14 +337,18 @@ export async function handleOverview(input: { since?: string }, context: { paseo // A range lists only the agents that used tokens in it or that work now. if (since !== null && summary.turns === 0 && open === null) continue; totals.turns += summary.turns; - totals.input += summary.input; - totals.cached += summary.cached; + if (summary.input !== null) totals.input = (totals.input ?? 0) + summary.input; + if (summary.cached !== null) totals.cached = (totals.cached ?? 0) + summary.cached; if (summary.cacheWrite != null) totals.cacheWrite = (totals.cacheWrite ?? 0) + summary.cacheWrite; - totals.output += summary.output; + if (summary.output !== null) totals.output = (totals.output ?? 0) + summary.output; if (summary.costUsd !== null) totals.costUsd = (totals.costUsd ?? 0) + summary.costUsd; if (summary.effectiveCostUsd !== null) totals.effectiveCostUsd = (totals.effectiveCostUsd ?? 0) + summary.effectiveCostUsd; totals.estimatedTurns += summary.estimatedTurns; totals.unpricedTurns += summary.unpricedTurns; + totals.unmeasuredTurns += summary.unmeasuredTurns; + totals.incompleteTurns += summary.incompleteTurns; + totals.partialCostTurns += summary.partialCostTurns; + totals.pricingWarningTurns += summary.pricingWarningTurns; const row: AgentUsageRow = { agentId, title: snapshot?.title ?? null, diff --git a/server/turns.test.ts b/server/turns.test.ts index 5697e93..4769f1c 100644 --- a/server/turns.test.ts +++ b/server/turns.test.ts @@ -167,3 +167,24 @@ test('a Codex delivery gap does not mark every subsequent uninterrupted turn as const next = streamTurn(state, finish('t2'))!; assert.equal(next.usageGap, undefined); }); + +test('late OMP usage refreshes the persisted snapshot without reopening or billing a closed turn', () => { + const state = createState(); + const activeTurn = { turnId: 't1', startedAt: start }; + noteUsage(state, { id: 'a', provider: 'omp', model: 'gpt-model', activeTurn, + runtimeInfo: { sessionId: 's' }, lastUsage: { inputTokens: 1000, cachedInputTokens: 4000, outputTokens: 100 } }); + const record = streamTurn(state, event({ type: 'turn_completed', provider: 'omp', turnId: 't1' }))!; + assert.equal(record.tokenScope, 'session'); + noteUsage(state, { id: 'a', provider: 'omp', activeTurn, + lastUsage: { inputTokens: 1300, cachedInputTokens: 4500, outputTokens: 200 } }); + assert.equal(state.open, null); + const restored = AgentStateSchema.parse(JSON.parse(JSON.stringify(state))); + assert.equal(restored.lastObservation?.input, 1300); + assert.equal(restored.lastObservation?.output, 200); + // A lower branch-view snapshot is not a fresh billing counter or a turn delta. + noteUsage(restored, { id: 'a', provider: 'omp', activeTurn: null, + lastUsage: { inputTokens: 100, cachedInputTokens: 0, outputTokens: 10 } }); + assert.equal(restored.lastObservation?.input, 100); + assert.equal(restored.open, null); + assert.equal(record.costUsd, null); +}); diff --git a/server/turns.ts b/server/turns.ts index 71e67df..ac39784 100644 --- a/server/turns.ts +++ b/server/turns.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import type { PaseoAgentTimelineEvent } from "./paseo-types.ts"; import { finalizeTurn, mergeObservation, sameTokens, tokenObservation, type UsageLike } from "../shared/aggregate.ts"; import type { TurnRecord } from "../shared/ledger.ts"; +import { tokenScope } from '../shared/semantics.ts'; const nullableCount = z.number().finite().nonnegative().nullable(); const observationSchema = z.object({ input: nullableCount, cached: nullableCount, cacheWrite: nullableCount.optional(), output: nullableCount, cost: nullableCount }); @@ -16,6 +17,7 @@ export const AgentStateSchema = z.object({ }).nullable(), prevSessionCostUsd: nullableCount, lastObservation: observationSchema.nullable(), costBaselineUncertain: z.boolean().optional(), + lastObservedAt: z.string().nullable().optional(), usageGapPending: z.boolean().optional(), lastCtx: z.object({ used: z.number(), max: z.number() }).nullable(), lastClosedTurnId: z.string().nullable(), lastClosedAt: z.string().nullable(), @@ -75,6 +77,7 @@ function sessionChanged(agentId: string, state: AgentState, sessionId: string | state.costBaselineUncertain = false; state.usageGapPending = false; state.lastObservation = null; + state.lastObservedAt = null; state.lastClosedTurnId = null; state.lastClosedAt = null; } @@ -92,20 +95,25 @@ export function noteUsage(state: AgentState, snapshot: AgentSnapshotLike): TurnR state.open.provider ??= state.provider; state.open.model ??= state.model; } + const active = snapshot.activeTurn; + if (active && state.open && active.turnId !== state.open.turnId) return interrupted; + const closedReplay = active && active.turnId === state.lastClosedTurnId && active.turnId !== state.open?.turnId + && (!active.startedAt || !state.lastClosedAt || Date.parse(active.startedAt) <= Date.parse(state.lastClosedAt)); + if (closedReplay && tokenScope(state) === 'turn') return interrupted; + const observation = tokenObservation(snapshot.lastUsage); + const isNew = observation !== null && (state.lastObservation === null || !sameTokens(state.lastObservation, observation)); + if (observation) { + state.lastObservation = state.lastObservation && !isNew + ? mergeObservation(state.lastObservation, observation) : observation; + state.lastObservedAt = new Date().toISOString(); + } const usage = snapshot.lastUsage; if (usage && Number.isFinite(usage.contextWindowUsedTokens) && Number.isFinite(usage.contextWindowMaxTokens)) { state.lastCtx = { used: usage.contextWindowUsedTokens!, max: usage.contextWindowMaxTokens! }; } - const active = snapshot.activeTurn; - if (active && active.turnId === state.lastClosedTurnId && active.turnId !== state.open?.turnId) { - if (!active.startedAt || !state.lastClosedAt || Date.parse(active.startedAt) <= Date.parse(state.lastClosedAt)) return interrupted; - state.lastClosedTurnId = null; // verified newer occurrence of a reused ID - } - if (active && state.open && active.turnId !== state.open.turnId) return interrupted; - const observation = tokenObservation(usage); - const isNew = observation !== null && (state.lastObservation === null || !sameTokens(state.lastObservation, observation)); - if (observation) state.lastObservation = state.lastObservation && !isNew - ? mergeObservation(state.lastObservation, observation) : observation; + // Cumulative/unknown late observations remain visible as snapshots only. + if (closedReplay) return interrupted; + if (active?.turnId === state.lastClosedTurnId) state.lastClosedTurnId = null; if (!state.open && !active) { state.usageGapPending = false; if (typeof usage?.totalCostUsd === "number" && Number.isFinite(usage.totalCostUsd) && usage.totalCostUsd >= 0) { diff --git a/shared/aggregate.test.ts b/shared/aggregate.test.ts index 34eea4c..0313f50 100644 --- a/shared/aggregate.test.ts +++ b/shared/aggregate.test.ts @@ -104,13 +104,13 @@ test("cumulative session cost produces a per-turn delta", () => { assert.equal(record.sessionCostUsd, 0.08); }); -test("cost lower than previous raw is treated as a reset, raw kept", () => { +test("a lower cumulative cost is not a verified session reset", () => { const record = finalizeTurn({ ...base, finalUsage: { inputTokens: 10, outputTokens: 10, totalCostUsd: 0.02 }, prevSessionCostUsd: 0.05, }); - assert.equal(record.costUsd, 0.02); + assert.equal(record.costUsd, null); assert.equal(record.sessionCostUsd, 0.02); }); @@ -165,3 +165,13 @@ test('Codex includes an unseen terminal request and retains raw per-request coun assert.equal(record.input, 220); assert.equal(record.requests?.length, 2); assert.equal(record.quality, 'partial'); }); + +test('partial request fields do not produce falsely complete component totals', () => { + const record = finalizeTurn({ ...base, provider: 'codex', observations: [ + { input: 100, cached: 80, cacheWrite: 10, output: 20, cost: null }, + { input: 120, cached: 90, output: null, cost: null }, + ] }); + assert.equal(record.input, 220); + assert.ok(record.cacheWrite == null); + assert.equal(record.output, null); +}); diff --git a/shared/aggregate.ts b/shared/aggregate.ts index 4ccb7c2..c0ed30e 100644 --- a/shared/aggregate.ts +++ b/shared/aggregate.ts @@ -1,4 +1,4 @@ -import { providerSemantics } from "./semantics.ts"; +import { providerSemantics, tokenScope } from "./semantics.ts"; import type { TurnRecord } from "./ledger.ts"; /** Reducer input. Paseo 0.8 exposes these fields except cacheWriteInputTokens. */ @@ -54,7 +54,8 @@ function sumField(observations: Observation[], field: "input" | "cached" | "cach let total: number | null = null; for (const observation of observations) { const value = observation[field]; - if (value != null) total = (total ?? 0) + value; + if (value == null) return null; + total = (total ?? 0) + value; } return total; } @@ -115,8 +116,10 @@ export function finalizeTurn(args: FinalizeInput): TurnRecord { const rawCost = num(args.finalUsage?.totalCostUsd) ?? observations.at(-1)?.cost ?? null; let costUsd: number | null = semantics.cost === "session" ? rawCost : null; - if (semantics.cost === "session" && rawCost !== null && args.prevSessionCostUsd !== null && rawCost >= args.prevSessionCostUsd) { - costUsd = Number((rawCost - args.prevSessionCostUsd).toFixed(6)); + if (semantics.cost === "session" && rawCost !== null && args.prevSessionCostUsd !== null) { + // Only a verified session change resets the baseline; a drop is not proof. + costUsd = rawCost >= args.prevSessionCostUsd + ? Number((rawCost - args.prevSessionCostUsd).toFixed(6)) : null; } const durationMs = @@ -130,6 +133,7 @@ export function finalizeTurn(args: FinalizeInput): TurnRecord { agentId: args.agentId, turnId: args.turnId, provider: args.provider, + tokenScope: tokenScope(args), model: args.model, startedAt: args.startedAt, endedAt: args.endedAt, diff --git a/shared/ledger.ts b/shared/ledger.ts index 038e84f..7a9dcfd 100644 --- a/shared/ledger.ts +++ b/shared/ledger.ts @@ -8,6 +8,8 @@ export const TurnRecordSchema = z.object({ turnId: z.string().nullable(), provider: z.string().nullable(), model: z.string().nullable(), + /** Omitted in v1 history; inferred only from verified harness semantics. */ + tokenScope: z.enum(['turn', 'session', 'unknown']).optional(), /** Provider session identity, when observed; optional for pre-0.8 records. */ sessionId: z.string().nullable().optional(), startedAt: z.string().nullable(), @@ -17,11 +19,10 @@ export const TurnRecordSchema = z.object({ /** A subscription gap may have hidden requests or the terminal outcome. */ usageGap: z.boolean().optional(), /** - * Turn token totals. On disk these are exactly as reported upstream (some - * providers count cache reads inside input — see semantics.ts); rows served - * over RPC normalize verified inclusive providers to ordinary input, with - * cache reads and known cache writes separate. Unknown semantics pass through. - * Null when the provider reported nothing. + * Raw observed counts on disk. Only turn-scoped observations enter totals. + * Session/unknown snapshots remain visible but cannot be added across turns. + * RPC input is uncached input for known harnesses, possibly including writes + * when their count was not exposed. Null means unavailable, not zero. */ input: z.number().nullable(), cached: z.number().nullable(), @@ -51,16 +52,23 @@ export const CostSourceSchema = z.enum(["reported", "override", "openrouter", "b export type CostSource = z.infer; export const CostBreakdownSchema = z.object({ - inUsd: z.number(), - cacheUsd: z.number(), - cacheWriteUsd: z.number().optional(), - outUsd: z.number(), + inUsd: z.number().nullable(), + cacheUsd: z.number().nullable(), + cacheWriteUsd: z.number().nullable().optional(), + outUsd: z.number().nullable(), otherUsd: z.number().nullable(), }); /** A record enriched at read time. The JSONL remains untouched and contains only upstream facts. */ export const TurnRowSchema = TurnRecordSchema.extend({ seq: z.number().int().positive(), + tokenScope: z.enum(['turn', 'session', 'unknown']), + /** Null when the prompt denominator cannot be established. */ + promptTokens: z.number().nullable(), + incompleteUsage: z.boolean(), + /** Some components or rates are unavailable; cost is only a partial estimate. */ + partialCost: z.boolean(), + pricingWarning: z.string().nullable(), effectiveCostUsd: z.number().nullable(), costSource: CostSourceSchema.nullable(), costBreakdown: CostBreakdownSchema.nullable(), @@ -72,6 +80,10 @@ export const InFlightSchema = z.object({ turnId: z.string().nullable(), startedAt: z.string(), modelCalls: z.number(), + tokenScope: z.enum(['turn', 'session', 'unknown']), + promptTokens: z.number().nullable(), + partialCost: z.boolean(), + pricingWarning: z.string().nullable(), input: z.number().nullable(), cached: z.number().nullable(), cacheWrite: z.number().nullable().optional(), @@ -86,15 +98,20 @@ export type InFlight = z.infer; export const SummarySchema = z.object({ turns: z.number(), - input: z.number(), - cached: z.number(), - cacheWrite: z.number().optional(), - output: z.number(), + input: z.number().nullable(), + cached: z.number().nullable(), + cacheWrite: z.number().nullable().optional(), + output: z.number().nullable(), costUsd: z.number().nullable(), /** Reported + estimated costs for priced turns. */ effectiveCostUsd: z.number().nullable(), estimatedTurns: z.number().int().nonnegative(), unpricedTurns: z.number().int().nonnegative(), + /** Observations that are not attributable to an individual turn. */ + unmeasuredTurns: z.number().int().nonnegative(), + incompleteTurns: z.number().int().nonnegative(), + partialCostTurns: z.number().int().nonnegative(), + pricingWarningTurns: z.number().int().nonnegative(), }); export type Summary = z.infer; @@ -103,6 +120,15 @@ export const CtxSchema = z.object({ max: z.number(), }); export type Ctx = z.infer; +export const UsageSnapshotSchema = z.object({ + scope: z.enum(['session', 'unknown']), + input: z.number().nullable(), cached: z.number().nullable(), + cacheWrite: z.number().nullable(), output: z.number().nullable(), + reportedCostUsd: z.number().nullable(), + observedAt: z.string().nullable(), +}); +export type UsageSnapshot = z.infer; + const SyncOutputSchema = z.object({ recordsRevision: z.string(), @@ -111,6 +137,8 @@ const SyncOutputSchema = z.object({ summary: SummarySchema, /** Last known context-window usage for the agent, in or out of a turn. */ ctx: CtxSchema.nullable(), + /** Latest cumulative/unknown observation, including late idle updates. Not a turn total. */ + usageSnapshot: UsageSnapshotSchema.nullable(), }); export type SyncResult = z.infer; diff --git a/shared/pricing.test.ts b/shared/pricing.test.ts index 3556c85..496ec02 100644 --- a/shared/pricing.test.ts +++ b/shared/pricing.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { test } from "node:test"; -import { cacheRatio, costBreakdown, costBreakdownWithPricing, modelPricing } from "./pricing.ts"; +import { cacheRatio, costBreakdown, costBreakdownWithPricing, knownCost, modelPricing, partialCost } from "./pricing.ts"; test("model matching covers known families and rejects unknown", () => { assert.deepEqual(modelPricing("claude-opus-5"), { input: 5, cacheRead: 0.5, output: 25 }); @@ -13,10 +13,10 @@ test("model matching covers known families and rejects unknown", () => { test('Opus 5.5 uses verified standard prices across aliases without guessing Fast Mode or future models', () => { const standard = { input: 4, cacheRead: 0.2, cacheWrite: 5, output: 20 }; - for (const model of ['claude-opus-5.5', 'claude-opus-5-5', 'anthropic/claude-opus-5.5', 'claude-opus-5-5[1m]']) { + for (const model of ['claude-opus-5.5', 'claude-opus-5-5', 'anthropic/claude-opus-5.5']) { assert.deepEqual(modelPricing(model), standard); } - for (const model of ['claude-opus-5.5-fast', 'claude-opus-5.5:batch', 'claude-opus-6', 'claude-opus-5.6']) { + for (const model of ['claude-opus-5-5[1m]', 'vendor/claude-opus-5-5', '["claude-opus-4","claude-opus-5"]', 'claude-opus-5.5-fast', 'claude-opus-5.5:batch', 'claude-opus-6', 'claude-opus-5.6']) { assert.equal(modelPricing(model), null); } assert.deepEqual(modelPricing('claude-opus-4-1-20250805'), { input: 15, cacheRead: 1.5, output: 75 }); @@ -71,16 +71,17 @@ test("breakdown is null for unknown models or missing usage", () => { }); test("zero-token turn with a real total attributes everything to otherUsd", () => { - const split = costBreakdown({ model: "claude-opus-5", input: 0, cached: 0, output: 0, costUsd: 0.5 }); - assert.deepEqual(split, { inUsd: 0, cacheUsd: 0, outUsd: 0, otherUsd: 0.5 }); + const split = costBreakdown({ model: "claude-opus-5", input: 0, cached: 0, cacheWrite: 0, output: 0, costUsd: 0.5 }); + assert.deepEqual(split, { inUsd: 0, cacheUsd: 0, cacheWriteUsd: 0, outUsd: 0, otherUsd: 0.5 }); }); -test("cacheRatio", () => { - assert.equal(cacheRatio(2000, 8000), 0.8); - assert.equal(cacheRatio(null, 8000), 1); - assert.equal(cacheRatio(2000, null), null); +test('cache rate requires a complete, consistent denominator', () => { + assert.equal(cacheRatio(8000, 10000), 0.8); + assert.equal(cacheRatio(8000, null), null); + assert.equal(cacheRatio(null, 10000), null); assert.equal(cacheRatio(0, 0), null); - assert.equal(cacheRatio(1000, 8000, 1000), 0.8); + assert.equal(cacheRatio(10001, 10000), null); + assert.equal(cacheRatio(0, 10000), 0); }); test('cache writes have an independent price and do not increase reported totals', () => { @@ -88,7 +89,19 @@ test('cache writes have an independent price and do not increase reported totals const split = costBreakdownWithPricing(usage, { input: 1, cacheRead: 0.1, cacheWrite: 2, output: 5 })!; assert.equal(split.inUsd, 0.00001); assert.equal(split.cacheWriteUsd, 0.00006); - assert.equal(split.inUsd + split.cacheUsd + split.cacheWriteUsd! + split.outUsd + split.otherUsd!, 1); - assert.equal(costBreakdownWithPricing(usage, { input: 1, cacheRead: 0.1, output: 5 })!.cacheWriteUsd, 0.00003); + assert.equal(knownCost(split)! + split.otherUsd!, 1); + assert.equal(costBreakdownWithPricing(usage, { input: 1, cacheRead: 0.1, output: 5 })!.cacheWriteUsd, null); assert.equal(costBreakdownWithPricing(usage, { input: 1, cacheRead: 0.1, cacheWrite: 0, output: 5 })!.cacheWriteUsd, 0); }); + +test('unknown token counts and rates stay unknown, while explicit zero remains free', () => { + const missing = costBreakdownWithPricing({ input: null, cached: 8000, output: null, costUsd: null }, { input: 1, cacheRead: 0.1, output: 5 })!; + assert.equal(missing.inUsd, null); + assert.equal(missing.outUsd, null); + assert.equal(missing.cacheWriteUsd, null); + assert.equal(knownCost(missing), 0.0008); + assert.equal(partialCost(missing), true); + const zero = costBreakdownWithPricing({ input: 0, cached: 0, cacheWrite: 0, output: 0, costUsd: null }, { input: 1, output: 5 })!; + assert.equal(knownCost(zero), 0); + assert.equal(partialCost(zero), false); +}); diff --git a/shared/pricing.ts b/shared/pricing.ts index d0e7067..41d4ae5 100644 --- a/shared/pricing.ts +++ b/shared/pricing.ts @@ -1,18 +1,17 @@ /** * Per-model pricing used to estimate a cost breakdown per turn. Upstream only * reports a total costUsd, never per-component costs, so the split shown in - * the UI is an estimate: token counts × list price. When a real total exists, - * the difference between it and the estimate is surfaced as `otherUsd` rather - * than smeared across the components. Missing upstream usage (including - * cache writes in Paseo 0.8) can contribute to that residual. + * the UI is an estimate: token counts × reference price. Reported runtime cost + * is not proof of an invoice. Its difference from known estimated components + * is a residual, not an inferred cache-write charge. * * Prices are $/MTok. Cache write is independent of the input/read rates; * neither cache lifetime nor a provider's write premium is inferred. */ export type ModelPricing = { input: number; - cacheRead: number; - /** Omitted in older price files; writes then use the ordinary input rate. */ + cacheRead?: number; + /** Missing means the write rate is unknown; zero is an explicitly free rate. */ cacheWrite?: number; output: number; }; @@ -33,20 +32,16 @@ const PRICING_TABLE: Array<[RegExp, ModelPricing]> = [ ]; export function canonicalModel(value: string): string { - let model = value.trim().toLowerCase(); - if (model.startsWith('[')) { - try { - const parsed: unknown = JSON.parse(model); - if (Array.isArray(parsed) && typeof parsed.at(-1) === 'string') model = parsed.at(-1); - } catch {} - } - model = model.split('/').at(-1) ?? model; - return model.replace(/\[1m\]$/i, '').replace(/[._]/g, '-').replace(/-+/g, '-'); + const model = value.trim().toLowerCase(); + // Standard Claude numeric versions use both 4.5 and 4-5 spellings. Preserve + // namespaces, context/mode suffixes and arbitrary model identities. + return /^(?:anthropic\/)?claude-(?:opus|sonnet|haiku|fable|mythos)-\d+(?:[.-]\d+)*$/.test(model) + ? model.replace(/\./g, '-') : model; } export function modelPricing(model: string | null): ModelPricing | null { if (!model) return null; - const canonical = canonicalModel(model).replace(/-\d{8}$/, ''); + const canonical = canonicalModel(model).replace(/^anthropic\//, '').replace(/-\d{8}$/, '').replace(/(?<=\d)\.(?=\d)/g, '-'); for (const [pattern, pricing] of PRICING_TABLE) { if (pattern.test(canonical)) return pricing; } @@ -54,15 +49,14 @@ export function modelPricing(model: string | null): ModelPricing | null { } export type CostBreakdown = { - inUsd: number; - cacheUsd: number; - cacheWriteUsd?: number; - outUsd: number; + inUsd: number | null; + cacheUsd: number | null; + cacheWriteUsd?: number | null; + outUsd: number | null; /** - * costUsd minus the list-price estimate of the known components; null when - * no real total was reported. Positive residual is cost the reported token - * counts can't account for (chiefly cache writes on Claude); a small - * negative residual just means the hardcoded list prices overestimate. + * Reported runtime cost minus the sum of known estimated components. + * This includes price/source differences as well as unobserved categories; + * it does not establish actual billing or identify missing token counts. */ otherUsd: number | null; }; @@ -94,22 +88,33 @@ export function costBreakdownWithPricing( pricing: ModelPricing, ): CostBreakdown | null { if (turn.input === null && turn.cached === null && turn.output === null && turn.cacheWrite == null) return null; - const inUsd = ((turn.input ?? 0) * pricing.input) / PER_MTOK; - const cacheUsd = ((turn.cached ?? 0) * pricing.cacheRead) / PER_MTOK; - const cacheWriteUsd = ((turn.cacheWrite ?? 0) * (pricing.cacheWrite ?? pricing.input)) / PER_MTOK; - const outUsd = ((turn.output ?? 0) * pricing.output) / PER_MTOK; - const otherUsd = turn.costUsd !== null ? turn.costUsd - (inUsd + cacheUsd + cacheWriteUsd + outUsd) : null; - return { inUsd, cacheUsd, ...(turn.cacheWrite != null ? { cacheWriteUsd } : {}), outUsd, otherUsd }; + const inUsd = turn.input === null ? null : turn.input * pricing.input / PER_MTOK; + const cacheUsd = turn.cached === null ? null : turn.cached === 0 ? 0 + : pricing.cacheRead === undefined ? null : turn.cached * pricing.cacheRead / PER_MTOK; + const cacheWriteUsd = turn.cacheWrite == null ? null : turn.cacheWrite === 0 ? 0 + : pricing.cacheWrite === undefined ? null : turn.cacheWrite * pricing.cacheWrite / PER_MTOK; + const outUsd = turn.output === null ? null : turn.output * pricing.output / PER_MTOK; + const known = (inUsd ?? 0) + (cacheUsd ?? 0) + (cacheWriteUsd ?? 0) + (outUsd ?? 0); + const otherUsd = turn.costUsd !== null ? turn.costUsd - known : null; + return { inUsd, cacheUsd, cacheWriteUsd, outUsd, otherUsd }; } -/** - * Share of prompt tokens served from cache: cached / (input + cached + writes). - * Expects input in canonical fresh-token form (see semantics.ts) — the server - * normalizes inclusive-input providers before values reach clients. - */ -export function cacheRatio(input: number | null, cached: number | null, cacheWrite?: number | null): number | null { - if (cached === null) return null; - const prompt = (input ?? 0) + cached + (cacheWrite ?? 0); - if (prompt <= 0) return null; +/** Ratio only when the complete prompt denominator is available. */ +export function cacheRatio(cached: number | null, prompt: number | null): number | null { + if (cached === null || prompt === null || prompt <= 0 || cached > prompt) return null; return cached / prompt; } + +/** Known components only; callers must preserve the separate completeness flag. */ +export function knownCost(split: CostBreakdown | null): number | null { + if (!split) return null; + let sum: number | null = null; + for (const value of [split.inUsd, split.cacheUsd, split.cacheWriteUsd, split.outUsd]) { + if (value != null) sum = (sum ?? 0) + value; + } + return sum; +} + +export function partialCost(split: CostBreakdown | null): boolean { + return !split || split.inUsd === null || split.cacheUsd === null || split.cacheWriteUsd == null || split.outUsd === null; +} diff --git a/shared/semantics.test.ts b/shared/semantics.test.ts index 2d4b185..ff6680d 100644 --- a/shared/semantics.test.ts +++ b/shared/semantics.test.ts @@ -1,35 +1,30 @@ -import assert from "node:assert/strict"; -import { test } from "node:test"; -import { freshInput, usageSemantics } from "./semantics.ts"; +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { freshInput, promptTokens, tokenScope, usageSemantics } from './semantics.ts'; -test("semantics lookup matches provider first, then model, else null", () => { - assert.deepEqual(usageSemantics("claude-tokenrouter", null), { inputIncludesCached: false }); - assert.deepEqual(usageSemantics("codex-tokenrouter", null), { inputIncludesCached: true }); - assert.deepEqual(usageSemantics(null, "gpt-5.6-sol"), { inputIncludesCached: true }); - assert.deepEqual(usageSemantics("some-router", "claude-fable-5"), { inputIncludesCached: false }); - assert.equal(usageSemantics("mystery-provider", "mystery-model"), null); - assert.equal(usageSemantics(null, null), null); +test('harness-normalized input is not normalized a second time', () => { + assert.equal(freshInput(usageSemantics('omp'), 160290, 2511639), 160290); + assert.equal(freshInput(usageSemantics('claude-router'), 6, 70751), 6); + assert.equal(freshInput(usageSemantics('codex-router'), 213818, 213252), 566); + assert.equal(tokenScope({ provider: 'omp' }), 'session'); + assert.equal(tokenScope({ provider: 'custom-gpt' }), 'unknown'); + assert.equal(usageSemantics('not-claude'), null); }); -test("freshInput subtracts cache reads for inclusive-input providers", () => { - const codex = usageSemantics("codex-tokenrouter", null); - // Real gpt-5.6-sol turn: inputTokens counts the whole prompt, cache hits included. - assert.equal(freshInput(codex, 213818, 213252), 566); - assert.equal(freshInput(codex, 100, null), 100); - assert.equal(freshInput(codex, null, 5000), null); - // Clamped rather than negative if a provider ever over-reports cache reads. - assert.equal(freshInput(codex, 100, 150), 0); +test('missing and inconsistent cache counts cannot fabricate fresh input', () => { + const codex = usageSemantics('codex'); + assert.equal(freshInput(codex, 100, null), null); + assert.equal(freshInput(codex, 100, 150), null); + assert.equal(freshInput(codex, 100, 60, 30), 10); + assert.equal(freshInput(codex, null, 60), null); }); -test("freshInput passes through for exclusive-input and unknown providers", () => { - const claude = usageSemantics("claude-tokenrouter", null); - assert.equal(freshInput(claude, 78, 2_974_078), 78); - assert.equal(freshInput(null, 78, 2_974_078), 78); - assert.equal(freshInput(null, null, null), null); -}); - -test('cache writes are separate from fresh input only for inclusive providers', () => { - assert.equal(freshInput(usageSemantics('codex', null), 100, 60, 30), 10); - assert.equal(freshInput(usageSemantics('claude', null), 10, 60, 30), 10); - assert.equal(freshInput(null, 100, 60, 30), 100); +test('prompt coverage follows the raw harness convention, not missing-value arithmetic', () => { + const usage = { input: 6, cached: 70751 }; + assert.equal(promptTokens('claude', usage), null); + assert.equal(promptTokens('claude', { ...usage, cacheWrite: 45829 }), 116586); + assert.equal(promptTokens('claude', { ...usage, cacheWrite: 0 }), 70757); + assert.equal(promptTokens('codex', { input: 100, cached: 80 }), 100); + assert.equal(promptTokens('codex', { input: 100, cached: 120 }), null); + assert.equal(promptTokens('unknown', { input: 100, cached: 80, cacheWrite: 0 }), null); }); diff --git a/shared/semantics.ts b/shared/semantics.ts index 1021422..567c207 100644 --- a/shared/semantics.ts +++ b/shared/semantics.ts @@ -1,58 +1,36 @@ -/** - * Providers disagree on what `inputTokens` means. Anthropic reports only the - * fresh (uncached) prompt tokens, with cache reads separate; OpenAI-style - * usage counts the whole prompt, cache hits included, so `inputTokens` already - * contains `cachedInputTokens`. Paseo's AgentUsage passes both through without - * defining which convention applies. - * - * The ledger stores values exactly as reported (so history can always be - * reinterpreted) and normalizes at read time to the canonical form that loses - * no information: input = ordinary input, cached = cache reads, cacheWrite = - * cache creation when reported. Inclusive-input providers are converted - * (input − cached − known writes); the reverse - * direction would be unrecoverable, which is why Anthropic semantics is the - * canonical one. - * - * Entries are added only after checking a real turn record against the - * provider's documented counting rules (see AGENTS.md for the workflow). - * Unknown providers get null and their numbers pass through unchanged — - * displayed as reported, never guessed. - */ -export type UsageSemantics = { - /** True when the provider's inputTokens already includes cachedInputTokens. */ - inputIncludesCached: boolean; -}; +/** Usage belongs to the harness contract, never to the selected model name. */ +export type UsageSemantics = { inputIncludesCached: boolean }; +export type TokenScope = 'turn' | 'session' | 'unknown'; + +const CLAUDE = /^(?:claude|anthropic)(?:$|[-/:])/i; +const CODEX = /^codex(?:$|[-/:])/i; +const OMP = /^omp(?:$|[-/:])/i; -/** Aggregation is a harness contract. A model name cannot establish it. */ export function providerSemantics(provider: string | null): { - tokens: 'turn' | 'request' | 'unknown'; cost: 'session' | 'unknown'; + tokens: 'turn' | 'request' | 'session' | 'unknown'; cost: 'session' | 'unknown'; } { - if (/claude|anthropic/i.test(provider ?? '')) return { tokens: 'turn', cost: 'session' }; - if (/codex/i.test(provider ?? '')) return { tokens: 'request', cost: 'unknown' }; + if (CLAUDE.test(provider ?? '')) return { tokens: 'turn', cost: 'session' }; + if (CODEX.test(provider ?? '')) return { tokens: 'request', cost: 'unknown' }; + // OMP exposes current-branch session totals, not monotonic billing counters. + // Without branch epochs and final-drain ordering, neither tokens nor cost + // can safely be differenced into turns. + if (OMP.test(provider ?? '')) return { tokens: 'session', cost: 'unknown' }; return { tokens: 'unknown', cost: 'unknown' }; } -const SEMANTICS_TABLE: Array<[RegExp, UsageSemantics]> = [ - [/claude|anthropic/i, { inputIncludesCached: false }], - [/codex|openai|gpt/i, { inputIncludesCached: true }], -]; +export function tokenScope(record: { provider: string | null; tokenScope?: TokenScope }): TokenScope { + if (record.tokenScope) return record.tokenScope; + const scope = providerSemantics(record.provider).tokens; + return scope === 'request' ? 'turn' : scope; +} -/** Looks up verified usage semantics by provider, falling back to model. */ -export function usageSemantics(provider: string | null, model: string | null): UsageSemantics | null { - for (const key of [provider, model]) { - if (!key) continue; - for (const [pattern, semantics] of SEMANTICS_TABLE) { - if (pattern.test(key)) return semantics; - } - } +export function usageSemantics(provider: string | null): UsageSemantics | null { + if (CLAUDE.test(provider ?? '') || OMP.test(provider ?? '')) return { inputIncludesCached: false }; + if (CODEX.test(provider ?? '')) return { inputIncludesCached: true }; return null; } -/** - * Canonical ordinary-input count for a single observation or record: subtracts - * reads and known writes when the provider counts them inside inputTokens, passes the - * value through otherwise (including unknown semantics). - */ +/** Uncached input; when writes are unavailable it can still contain writes. */ export function freshInput( semantics: UsageSemantics | null, input: number | null, @@ -60,5 +38,21 @@ export function freshInput( cacheWrite?: number | null, ): number | null { if (!semantics?.inputIncludesCached || input === null) return input; - return Math.max(0, input - (cached ?? 0) - (cacheWrite ?? 0)); + if (cached === null) return null; + const remaining = input - cached - (cacheWrite ?? 0); + return remaining >= 0 ? remaining : null; +} + +/** Complete prompt size from raw usage; missing counts are not measured zero. */ +export function promptTokens(provider: string | null, usage: { + input: number | null; cached: number | null; cacheWrite?: number | null; +}): number | null { + const semantics = usageSemantics(provider); + if (!semantics || usage.input === null) return null; + if (semantics.inputIncludesCached) { + if (usage.cached != null && usage.cached + (usage.cacheWrite ?? 0) > usage.input) return null; + return usage.input; + } + if (usage.cached === null || usage.cacheWrite == null) return null; + return usage.input + usage.cached + usage.cacheWrite; } diff --git a/shared/timeline.ts b/shared/timeline.ts index 90d797f..5e2c201 100644 --- a/shared/timeline.ts +++ b/shared/timeline.ts @@ -6,5 +6,6 @@ export const UsageTimelineSchema = TurnRowSchema.pick({ id: true, input: true, cached: true, cacheWrite: true, output: true, effectiveCostUsd: true, costSource: true, quality: true, status: true, durationMs: true, usageGap: true, -}); + tokenScope: true, incompleteUsage: true, partialCost: true, +}).partial({ tokenScope: true, incompleteUsage: true, partialCost: true }); export type UsageTimeline = z.infer; From 0ac6d7288bfc6db45fce74ea6226b020e9fc5f32 Mon Sep 17 00:00:00 2001 From: stv1024 Date: Fri, 16 Oct 2026 15:33:40 +0800 Subject: [PATCH 2/6] fix: settle OMP turns after the final usage drain OMP emits turn_completed without usage, and its poller publishes the final session snapshot afterwards on the agent_update channel. Turns settled before that snapshot, dropping the last call or recording an empty turn. Session-scope completed turns now keep a journaled `ending` outcome and settle on the first new observation, the next turn start, a 2s drain timeout, or shutdown. The reported status and endedAt are kept. modelCalls is null for session/unknown scope, because a snapshot count is not a model-call count. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 3 +- client/panel.tsx | 2 +- docs/issue-omp-turn-usage-settlement.md | 126 ++++++++++++++++++++++++ server/pricing.ts | 2 + server/tracker.ts | 39 +++++--- server/turns.test.ts | 102 +++++++++++++++++-- server/turns.ts | 39 ++++++-- shared/aggregate.ts | 6 +- shared/ledger.ts | 9 +- 9 files changed, 295 insertions(+), 33 deletions(-) create mode 100644 docs/issue-omp-turn-usage-settlement.md diff --git a/AGENTS.md b/AGENTS.md index 56dd6e5..8115c8c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,7 +68,8 @@ paseo agent archive # 测完归档,别 - 总览是 `addScreen`(id `ledger-overview` 不变),`range` 放在 screen params 里(`today`/`7d`/`30d`/`all`,缺省 all)。screen props 没有 setParams;切换范围用 `client.openScreen`(宿主 router.push,会产生历史记录),标题函数随 params 变化。 - 侧边栏是 `addSidebarFooterItem` + `SidebarRow`,trailing 显示今日费用。"今天"按客户端本地午夜计算,以 ISO `since` 传给 `ledger.overview`;服务端按 `Date.parse(endedAt)` 过滤 read-model 行,不在服务端猜时区。 - `addSurface` / `addSidebarItem` / `openSurface` 已弃用,不要再用。不接 `registerUsageSource`(那是订阅额度窗口,不是逐轮 usage;用户已决定不放进 Usage 面板)。 -- 0.11.2 的 cache-write、Codex 内部子代理 usage、OMP final-drain/branch epoch 缺口仍在。OMP 统计已包含部分子代理,不能无条件再叠加。徽标使用 Session token snapshot,不以 unknown 代替已知快照;未知 scope 不求 token 总数。历史 Timeline 缺少 scope 元数据时只声明元数据缺失,不声明账本排除。更新后重载客户端以使用新字段。 +- OMP 轮末 drain(0.11.2 源码验证):`turn_completed` 不带 usage,先于 poller 的最终 `usage_updated` 发出;最终快照仍带 activeTurn,且 usage 未变化时不发。所以 session scope 的 completed 轮先记 `open.ending`(随 journal 恢复),在第一个新观察、下一轮开始或 2s 超时时结算,状态和 endedAt 取 ending。失败/取消立即结算。session/unknown 的 `modelCalls` 为 null(快照数不是调用数)。 +- 0.11.2 的 cache-write、Codex 内部子代理 usage、OMP branch epoch 缺口仍在。OMP 统计已包含部分子代理,不能无条件再叠加。徽标使用 Session token snapshot,不以 unknown 代替已知快照;未知 scope 不求 token 总数。历史 Timeline 缺少 scope 元数据时只声明元数据缺失,不声明账本排除。更新后重载客户端以使用新字段。 ## 截图 diff --git a/client/panel.tsx b/client/panel.tsx index d6ad903..b02b38d 100644 --- a/client/panel.tsx +++ b/client/panel.tsx @@ -84,7 +84,7 @@ function InFlightCard({ inFlight, seq, theme }: { inFlight: InFlight; seq: numbe {tokens} {pct ? ` (${pct} cached)` : ' (cache rate unknown)'} - {inFlight.modelCalls > 0 ? ` · ${inFlight.modelCalls} observation${inFlight.modelCalls > 1 ? 's' : ''}` : ''} + {inFlight.modelCalls != null && inFlight.modelCalls > 0 ? ` · ${inFlight.modelCalls} observation${inFlight.modelCalls > 1 ? 's' : ''}` : ''} ) : ( diff --git a/docs/issue-omp-turn-usage-settlement.md b/docs/issue-omp-turn-usage-settlement.md new file mode 100644 index 0000000..76399f5 --- /dev/null +++ b/docs/issue-omp-turn-usage-settlement.md @@ -0,0 +1,126 @@ +# Issue:OMP 轮次结算丢失/滞后 usage 快照(研究与修复交接说明) + +状态:已修复(见 §8)· 发现日期 2026-10-16 · 发现方式:审计 agent `2b21c659-14fa-454c-b4b6-00fd33e9c4b8`(Paseo 0.11.2,provider `omp`,model `owtoken/mimo-v2.6-pro`)的 token ledger 记录,与 omp 原生 session 日志逐调用比对。 + +## 1. 现象 + +OMP 轮次的 ledger 记录经常**赶不上最后一个模型调用的 usage**,表现为两种形态: + +1. **记录停在上一个快照**:`source=last_observation`,数值只覆盖到本轮倒数第二次调用(甚至更早),轮次末尾的调用被静默丢掉。 +2. **整轮空记录**:`source=none, quality=unavailable, modelCalls=0`,token/cost 全为 `null`,但该轮实际发生过带 usage 的模型调用。 + +## 2. 实证(审计样本,账本 vs 原生 session 真值) + +真值来源:`C:\Users\PC\.omp\agent\sessions\--D--projects-motion-lab--\2026-10-16T05-57-12-258Z_01a14349-2fc2-74e4-983e-e57369a51f4f.jsonl`, +其中 assistant 消息逐调用携带 `usage = { input, output, cacheRead, cacheWrite, totalTokens, cost }`(`cacheRead` = cached input)。 +账本来源:`~/.paseo/plugins/token-ledger/ledger.jsonl`。 + +### 样本 A — 轮次 `83010c14`(记录 id `d5eaf749-8896-4964-97e6-46713e9718c2`) + +账本记录:`05:57:12.377 → 05:57:49.215, source=last_observation, quality=partial, modelCalls=2, +input=2078, cached=9984, output=484, sessionCostUsd=0.00943752` + +真值(本轮 2 次模型调用): + +| 调用时间 | input | cacheRead | output | cost | +|---|---|---|---|---| +| 05:57:28.080 | 2078 | 9984 | 484 | 0.00943752 | +| 05:57:49.198 | 673 | 12032 | 619 | 0.00609396 | + +记录数值(2078/9984/484,cost 0.00943752)**恰好等于第 1 次调用后的累计快照**,第 2 次调用完全未反映。 +第 2 次调用的 usage 时间戳(05:57:49.198)距轮次结算(05:57:49.215)只有 **17ms**。 + +### 样本 B — 轮次 `afd69f4c`(记录 id `67f1e246-aa14-427d-b5e6-1506528edda2`) + +账本记录:`05:58:30.208 → 05:58:45.631, source=none, quality=unavailable, modelCalls=0`,全部计数为 `null`。 + +真值:本轮含 1 次模型调用,05:58:45.622,usage `input=455, cacheRead=12672, output=307`。 +调用结束距轮次结算仅 **9ms**。本轮账本为零,但调用真实存在。 + +### 交叉验证(说明快照本身是准的) + +该 session 前 10 次调用逐调用求和 = `input 30131 / cached 165632 / output 4091`, +与 agent 元数据 `C:\Users\PC\.paseo\agents\D-projects-motion-lab\2b21c659-....json` 的 `lastUsage` 逐字段相等。 +即 OMP 的 `lastUsage` 是**精确的 session 累计值**;问题不在 usage 数据本身,而在**结算时机**。 + +## 3. 机制分析(对应代码) + +数据流(`server/turns.ts`、`shared/aggregate.ts`): + +- 观测(observation)唯一来源是 agent 快照的 `lastUsage`,由 `noteUsage()` 收集进 `open.observations`; + 与 `state.lastObservation` 做 `sameTokens` 去重——**重复的旧快照不入列**。 +- 轮次终止事件到达时 `closeTurn()` **立即**用已收集的 observations 调 `finalizeTurn()` 落库; + `finalUsage` 取自终止事件本身,而 OMP 的终止事件不带 usage → 降级链落到 + `observations.at(-1)`(`last_observation`)或空(`none`)。 +- `closeTurn` 之后到达的快照被有意丢弃(`noteUsage` 注释 "Cumulative/unknown late observations remain visible as snapshots only", + 另有 latest-snapshot card 展示)。**没有结算后的补齐/升级机制。** + +结论:**timeline 事件流与 usage 快照流是两条异步通道,顺序无保证**(仓库 AGENTS.md 已记录同类问题), +而轮次结算不等待 usage 快照的"最终排空"(final drain)。两个样本的 9ms/17ms 时间差正是这个竞争窗口。 + +附带问题:`modelCalls: observations.length`(`aggregate.ts`)对 OMP 而言数的是**快照数,不是模型调用数**。 +样本 A `modelCalls=2` 只是巧合(快照数恰好=调用数,但数值只覆盖 1 次调用);样本 B `modelCalls=0` 而真值是 1 次调用。 + +## 4. 影响面(ledger.jsonl,截至 2026-10-16,共 2005 条) + +- provider `omp`:`last_observation` 146 条(全部 modelCalls≥1,quality 全为 partial), + `none` 23 条(其中 status=completed 18 条)。**OMP 没有任何一条 exact 级别记录。** +- 其他 provider 的 `completed+none`(可能另有原因,不在本 issue 范围):deepseek-harness 33、claude-tokenrouter 3、kimi-tokenrouter 2。 +- 注意设计现状:`providerSemantics('omp')` = `{ tokens: 'session', cost: 'unknown' }`, + OMP 的 session 累计值本来就**不计入轮次合计/日期区间合计**(`tokenScope: 'session'`,见 README/OVERVIEW)。 + 所以账面金额没有因此错算;损失的是**轮次级归因**(panel 的 Turn history 行显示不全或显示空)。 + +## 5. 研究问题(修复 agent 需要回答) + +1. Paseo 0.11.2 的公开插件 API(见 `docs/paseo-usage-api.md`)现在能否提供 OMP 的 + per-turn / per-request usage 或终止事件附带 usage?若能,直接走 `turn_usage` 路径。 +2. 若不能:结算是否应该做 **final drain**——终止事件到达后短暂等待(或等到 + `activeTurn` 消失且快照时间戳晚于最后一次调用)再 `closeTurn`? + 延迟落库对 checkpoint/恢复逻辑(`tracker-state.json`、`lastClosedTurnId` 防重放)有什么影响? +3. 或者**两段式结算**:先落库,末快照到达后升级记录。与既有约束"磁盘原始值不重写"冲突吗? + (旧账本里存在 `turnId:endedAt` 后缀的修订 id 形态,可评估该模式。) +4. `modelCalls` 在 session/unknown scope 下的语义要不要修正(快照数≠调用数),避免误导。 +5. 快照是精确累计值(§2 已验证),同一 session/branch epoch 内首尾差值即可还原轮次增量。 + 但 `semantics.ts` 注释警告:OMP 分支视图总量可回退、无 branch epoch、无 final-drain 顺序保证。 + **在拿到 epoch 证据之前不要对累计值做差分归因**——这是设计红线,先解决观测到的竞争,再评估差分。 + +## 6. 建议验收标准 + +- 复现:构造/捕捉"终止事件先于末次 usage 快照到达"的时序(可用现有样本 A/B 的事件序列做回归用例), + 修复前记录缺末次调用或整轮为空,修复后记录覆盖到末次调用(或显式 `usageGap` 标记,不得静默丢数)。 +- `modelCalls` 不再把快照数当调用数虚报(或改名/留空)。 +- 不重复计数:相邻轮次不得都吸收同一累计快照;OMP 数值仍不进入轮次合计(scope 语义不变)。 +- 既有测试全绿(`shared/aggregate.test.ts`、`server/turns.test.ts`、`server/tracker.test.ts`、`server/recovery.test.ts`); + 新增确定性单测覆盖该竞争时序。 +- 尊重仓库 AGENTS.md 既有约定:turnId 非全局唯一、终止事件后快照可能回放 activeTurn、时间线不回填、未知不估算。 + +## 7. 验证用真值提取方法 + +逐调用 usage(真值): + +```python +import json +p = r"C:\Users\PC\.omp\agent\sessions\\_.jsonl" +for line in open(p, encoding="utf-8"): + o = json.loads(line) + if o.get("type") == "message" and o["message"].get("role") == "assistant" and o["message"].get("usage"): + u = o["message"]["usage"] + print(o["timestamp"], u["input"], u["cacheRead"], u["cacheWrite"], u["output"], u["cost"]["total"]) +``` + +- session 累计快照(对照):`~/.paseo/agents//.json` → `lastUsage`。 +- 账本:`~/.paseo/plugins/token-ledger/ledger.jsonl`,按 `agentId` 过滤。 +- 已知可复算不变量:同一 session 内逐调用 usage 求和 == 末次 `lastUsage`(样本 session 已验证)。 + +## 8. 修复记录(2026-10-16) + +§5 结论: +1. 不能。0.11.2 协议没有 `usage_updated` 推给插件;OMP 的 `turn_completed` 不带 usage(`agent/providers/omp/agent.js` `completeTurn`)。 +2. 采用 final drain。`OmpUsagePoller.completeTurn()` 异步读 stats 后才发 `usage_updated`,此时 `activeTurnId` 仍指向刚结束的轮;usage 未变化时不发。 + 因此 session scope 的 completed 轮记 `open.ending`,在以下任一条件下结算:第一个新观察快照、下一轮开始、2s 超时、插件停止/agent 退役。 + `ending` 随 journal 持久化;结算的 status/endedAt 取 ending,不被超时或中断改写。`lastClosedTurnId` 防重放不变。 +3. 不采用两段式结算。记录 id 为稳定 UUID,磁盘记录不重写。 +4. `modelCalls` 在 session/unknown scope 下为 null(新记录写 null,旧记录读时归一为 null)。 +5. 未做累计差分。OMP 记录仍是 session 快照,不计入轮次合计,`costUsd` 仍为 null。 + +实测:OMP agent `16838577…` 一轮 2 次调用,最后调用 07:32:09.242,记录 endedAt 07:32:09.252,`input=38860/output=194` 与原生日志逐调用求和相等。 diff --git a/server/pricing.ts b/server/pricing.ts index 1dd084a..c933f1f 100644 --- a/server/pricing.ts +++ b/server/pricing.ts @@ -245,6 +245,8 @@ export function enrichTurn(record: TurnRecord, seq: number): TurnRow { : estimated.costBreakdown; return { ...record, input, seq, tokenScope: scope, + // Historical session/unknown rows counted snapshots, not model calls. + modelCalls: scope === 'turn' ? record.modelCalls : null, promptTokens: promptTokens(record.provider, record), incompleteUsage: scope !== 'turn' || record.quality !== 'exact' || !!record.usageGap || input === null || record.cached === null || record.cacheWrite == null || record.output === null, diff --git a/server/tracker.ts b/server/tracker.ts index 83012f4..f14624b 100644 --- a/server/tracker.ts +++ b/server/tracker.ts @@ -1,4 +1,4 @@ -import { closeTurn, createState, markUsageGap, reconcileUsage, noteUsage, startLifecycleTurn, streamTurn, type AgentState, type AgentSnapshotLike } from "./turns.ts"; +import { closeTurn, createState, markUsageGap, reconcileUsage, noteUsage, startLifecycleTurn, streamTurn, endLifecycleTurn, type AgentState, type AgentSnapshotLike } from "./turns.ts"; import { loadJournal, saveJournal } from "./journal.ts"; import type { PluginLifecycleEvents } from "@getpaseo/plugin/server"; import { subscribeAgents } from "../shared/agents.ts"; @@ -76,7 +76,28 @@ function stateFor(agentId: string): AgentState { return state; } export function observeStream(payload: PaseoAgentTimelineEvent): void { - if (!stopped) persist(streamTurn(stateFor(payload.agentId), payload)); + if (stopped) return; + const state = stateFor(payload.agentId); + persist(streamTurn(state, payload)); + if (state.open?.ending) scheduleSettle(payload.agentId, state); +} +// OMP publishes its final usage only when it changed, so a missing drain +// snapshot is normal. The bound covers its async stats read, not its 3s poll. +const SESSION_DRAIN_MS = 2000; +const STREAM_SETTLE_MS = 250; +function scheduleSettle(agentId: string, state: AgentState): void { + const open = state.open; + if (!open?.ending) return; + const key = open.key; + if (terminalTimers.has(key)) return; + const session = tokenScope({ provider: open.provider ?? state.provider }) === 'session'; + // The stream carries terminal usage; the hook independently carries outcome. + // Give the usage a brief chance to settle, then finalize observed facts only. + const settle = () => { + terminalTimers.delete(key); + if (state.open?.key === key) persist(closeTurn(agentId, state, state.open.ending?.status ?? 'unknown', null, new Date().toISOString())); + }; + terminalTimers.set(key, { timer: setTimeout(settle, session ? SESSION_DRAIN_MS : STREAM_SETTLE_MS), settle }); } export async function observeStart(event: PluginLifecycleEvents["agent.turn_started"], paseo: PaseoApi): Promise { await ensureTracker(paseo); @@ -90,17 +111,9 @@ export async function observeEnd(event: PluginLifecycleEvents["agent.turn_ended" await ensureTracker(paseo); if (stopped) return; const state = stateFor(event.agent.id); - if (!state.open || state.open.turnId !== event.turnId) return; - const key = state.open.key; - const endedAt = new Date().toISOString(); - clearTimeout(terminalTimers.get(key)?.timer); - // The stream carries terminal usage; the hook independently carries outcome. - // Give the stream a brief chance to settle, then finalize observed facts only. - const settle = () => { - terminalTimers.delete(key); - if (state.open?.key === key) persist(closeTurn(event.agent.id, state, event.outcome.kind, null, endedAt)); - }; - terminalTimers.set(key, { timer: setTimeout(settle, 250), settle }); + if (!endLifecycleTurn(state, event.turnId, event.outcome.kind, new Date().toISOString())) return; + checkpoint(); + scheduleSettle(event.agent.id, state); } function retireAgent(id: string, gap = false): void { needsReconcile.delete(id); diff --git a/server/turns.test.ts b/server/turns.test.ts index 4769f1c..16a34ed 100644 --- a/server/turns.test.ts +++ b/server/turns.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import type { PaseoAgentStream } from '@getpaseo/client'; -import { AgentStateSchema, createState, markUsageGap, reconcileUsage, noteUsage, startLifecycleTurn, streamTurn } from './turns.ts'; +import { AgentStateSchema, closeTurn, createState, endLifecycleTurn, markUsageGap, reconcileUsage, noteUsage, startLifecycleTurn, streamTurn } from './turns.ts'; const start = '2026-09-14T00:00:00.000Z'; const end = '2026-09-14T00:00:01.000Z'; const usage = { inputTokens: 100, cachedInputTokens: 40, outputTokens: 10 }; @@ -168,19 +168,26 @@ test('a Codex delivery gap does not mark every subsequent uninterrupted turn as assert.equal(next.usageGap, undefined); }); -test('late OMP usage refreshes the persisted snapshot without reopening or billing a closed turn', () => { +test('late OMP usage settles a deferred turn once, then only refreshes the snapshot', () => { const state = createState(); const activeTurn = { turnId: 't1', startedAt: start }; noteUsage(state, { id: 'a', provider: 'omp', model: 'gpt-model', activeTurn, runtimeInfo: { sessionId: 's' }, lastUsage: { inputTokens: 1000, cachedInputTokens: 4000, outputTokens: 100 } }); - const record = streamTurn(state, event({ type: 'turn_completed', provider: 'omp', turnId: 't1' }))!; + assert.equal(streamTurn(state, event({ type: 'turn_completed', provider: 'omp', turnId: 't1' })), null); + // OMP publishes the drain while its activeTurnId still names the ending turn. + const record = noteUsage(state, { id: 'a', provider: 'omp', activeTurn, + lastUsage: { inputTokens: 1300, cachedInputTokens: 4500, outputTokens: 200 } })!; assert.equal(record.tokenScope, 'session'); - noteUsage(state, { id: 'a', provider: 'omp', activeTurn, - lastUsage: { inputTokens: 1300, cachedInputTokens: 4500, outputTokens: 200 } }); + assert.equal(record.input, 1300); + assert.equal(record.status, 'completed'); + assert.equal(record.endedAt, end); assert.equal(state.open, null); + noteUsage(state, { id: 'a', provider: 'omp', activeTurn, + lastUsage: { inputTokens: 1400, cachedInputTokens: 4600, outputTokens: 210 } }); + assert.equal(state.open, null, 'a replayed activeTurn does not reopen the settled turn'); const restored = AgentStateSchema.parse(JSON.parse(JSON.stringify(state))); - assert.equal(restored.lastObservation?.input, 1300); - assert.equal(restored.lastObservation?.output, 200); + assert.equal(restored.lastObservation?.input, 1400); + assert.equal(restored.lastObservation?.output, 210); // A lower branch-view snapshot is not a fresh billing counter or a turn delta. noteUsage(restored, { id: 'a', provider: 'omp', activeTurn: null, lastUsage: { inputTokens: 100, cachedInputTokens: 0, outputTokens: 10 } }); @@ -188,3 +195,84 @@ test('late OMP usage refreshes the persisted snapshot without reopening or billi assert.equal(restored.open, null); assert.equal(record.costUsd, null); }); + +const omp = (type: 'turn_started' | 'turn_completed', turnId: string, timestamp = type === 'turn_started' ? start : end) => + event({ type, provider: 'omp', turnId }, timestamp); + +// Issue sample A (turn 83010c14): the second call's usage arrived 17ms after the terminal event. +test('OMP sample A: a final snapshot after the terminal event is not dropped', () => { + const state = createState(); + const activeTurn = { turnId: 't1', startedAt: start }; + streamTurn(state, omp('turn_started', 't1')); + noteUsage(state, { id: 'a', provider: 'omp', activeTurn, + lastUsage: { inputTokens: 2078, cachedInputTokens: 9984, outputTokens: 484, totalCostUsd: 0.00943752 } }); + assert.equal(streamTurn(state, omp('turn_completed', 't1')), null); + const record = noteUsage(state, { id: 'a', provider: 'omp', activeTurn, + lastUsage: { inputTokens: 2751, cachedInputTokens: 22016, outputTokens: 1103, totalCostUsd: 0.01553148 } })!; + assert.equal(record.input, 2751); assert.equal(record.cached, 22016); assert.equal(record.output, 1103); + assert.equal(record.sessionCostUsd, 0.01553148); + assert.equal(record.costUsd, null, 'OMP session cost is not differenced into turns'); + assert.equal(record.modelCalls, null, 'a snapshot count is not a model-call count'); +}); + +// Issue sample B (turn afd69f4c): the only call's usage arrived 9ms after the terminal event. +test('OMP sample B: a turn whose only snapshot drains late is not empty', () => { + const state = createState(); + noteUsage(state, { id: 'a', provider: 'omp', lastUsage: { inputTokens: 2751, cachedInputTokens: 22016, outputTokens: 1103 } }); + streamTurn(state, omp('turn_started', 't2')); + assert.equal(streamTurn(state, omp('turn_completed', 't2')), null); + const record = noteUsage(state, { id: 'a', provider: 'omp', activeTurn: { turnId: 't2', startedAt: start }, + lastUsage: { inputTokens: 3206, cachedInputTokens: 34688, outputTokens: 1410 } })!; + assert.equal(record.input, 3206); + assert.equal(record.source, 'last_observation'); + assert.equal(record.modelCalls, null); +}); + +test('OMP drain does not absorb a snapshot twice, and a new turn settles an unchanged drain', () => { + const state = createState(); + const s1 = { inputTokens: 100, cachedInputTokens: 0, outputTokens: 10 }; + streamTurn(state, omp('turn_started', 't1')); + streamTurn(state, omp('turn_completed', 't1')); + assert.equal(noteUsage(state, { id: 'a', provider: 'omp', activeTurn: { turnId: 't1', startedAt: start }, lastUsage: s1 })!.input, 100); + streamTurn(state, omp('turn_started', 't2', end)); + // A replay of the absorbed snapshot is not new usage for the next turn. + assert.equal(noteUsage(state, { id: 'a', provider: 'omp', activeTurn: { turnId: 't2', startedAt: end }, lastUsage: s1 }), null); + assert.equal(state.open?.observations.length, 0); + // Unchanged usage publishes no drain; the next turn start settles the ending one. + streamTurn(state, omp('turn_completed', 't2', '2026-09-14T00:00:02.000Z')); + const second = streamTurn(state, omp('turn_started', 't3', '2026-09-14T00:00:03.000Z'))!; + assert.equal(second.turnId, 't2'); + assert.equal(second.status, 'completed', 'a known outcome is not rewritten as canceled'); + assert.equal(second.endedAt, '2026-09-14T00:00:02.000Z'); + assert.equal(second.source, 'none'); + assert.equal(state.open?.turnId, 't3'); +}); + +test('OMP failures settle at once; Codex still counts its requests', () => { + const state = createState(); + streamTurn(state, omp('turn_started', 't1')); + assert.ok(streamTurn(state, event({ type: 'turn_failed', provider: 'omp', turnId: 't1', error: 'x' }))); + const codex = createState(); streamTurn(codex, begin()); + noteUsage(codex, { id: 'a', lastUsage: usage }); + assert.equal(streamTurn(codex, finish())!.modelCalls, 1); +}); + +test('a deferred outcome survives a checkpoint and keeps its reported status and time', () => { + const state = createState(); + streamTurn(state, omp('turn_started', 't1')); + streamTurn(state, omp('turn_completed', 't1')); + const restored = AgentStateSchema.parse(JSON.parse(JSON.stringify(state))); + assert.equal(restored.open?.ending?.status, 'completed'); + const record = closeTurn('a', restored, 'canceled', null, '2026-09-14T00:00:09.000Z')!; + assert.equal(record.status, 'completed'); assert.equal(record.endedAt, end); +}); + +test('the stream outcome overrides an earlier hook outcome', () => { + const state = createState(); + streamTurn(state, omp('turn_started', 't1')); + assert.equal(endLifecycleTurn(state, 'other', 'failed', end), false); + assert.equal(endLifecycleTurn(state, 't1', 'unknown', '2026-09-14T00:00:00.500Z'), true); + streamTurn(state, omp('turn_completed', 't1')); + const record = noteUsage(state, { id: 'a', provider: 'omp', activeTurn: { turnId: 't1', startedAt: start }, lastUsage: usage })!; + assert.equal(record.status, 'completed'); assert.equal(record.endedAt, end); +}); diff --git a/server/turns.ts b/server/turns.ts index ac39784..4f74231 100644 --- a/server/turns.ts +++ b/server/turns.ts @@ -12,6 +12,9 @@ export const AgentStateSchema = z.object({ provider: z.string().nullable(), model: z.string().nullable(), sessionId: z.string().nullable(), open: z.object({ key: z.string(), turnId: z.string().nullable(), startedAt: z.string(), usageGap: z.boolean().optional(), + /** Known outcome whose settlement waits for usage that drains after the terminal event. */ + ending: z.object({ status: z.enum(['completed', 'failed', 'canceled', 'unknown']), endedAt: z.string(), + from: z.enum(['stream', 'hook']) }).optional(), provider: z.string().nullable(), model: z.string().nullable(), sessionId: z.string().nullable(), observations: z.array(observationSchema), lastUsage: contextSchema.nullable(), }).nullable(), @@ -44,6 +47,8 @@ function openTurn(state: AgentState, turnId: string | null, startedAt: string) { export function closeTurn(agentId: string, state: AgentState, status: TurnRecord["status"], finalUsage: UsageLike | null, endedAt: string): TurnRecord | null { const open = state.open; if (!open) return null; + // Interruption or a drain timeout cannot overrule an outcome already reported. + if (open.ending) { status = open.ending.status; endedAt = open.ending.endedAt; } state.open = null; state.usageGapPending = false; state.lastClosedTurnId = open.turnId; @@ -86,7 +91,7 @@ function sessionChanged(agentId: string, state: AgentState, sessionId: string | return interrupted; } export function noteUsage(state: AgentState, snapshot: AgentSnapshotLike): TurnRecord | null { - const interrupted = sessionChanged(snapshot.id, state, snapshot.runtimeInfo?.sessionId); + let interrupted = sessionChanged(snapshot.id, state, snapshot.runtimeInfo?.sessionId); if (snapshot.provider) state.provider = snapshot.provider; if (snapshot.model !== undefined) state.model = snapshot.model; // Fill metadata that wasn't available at start; don't reattribute an old turn @@ -96,7 +101,11 @@ export function noteUsage(state: AgentState, snapshot: AgentSnapshotLike): TurnR state.open.model ??= state.model; } const active = snapshot.activeTurn; - if (active && state.open && active.turnId !== state.open.turnId) return interrupted; + if (active && state.open && active.turnId !== state.open.turnId) { + // A newer turn proves that an ending turn has finished draining. + if (!state.open.ending) return interrupted; + interrupted = closeTurn(snapshot.id, state, 'unknown', null, new Date().toISOString()); + } const closedReplay = active && active.turnId === state.lastClosedTurnId && active.turnId !== state.open?.turnId && (!active.startedAt || !state.lastClosedAt || Date.parse(active.startedAt) <= Date.parse(state.lastClosedAt)); if (closedReplay && tokenScope(state) === 'turn') return interrupted; @@ -136,6 +145,11 @@ export function noteUsage(state: AgentState, snapshot: AgentSnapshotLike): TurnR open.observations[last] = mergeObservation(open.observations[last], observation); } } + // The first changed session snapshot after the outcome is the final drain: + // the harness publishes it only once, and only when usage changed. + if (open.ending && isNew && tokenScope({ provider: open.provider ?? state.provider }) === 'session') { + return closeTurn(snapshot.id, state, open.ending.status, null, open.ending.endedAt) ?? interrupted; + } return interrupted; } @@ -161,6 +175,12 @@ export function reconcileUsage(state: AgentState, snapshot: AgentSnapshotLike): if (interrupted) records.push(interrupted); return records; } +/** Records a hook outcome; the stream outcome, when present, remains authoritative. */ +export function endLifecycleTurn(state: AgentState, turnId: string | null, status: TurnRecord['status'], at: string): boolean { + if (!state.open || state.open.turnId !== turnId) return false; + state.open.ending ??= { status, endedAt: at, from: 'hook' }; + return true; +} export function startLifecycleTurn(agentId: string, state: AgentState, turnId: string | null, at: string): TurnRecord | null { // A delayed hook must not resurrect the turn already completed on the stream. if (state.open?.turnId === turnId || state.lastClosedTurnId === turnId) return null; @@ -191,11 +211,18 @@ export function streamTurn(state: AgentState, payload: PaseoAgentTimelineEvent): } case "turn_completed": case "turn_failed": - case "turn_canceled": + case "turn_canceled": { if (event.turnId && state.open?.turnId && event.turnId !== state.open.turnId) return null; - return closeTurn(payload.agentId, state, - event.type === "turn_completed" ? "completed" : event.type === "turn_failed" ? "failed" : "canceled", - event.type === "turn_completed" ? event.usage ?? null : null, at); + if (!state.open) return null; + const status = event.type === "turn_completed" ? "completed" : event.type === "turn_failed" ? "failed" : "canceled"; + const usage = event.type === "turn_completed" ? event.usage ?? null : null; + if (state.open.ending?.from !== 'stream') state.open.ending = { status, endedAt: at, from: 'stream' }; + // OMP reports no terminal usage and publishes its final session snapshot + // only after turn_completed, on the separate agent_update channel. + if (status === 'completed' && !tokenObservation(usage) + && tokenScope({ provider: state.open.provider ?? state.provider }) === 'session') return null; + return closeTurn(payload.agentId, state, status, usage, at); + } default: return null; } } diff --git a/shared/aggregate.ts b/shared/aggregate.ts index c0ed30e..c5ef840 100644 --- a/shared/aggregate.ts +++ b/shared/aggregate.ts @@ -125,6 +125,7 @@ export function finalizeTurn(args: FinalizeInput): TurnRecord { const durationMs = args.startedAt !== null ? Math.max(0, Date.parse(args.endedAt) - Date.parse(args.startedAt)) : null; + const scope = tokenScope(args); return { v: 1, // endedAt is part of the id because Paseo turnIds ("foreground-turn-N") @@ -133,7 +134,7 @@ export function finalizeTurn(args: FinalizeInput): TurnRecord { agentId: args.agentId, turnId: args.turnId, provider: args.provider, - tokenScope: tokenScope(args), + tokenScope: scope, model: args.model, startedAt: args.startedAt, endedAt: args.endedAt, @@ -147,7 +148,8 @@ export function finalizeTurn(args: FinalizeInput): TurnRecord { sessionCostUsd: rawCost, costScope: semantics.cost, ...(semantics.tokens === "request" ? { requests: observations.map(({ cost, ...tokens }) => tokens) } : {}), - modelCalls: observations.length, + // For session/unknown scope, snapshots may not correspond 1:1 with model calls. + modelCalls: scope === 'turn' ? observations.length : null, quality, source, }; diff --git a/shared/ledger.ts b/shared/ledger.ts index 7a9dcfd..16ec693 100644 --- a/shared/ledger.ts +++ b/shared/ledger.ts @@ -40,8 +40,11 @@ export const TurnRecordSchema = z.object({ input: z.number().nullable(), cached: z.number().nullable(), output: z.number().nullable(), cacheWrite: z.number().finite().nonnegative().nullable().optional(), })).optional(), - /** Number of distinct token-bearing usage observations during the turn. */ - modelCalls: z.number(), + /** + * Number of distinct token-bearing usage observations during the turn; null for + * session/unknown scope where snapshots do not map 1:1 to model calls. + */ + modelCalls: z.number().nullable(), quality: z.enum(["exact", "partial", "unavailable"]), /** How the token totals were derived: turn_usage | summed_observations | last_observation | none */ source: z.string(), @@ -79,7 +82,7 @@ export const InFlightSchema = z.object({ usageGap: z.boolean().optional(), turnId: z.string().nullable(), startedAt: z.string(), - modelCalls: z.number(), + modelCalls: z.number().nullable(), tokenScope: z.enum(['turn', 'session', 'unknown']), promptTokens: z.number().nullable(), partialCost: z.boolean(), From 6cfaadd3e29280a3b4e40692caed1cc5a0f61146 Mon Sep 17 00:00:00 2001 From: stv1024 Date: Fri, 16 Oct 2026 15:46:49 +0800 Subject: [PATCH 3/6] docs: record OMP drain workaround design rationale This workaround can be removed once Paseo provides turn_completed.usage for OMP or a definitive final-drain signal. The notes document what to delete and when it's safe to refactor. --- .../omp-drain-workaround-design-notes.md" | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 "docs/\351\232\217\347\254\224/omp-drain-workaround-design-notes.md" diff --git "a/docs/\351\232\217\347\254\224/omp-drain-workaround-design-notes.md" "b/docs/\351\232\217\347\254\224/omp-drain-workaround-design-notes.md" new file mode 100644 index 0000000..f1100f3 --- /dev/null +++ "b/docs/\351\232\217\347\254\224/omp-drain-workaround-design-notes.md" @@ -0,0 +1,79 @@ +# OMP drain workaround 设计权衡记录 + +日期:2026-10-16 +背景:修复 issue-omp-turn-usage-settlement.md 中记录的"OMP 轮次结算丢失 usage"问题 +提交:0ac6d72 + +## 问题根源 + +Paseo 0.11.2 的两个异步通道无法对齐: + +1. **timeline** 的 `turn_completed` 不带 usage(OMP 特有) +2. **agent_update** 的 `lastUsage` 快照在终止事件之后才异步到达(9–17ms 实测) +3. 快照里 `activeTurn` 仍然指向刚结束的轮(所以不能用"变 null"当 drain 信号) +4. usage 没变化时 OMP 不发最后一个快照(所以必须有超时后备) + +## 当前方案(0ac6d72) + +session scope 的 completed 轮先记 `open.ending`(持久化进 journal),延迟结算,在以下任一条件触发: + +1. 收到第一个新的 usage 观察(`noteUsage`) +2. 下一轮开始(`streamTurn` 的 turn_started) +3. 2s 超时(`scheduleSettle`) +4. 插件停止或 agent 归档(`stopTracker` / `retireAgent`) + +结算时的 status/endedAt 用 `ending` 记录的值,不被超时改写。 + +## 为什么不优雅 + +1. **状态机多了一层**:turn 生命周期从"open → closed"变成"open → ending → closed"(仅 session scope)。journal 要持久化 ending,恢复时要处理。 +2. **结算触发点分散**:四个触发条件分散在三个函数里,改 turn 生命周期时要检查所有分支。 +3. **超时是猜的**:2s 没有依据(只是"比 3s 轮询周期短,比异步 stats 读慢"),OMP 改行为或网络慢时会失效。 +4. **两套语义共存**:Codex/Claude 立即结算,OMP 延迟结算,`tokenScope` 判断散布在多处。 + +## 考虑过但放弃的方案 + +| 方案 | 为什么不行 | +|---|---| +| 立即用已有观察结算 | 旧代码,丢最后一次调用或整轮为空 | +| 等 activeTurn 消失 | 源码证实最后快照发出时 activeTurn 还在,信号来得比 drain 早 | +| 只在下一轮开始时结算上一轮 | 最后一轮可能永远不结算,面板显示"进行中"会很怪 | +| 两段式结算,后续升级记录 | 与"磁盘原始值不重写"约束冲突,Timeline 也已发过 | +| Codex/Claude 也延迟,统一逻辑 | 它们的终止事件已带 usage,延迟纯属浪费,所有轮次晚 250ms 显示 | +| 标注"OMP 不保证准确"放弃修 | 对用户不诚实,plugin 已发布不能降级承诺 | + +## 长远重构方向(如果上游修了) + +**最理想**:Paseo 让 OMP 的 `turn_completed` 带上 usage(或新增 `turn_usage_finalized` 事件)。 + +届时可以**删除**: +- `open.ending` 字段和相关 schema +- `endLifecycleTurn` 函数 +- `scheduleSettle` 和 `SESSION_DRAIN_MS` +- `noteUsage` / `streamTurn` 里的 `if (open.ending && ...)` 分支 +- `observeEnd` 的 `scheduleSettle` 调用 + +**保留**: +- `tokenScope` 判断(这是语义区分,不是 workaround) +- journal 持久化逻辑(ending 字段可废弃但保留兼容读,不影响) +- `lastClosedTurnId` 防重放(这是所有 provider 都需要的) + +重构后 OMP 回到和 Claude 一样的立即结算路径,代码复杂度回到 2026-10-15 以前的水平。 + +## 判断何时可以重构 + +监控信号: +1. Paseo release notes 提到 OMP turn_completed 带 usage 或相关修复 +2. `@getpaseo/protocol` 的 `AgentStreamEventPayloadSchema` 里 `turn_completed` 的 `usage` 字段从 optional 变 required(对 OMP) +3. 实测 OMP 轮次的 `finalizeTurn` 里 `finalUsage` 不再为 null + +届时翻出这份文档,按"长远重构方向"清单逐项删除 workaround 代码,跑测试,确认 OMP 实测记录的 source 从 `last_observation` 变成 `turn_usage`,即可完成清理。 + +## 当前方案的验收证据 + +- typecheck 通过,72/72 测试通过 +- 新增 6 个确定性测试覆盖竞争时序(samples A/B、无重复计数、ending 恢复、stream 优先) +- 实测 OMP agent `16838577…`,2 次调用,最后调用 07:32:09.242,记录 endedAt 07:32:09.252,token 总和与原生日志一致 +- `modelCalls=null`(不虚报快照数) + +这个方案在**既有约束下**(磁盘不重写、turnId 非全局唯一、timeline 不回填、未知不估算)是能做到的最务实的。 From a78098389408fbff91f4968603fc156f6cabf59c Mon Sep 17 00:00:00 2001 From: stv1024 Date: Fri, 23 Oct 2026 11:00:25 +0800 Subject: [PATCH 4/6] docs: add UI design principle to AGENTS.md Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 8115c8c..b72dc95 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,10 @@ TokenLedger 最低支持 Paseo v0.11.0,不设版本上限(id `token-ledger`,SDK 开发依赖固定 0.11.1;已验证宿主 0.11.1)。0.9.1–0.10.x 用户留在 v0.6.3。允许加载不代表所有未来版本均已实测;兼容策略与验证记录见 `docs/compatibility.md`。**若存在 `AGENTS.local.md`(gitignore,本机专用),先读它——本机路径、工具链怪癖、测试 provider、账号事项都在那里,其内容优先于本文件。** +## UI 设计理念 + +Avoid unnecessary secondary labels and descriptions. Let the layout do the explaining. + ## 开发循环 ```bash From 4b0d275458d39df83998413a694c41fae08234bb Mon Sep 17 00:00:00 2001 From: stv1024 Date: Fri, 23 Oct 2026 11:00:26 +0800 Subject: [PATCH 5/6] fix: track agents in Paseo 0.12 background workspaces Paseo 0.12 hides background workspaces from default agent and workspace lists and sends a catalog remove for agents that the filter hides. The tracker retired those agents, so their turns were recorded as canceled with no usage. Request background workspaces in every catalog list, falling back to an unfiltered list on hosts that predate them, so the minimum stays 0.11.0. Confirm each remove by agent ID before settling the open turn. Add 0.12.0 to the CI matrix and record the 0.12 review. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 +- AGENTS.md | 9 ++++- CHANGELOG.md | 1 + docs/compatibility.md | 33 +++++++++++++++- server/background.test.ts | 79 +++++++++++++++++++++++++++++++++++++++ server/catalog.ts | 10 ++++- server/tracker.ts | 33 +++++++++++----- shared/agents.test.ts | 46 +++++++++++++++++++++-- shared/agents.ts | 45 +++++++++++++--------- shared/background.ts | 21 +++++++++++ 10 files changed, 243 insertions(+), 36 deletions(-) create mode 100644 server/background.test.ts create mode 100644 shared/background.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4081265..006a296 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - paseo: ['0.11.0', '0.11.1', 'latest'] + paseo: ['0.11.0', '0.11.1', '0.12.0', 'latest'] name: Paseo SDK ${{ matrix.paseo }} steps: - uses: actions/checkout@v4 diff --git a/AGENTS.md b/AGENTS.md index b72dc95..30e60ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md — 开发调试速查 -TokenLedger 最低支持 Paseo v0.11.0,不设版本上限(id `token-ledger`,SDK 开发依赖固定 0.11.1;已验证宿主 0.11.1)。0.9.1–0.10.x 用户留在 v0.6.3。允许加载不代表所有未来版本均已实测;兼容策略与验证记录见 `docs/compatibility.md`。**若存在 `AGENTS.local.md`(gitignore,本机专用),先读它——本机路径、工具链怪癖、测试 provider、账号事项都在那里,其内容优先于本文件。** +TokenLedger 最低支持 Paseo v0.11.0,不设版本上限(id `token-ledger`,SDK 开发依赖固定 0.11.1;已验证宿主 0.11.1、0.12.0)。0.9.1–0.10.x 用户留在 v0.6.3。允许加载不代表所有未来版本均已实测;兼容策略与验证记录见 `docs/compatibility.md`。**若存在 `AGENTS.local.md`(gitignore,本机专用),先读它——本机路径、工具链怪癖、测试 provider、账号事项都在那里,其内容优先于本文件。** ## UI 设计理念 @@ -75,6 +75,13 @@ paseo agent archive # 测完归档,别 - OMP 轮末 drain(0.11.2 源码验证):`turn_completed` 不带 usage,先于 poller 的最终 `usage_updated` 发出;最终快照仍带 activeTurn,且 usage 未变化时不发。所以 session scope 的 completed 轮先记 `open.ending`(随 journal 恢复),在第一个新观察、下一轮开始或 2s 超时时结算,状态和 endedAt 取 ending。失败/取消立即结算。session/unknown 的 `modelCalls` 为 null(快照数不是调用数)。 - 0.11.2 的 cache-write、Codex 内部子代理 usage、OMP branch epoch 缺口仍在。OMP 统计已包含部分子代理,不能无条件再叠加。徽标使用 Session token snapshot,不以 unknown 代替已知快照;未知 scope 不求 token 总数。历史 Timeline 缺少 scope 元数据时只声明元数据缺失,不声明账本排除。更新后重载客户端以使用新字段。 +## 0.12 适配约束 + +- **background workspace 默认被过滤**:0.12 的 `agents.list` / `workspaces.list` 及其 update 流不带 `filter.includeBackground` 时排除 background workspace,被过滤的 agent 以 `remove` 推送。MCP 新建 workspace 会继承调用方的 background,子 agent 常在其中。所有目录 list 都经 `shared/background.ts` 的 `includingBackground`;同一订阅的翻页必须沿用 open 时的 filter。0.12+ 客户端 SDK 连旧 daemon 会抛 "Update the host to use background workspaces.",回退为无 filter,不抬高最低版本。 +- **`remove` 不等于删除**:tracker 收到 update 流的 `remove` 后先 `agents.ref(id).refresh()` 确认(按 ID 不受 background 过滤),agent 仍在则保留 open turn,确认消失才 retire。 +- CLI 的 `paseo agent archive ` 找不到 background agent;测试后用 `paseo workspace archive` 和 `paseo project delete` 清理。 +- 0.12 编译器允许 type-only import `@getpaseo/protocol/*` 子路径,但最低版本仍是 0.11,上面的 import 禁令不变。0.13 将弃用 `/client/react-native`(改用 `/client/ui`),届时再迁移。 + ## 截图 发布用截图不能出现真实 agent 标题、workspace 或项目名。用独立 demo daemon 和合成数据: diff --git a/CHANGELOG.md b/CHANGELOG.md index 94037ef..2e84865 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Track agents in Paseo 0.12 background workspaces. Paseo 0.12 hides them from default agent and workspace lists, so their turns were closed as canceled with no usage and their workspace names were missing. Catalog lists now request background workspaces and fall back to unfiltered lists on hosts that predate them. A catalog removal is confirmed by agent ID before the agent's open turn is settled. - Add an overview screenshot (demo data, with the sidebar footer row) to the README and as the first manifest media image. - Fix harness input semantics: OMP exclusive input is no longer reduced again by a GPT-model fallback; unverified providers are never inferred from model names. - Separate per-turn usage from OMP current-session/branch totals and unknown-scope snapshots, including historical v1 records. Snapshots remain visible but are excluded from turn/date-range sums and estimates. Late idle usage updates refresh a separate latest-snapshot card, not an invented turn delta. diff --git a/docs/compatibility.md b/docs/compatibility.md index 837d42f..25bcd2d 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -28,7 +28,7 @@ the release for Paseo 0.9.1-0.10.x hosts. any other type import breaks installation. Updating the host does not require updating these pins in lockstep. - **Ongoing checks:** CI runs the typecheck and existing tests against 0.11.0, - 0.11.1, and npm `latest`, on pushes, PRs, manual runs, and weekly. These checks + 0.11.1, 0.12.0, and npm `latest`, on pushes, PRs, manual runs, and weekly. These checks detect type and SDK regressions; real provider turns remain necessary to verify token/cost semantics and host behavior. CI failures do not silently rewrite the compatibility range or disable installed plugins. @@ -46,6 +46,7 @@ exclude beta builds. | 0.10.2 | Installed desktop/daemon, published SDK, typecheck, 65 tests, plugin reload, real Claude/Codex turns on 2026-09-30 | Compatible with v0.6.3 | | 0.11.0 | Published SDK (plugin dist identical to 0.11.1), typecheck and 65 tests on 2026-10-09 | Minimum for v0.7.0; no installed host tested | | 0.11.1 | Installed desktop/daemon, published SDK, typecheck, 65 tests, dependency-free compile probe, plugin reload, real Claude/Codex turns, manual sidebar and overview check on 2026-10-09 | Supported by v0.7.0 | +| 0.12.0 | Installed desktop/daemon, published SDK, typecheck and 75 tests against both the 0.11.1 pin and 0.12.0, plugin reload, real Claude turns in a background workspace (before/after control) on 2026-10-23 | Requires the background-workspace fix below; otherwise compatible | | 0.11.2 | Unreleased accounting fixes: pinned-SDK typecheck, 66 tests, installed-plugin reload, replay of existing Claude/OMP records, real host Web UI overview/pill/panel and live OMP snapshots on 2026-10-14 | Harness scopes and incomplete coverage verified; missing upstream data remains unavailable | The 0.11.1 investigation found: @@ -104,3 +105,33 @@ An isolated 0.11.2 daemon with synthetic data verified the overview warning, the `Session ≈4.12M tok` pill and its snapshot panel, and supplied the refreshed screenshots. No model prompt was sent. The production plugin was reloaded for local trial only; no package version was published. + +The 0.12.0 review compared the v0.11.2 and v0.12.0 sources: + +- Paseo 0.12 adds background workspaces. `agents.list`, `workspaces.list` + and their `agent_update` / `workspace_update` streams exclude them unless the + request sets `filter.includeBackground`, and an agent that is hidden by the + filter arrives as `remove`. TokenLedger previously retired such agents: one + real turn in a background workspace was recorded as a 3 ms `canceled` turn + with no usage. TokenLedger now requests background workspaces, confirms each + `remove` with `agents.ref(id).refresh()`, and keeps the open turn when the + agent still exists. The same turn sequence then recorded `completed` turns. +- A 0.12+ client SDK rejects `includeBackground` with "Update the host to use + background workspaces." when the host does not advertise the feature (a 0.12 + app on a 0.11 daemon). TokenLedger retries those lists without the filter, so + the minimum remains 0.11.0. Older daemons drop the unknown filter field. +- The plugin SDK client API, lifecycle hooks, timeline subscription and + `AgentUsage` are unchanged. Claude still omits cache creation and reports a + session-cumulative `totalCostUsd`; Codex still reports per-request `last` + usage with cached input included; OMP still emits `turn_completed` before + the final usage drain. The proxy context-meter fixes change only + `contextWindowUsedTokens`. +- The compiler now resolves type-only `@getpaseo/protocol/*` subpath imports. + TokenLedger does not rely on this while it supports 0.11.0. +- Agent-to-agent messages now steer a running turn instead of interrupting it, + so one record can cover several prompts. Plugin ACP providers without + `prompt.steer` still replace the turn. +- 0.13.0-beta.1 deprecates `/client/react-native` in favor of `/client/ui` + (alias kept until 2027-04-20), empties the deprecated `timeline` field of the + `turn_ended` hook (TokenLedger does not read it), and applies the background + filter to stored agents as well. diff --git a/server/background.test.ts b/server/background.test.ts new file mode 100644 index 0000000..d182a05 --- /dev/null +++ b/server/background.test.ts @@ -0,0 +1,79 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import type { PaseoApi, PaseoAgentTimelineEvent, PaseoAgentUpdate } from '@getpaseo/client'; +import { ownedSnapshot } from '../shared/subscription-test-helpers.ts'; + +test('a catalog remove for an agent that still exists keeps its turn open; a confirmed removal settles it', async () => { + const home = await mkdtemp(join(tmpdir(), 'token-ledger-background-')); + const previousHome = process.env.PASEO_HOME; + process.env.PASEO_HOME = home; + const dir = join(home, 'plugins', 'token-ledger'); + await mkdir(dir, { recursive: true }); + await writeFile(join(dir, 'pricing.json'), JSON.stringify({version: 1, currency: 'USD', prices: []})); + await writeFile(join(dir, 'openrouter-pricing.json'), JSON.stringify({fetchedAt: new Date().toISOString(), prices: []})); + const running = (id: string, turnId: string) => ({ id, provider: 'codex', model: 'gpt-5', workspaceId: 'w', status: 'running', + runtimeInfo: { sessionId: 's' }, activeTurn: { turnId, startedAt: '2026-10-23T00:00:00Z' }, + lastUsage: { inputTokens: 100, cachedInputTokens: 50, outputTokens: 10 } }); + // The background agent still resolves by ID; the deleted one does not. + const current = new Map([['bg', running('bg', 't1')], ['gone', running('gone', 't2')]]); + const timelines = new Map void>(); + const listFilters: unknown[] = []; + let update: (event: PaseoAgentUpdate) => void = () => {}; + const paseo = { agents: { + subscribe: () => () => {}, + list: async (options: { filter?: unknown; subscribe?: object }) => { + listFilters.push(options.filter); + const owned = ownedSnapshot({ entries: [...current.values()].map((agent) => ({ agent })), pageInfo: { nextCursor: null } }); + update = (event) => owned.update({ type: 'agent_update', payload: event }); + return owned.page; + }, + ref: (id: string) => ({ + refresh: async () => current.get(id) ?? null, + current: () => current.get(id) ?? null, + timeline: { + subscribe: (fn: (event: PaseoAgentTimelineEvent) => void) => { + timelines.set(id, fn); + return Object.assign(() => {}, { ready: Promise.resolve(), release: async () => {} }); + }, + append: async () => ({ seq: 1, epoch: 'test' }), + }, + }), + }, workspaces: { list: async (options: { filter?: unknown }) => { + listFilters.push(options.filter); + return ownedSnapshot({ entries: [], pageInfo: { nextCursor: null } }).page; + } } } as unknown as PaseoApi; + const tracker = await import('./tracker.ts'); + const store = await import('./store.ts'); + const records = () => store.allRecords().map((record) => [record.agentId, record.status]); + const waitForRecords = async (count: number) => { + for (let i = 0; i < 200 && records().length < count; i++) await new Promise((resolve) => setTimeout(resolve, 10)); + }; + try { + await tracker.ensureTracker(paseo); + for (const [id, turnId] of [['bg', 't1'], ['gone', 't2']]) { + timelines.get(id)!({ agentId: id, timestamp: '2026-10-23T00:00:00Z', event: { type: 'turn_started', provider: 'codex', turnId } }); + } + current.delete('gone'); + update({ kind: 'remove', agentId: 'bg' }); + update({ kind: 'remove', agentId: 'gone' }); + await waitForRecords(1); + // Let the background agent's confirmation finish too. + await new Promise((resolve) => setTimeout(resolve, 20)); + assert.deepEqual(records(), [['gone', 'canceled']]); + timelines.get('bg')!({ agentId: 'bg', timestamp: '2026-10-23T00:00:02Z', event: { type: 'turn_completed', provider: 'codex', turnId: 't1', + usage: { inputTokens: 120, cachedInputTokens: 60, outputTokens: 20 } } }); + // Terminal usage settles after the tracker's short stream window. + await waitForRecords(2); + assert.deepEqual(records(), [['gone', 'canceled'], ['bg', 'completed']]); + await tracker.handleOverview({}, { paseo }); + assert.ok(listFilters.length >= 2); + for (const filter of listFilters) assert.deepEqual(filter, { includeBackground: true }); + } finally { + await tracker.stopTracker(); + if (previousHome === undefined) delete process.env.PASEO_HOME; else process.env.PASEO_HOME = previousHome; + await rm(home, { recursive: true, force: true }); + } +}); diff --git a/server/catalog.ts b/server/catalog.ts index f40a96e..42627b3 100644 --- a/server/catalog.ts +++ b/server/catalog.ts @@ -1,4 +1,5 @@ import type { PaseoAgent, PaseoApi, PaseoWorkspace, PaseoWorkspaceListResult, PaseoWorkspaceUpdate } from './paseo-types.ts'; +import { includingBackground } from '../shared/background.ts'; import { subscribeCatalog, type CatalogSubscription } from '../shared/catalog-subscription.ts'; type Metadata = Pick; @@ -18,9 +19,14 @@ export class Catalog { ensureWorkspaces(paseo: PaseoApi): Promise { if (this.disposed) return Promise.resolve(); + let filter: {} = {}; this.subscription ??= subscribeCatalog({ - open: () => paseo.workspaces.list({ subscribe: {}, page: { limit: 200 } }), - next: (cursor) => paseo.workspaces.list({ page: { limit: 200, cursor } }), + open: () => includingBackground(async (option) => { + const page = await paseo.workspaces.list({ subscribe: {}, filter: option, page: { limit: 200 } }); + filter = option; + return page; + }), + next: (cursor) => paseo.workspaces.list({ filter, page: { limit: 200, cursor } }), entries: (page) => page.entries, id: (workspace) => workspace.id, update: (value) => { diff --git a/server/tracker.ts b/server/tracker.ts index f14624b..a89d669 100644 --- a/server/tracker.ts +++ b/server/tracker.ts @@ -182,17 +182,19 @@ export function ensureTracker(paseo: PaseoApi): Promise { if (open && !agents.get(event.agentId)?.open) needsReconcile.delete(event.agentId); }, gap: (id) => { needsReconcile.add(id); markUsageGap(stateFor(id)); checkpoint(); }, - restored: async (id) => { - const revision = agentRevisions.get(id); - const handle = paseo.agents.ref(id); - await handle.refresh(); - if (stopped || agentRevisions.get(id) !== revision) return; - const snapshot = handle.current(); - if (!snapshot) { retireAgent(id, true); return; } - applyAgent(snapshot, true); - }, + restored: (id) => reconcileAgent(id, true), error: (id, error) => console.error('token-ledger: timeline subscription failed', id, error), }); + // Re-read one agent by ID; a ref resolves agents that lists may hide. + const reconcileAgent = async (id: string, restored: boolean) => { + const revision = agentRevisions.get(id); + const handle = paseo.agents.ref(id); + await handle.refresh(); + if (stopped || agentRevisions.get(id) !== revision) return; + const snapshot = handle.current(); + if (!snapshot) { catalog.remove(id); retireAgent(id, restored); return; } + applyAgent(snapshot, restored); + }; const applyAgent = (agent: PaseoAgent, restored = false) => { restored = needsReconcile.delete(agent.id) || restored; bump(agent.id); @@ -213,7 +215,18 @@ export function ensureTracker(paseo: PaseoApi): Promise { onUpdate: (update) => { if (stopped) return; if (update.kind === 'upsert') applyAgent(update.agent); - else { bump(update.agentId); catalog.remove(update.agentId); retireAgent(update.agentId); } + else { + // A remove can mean the agent left this list's filter (such as a + // background workspace), not that it was archived or deleted. + const id = update.agentId; + bump(id); + const revision = agentRevisions.get(id); + void reconcileAgent(id, false).catch((error) => { + console.error('token-ledger: could not confirm agent removal', id, error); + if (stopped || agentRevisions.get(id) !== revision) return; + catalog.remove(id); retireAgent(id); + }); + } }, onError: (error) => { for (const [id, state] of agents) { needsReconcile.add(id); markUsageGap(state); } diff --git a/shared/agents.test.ts b/shared/agents.test.ts index fce3bbb..9b29e0e 100644 --- a/shared/agents.test.ts +++ b/shared/agents.test.ts @@ -13,8 +13,8 @@ test('one-off catalog reads traverse all pages without creating subscriptions', } } } as unknown as PaseoApi; assert.deepEqual((await listAgents(paseo)).map(a => a.id), ['a', 'b']); assert.deepEqual(calls, [ - { page: { limit: 200 } }, - { page: { limit: 200, cursor: 'next' } }, + { filter: { includeBackground: true }, page: { limit: 200 } }, + { filter: { includeBackground: true }, page: { limit: 200, cursor: 'next' } }, ]); }); @@ -34,7 +34,7 @@ test('real 0.9 SDK accepts host-assigned catalog IDs, forwards restored pages an }); try { await subscription.ready; - assert.deepEqual(calls, [{ subscribe: {}, page: { limit: 200 } }]); + assert.deepEqual(calls, [{ subscribe: {}, filter: { includeBackground: true }, page: { limit: 200 } }]); assert.deepEqual(snapshots, [['a', 'b']]); owned.snapshot({ entries: [{ agent: { id: 'new' } }], pageInfo: { nextCursor: 'next' } }); await new Promise((resolve) => setImmediate(resolve)); @@ -46,3 +46,43 @@ test('repeated pagination cursor fails instead of looping forever', async () => const paseo = { agents: { list: async () => ({ entries: [], pageInfo: { nextCursor: 'same' } }) } } as unknown as PaseoApi; await assert.rejects(listAgents(paseo), /Repeated/); }); + +test('hosts without background workspaces get unfiltered lists on every page', async () => { + const calls: unknown[] = []; + const first = { entries: [{ agent: { id: 'a' } }], pageInfo: { nextCursor: 'next' } }; + const owned = ownedSnapshot(first); + // A 0.12+ client SDK rejects the filter before sending it to an older host. + const paseo = { agents: { list: async (options: { filter?: { includeBackground?: boolean }; subscribe?: {}; page: { cursor?: string } }) => { + calls.push(options); + if (options.filter?.includeBackground) throw new Error('Update the host to use background workspaces.'); + if (options.page.cursor) return { entries: [{ agent: { id: 'b' } }], pageInfo: { nextCursor: null } }; + return options.subscribe ? owned.page : first; + } } } as unknown as PaseoApi; + assert.deepEqual((await listAgents(paseo)).map((a) => a.id), ['a', 'b']); + assert.deepEqual(calls.splice(0), [ + { filter: { includeBackground: true }, page: { limit: 200 } }, + { filter: {}, page: { limit: 200 } }, + { filter: {}, page: { limit: 200, cursor: 'next' } }, + ]); + const snapshots: string[][] = []; + const subscription = subscribeAgents(paseo, { + onSnapshot: (agents) => snapshots.push(agents.map((agent) => agent.id)), + onUpdate: () => {}, onError: (error) => { throw error; }, + }); + try { + await subscription.ready; + assert.deepEqual(snapshots, [['a', 'b']]); + assert.deepEqual(calls, [ + { subscribe: {}, filter: { includeBackground: true }, page: { limit: 200 } }, + { subscribe: {}, filter: {}, page: { limit: 200 } }, + { filter: {}, page: { limit: 200, cursor: 'next' } }, + ]); + } finally { await subscription.release(); } +}); + +test('other list failures are not retried without the filter', async () => { + let calls = 0; + const paseo = { agents: { list: async () => { calls++; throw new Error('offline'); } } } as unknown as PaseoApi; + await assert.rejects(listAgents(paseo), /offline/); + assert.equal(calls, 1); +}); diff --git a/shared/agents.ts b/shared/agents.ts index 7ff259d..799682b 100644 --- a/shared/agents.ts +++ b/shared/agents.ts @@ -1,3 +1,4 @@ +import { includingBackground } from './background.ts'; import { subscribeCatalog, type CatalogSubscription, type OwnedSubscription } from './catalog-subscription.ts'; // Shared code cannot import host client or server SDK types. Callers pass the @@ -6,26 +7,28 @@ type AgentPage = { entries: readonly { agent: A }[]; pageInfo: { nextCursor?: export type AgentUpdate = { kind: 'upsert'; agent: A } | { kind: 'remove'; agentId: string }; export interface AgentCatalogApi { readonly agents: { - list(options: { subscribe: {}; page: { limit: number } }): Promise & { subscription: OwnedSubscription> }>; - list(options: { page: { limit: number; cursor?: string } }): Promise>; + list(options: { subscribe: {}; filter: {}; page: { limit: number } }): Promise & { subscription: OwnedSubscription> }>; + list(options: { filter: {}; page: { limit: number; cursor?: string } }): Promise>; }; } /** A list page is not the complete catalog. Subscribe only on the first page. */ -export async function listAgents(paseo: AgentCatalogApi): Promise { - const agents = new Map(); - let cursor: string | undefined; - const seen = new Set(); - do { - const page = await paseo.agents.list({ - page: { limit: 200, ...(cursor ? { cursor } : {}) }, - }); - for (const { agent } of page.entries) agents.set(agent.id, agent); - cursor = page.pageInfo.nextCursor ?? undefined; - if (cursor && seen.has(cursor)) throw new Error("Repeated agent pagination cursor"); - if (cursor) seen.add(cursor); - } while (cursor); - return [...agents.values()]; +export function listAgents(paseo: AgentCatalogApi): Promise { + return includingBackground(async (filter) => { + const agents = new Map(); + let cursor: string | undefined; + const seen = new Set(); + do { + const page = await paseo.agents.list({ + filter, page: { limit: 200, ...(cursor ? { cursor } : {}) }, + }); + for (const { agent } of page.entries) agents.set(agent.id, agent); + cursor = page.pageInfo.nextCursor ?? undefined; + if (cursor && seen.has(cursor)) throw new Error("Repeated agent pagination cursor"); + if (cursor) seen.add(cursor); + } while (cursor); + return [...agents.values()]; + }); } export function subscribeAgents(paseo: AgentCatalogApi, handlers: { @@ -33,9 +36,15 @@ export function subscribeAgents(paseo: AgentCatalogApi onUpdate(update: AgentUpdate): void; onError(error: unknown): void; }): CatalogSubscription { + // Pages must use the same filter as the subscription that opened them. + let filter: {} = {}; return subscribeCatalog({ - open: () => paseo.agents.list({ subscribe: {}, page: { limit: 200 } }), - next: (cursor) => paseo.agents.list({ page: { limit: 200, cursor } }), + open: () => includingBackground(async (option) => { + const page = await paseo.agents.list({ subscribe: {}, filter: option, page: { limit: 200 } }); + filter = option; + return page; + }), + next: (cursor) => paseo.agents.list({ filter, page: { limit: 200, cursor } }), entries: (page) => page.entries.map(({ agent }) => agent), id: (agent) => agent.id, update: (value) => { diff --git a/shared/background.ts b/shared/background.ts new file mode 100644 index 0000000..a2d1556 --- /dev/null +++ b/shared/background.ts @@ -0,0 +1,21 @@ +// Paseo 0.12 hides agents in background workspaces from agents.list, +// workspaces.list and their agent_update streams unless the request opts in. +// Typed as {} so SDK pins that predate the field still accept it; older hosts +// have no background workspaces and drop the unknown field. +const INCLUDE_BACKGROUND: {} = { includeBackground: true }; + +// A 0.12+ client SDK rejects the filter before sending it to a host that does +// not advertise background workspaces. +function isUnsupported(error: unknown): boolean { + return error instanceof Error && /use background workspaces/i.test(error.message); +} + +/** Lists with background workspaces included, or unfiltered on hosts that lack them. */ +export async function includingBackground(call: (filter: {}) => Promise): Promise { + try { + return await call(INCLUDE_BACKGROUND); + } catch (error) { + if (!isUnsupported(error)) throw error; + return call({}); + } +} From 4142cb4400317b7f202be265d95765a213e80069 Mon Sep 17 00:00:00 2001 From: stv1024 Date: Fri, 23 Oct 2026 12:01:07 +0800 Subject: [PATCH 6/6] Release v0.8.0: Paseo 0.12 background workspaces and accounting accuracy Remove internal research notes with local data from the repository and keep them under the gitignored docs/private/. Add a privacy rule to AGENTS.md: no private gateway information or data in published content, and no feature may depend on a private gateway. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 2 + AGENTS.md | 6 + CHANGELOG.md | 4 +- README.md | 8 +- docs/issue-omp-turn-usage-settlement.md | 126 ------------------ docs/releases/v0.8.0.md | 42 ++++++ .../omp-drain-workaround-design-notes.md" | 79 ----------- package-lock.json | 4 +- package.json | 2 +- 9 files changed, 60 insertions(+), 213 deletions(-) delete mode 100644 docs/issue-omp-turn-usage-settlement.md create mode 100644 docs/releases/v0.8.0.md delete mode 100644 "docs/\351\232\217\347\254\224/omp-drain-workaround-design-notes.md" diff --git a/.gitignore b/.gitignore index 0ee1ac4..3b7d684 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ DEVELOPMENT_PLAN.md AGENTS.local.md # local Paseo agent settings (per-user) paseo.json +# private research notes (local paths, provider names, ledger data) +docs/private/ diff --git a/AGENTS.md b/AGENTS.md index 61fba7f..65e364a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,12 @@ TokenLedger 最低支持 Paseo v0.11.0,不设版本上限(id `token-ledger` Avoid unnecessary secondary labels and descriptions. Let the layout do the explaining. +## 隐私红线 + +- **禁止发布 Stephen 私人网关的任何信息或数据**:网关名称、provider/model 名(含本机 provider 配置名)、价格表、汇率、额度、账号、端点,以及真实账本数据、agent/会话 ID、本机路径。范围包括源码、测试、文档、截图、commit message、PR/issue/Release 文本和 npm 包。 +- **新功能不能依赖私人网关**:只针对公开 provider/harness 的已验证语义设计;不内置私有价格种子或固定汇率。测试和文档用合成数据(如 `acme-web`、`openai/gpt-5`)。 +- 私人网关只用于本机实测,相关事项写在 `AGENTS.local.md`;含真实数据的调研笔记放 `docs/private/`(已 gitignore)。提交前 `git grep -i` 检查网关名和本机路径,发版前用 `npm pack --dry-run` 核对包内容。 + ## 开发循环 ```bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 79d348b..5942a7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog -## Unreleased +## v0.8.0 (2026-10-23) — Paseo 0.12 background workspaces and accounting accuracy + +Requires Paseo **0.11.0 or newer**; verified on 0.11.1 and 0.12.0. Existing ledger records are reinterpreted on read, not rewritten; existing custom price files are kept. Reload or reopen the app after updating. See the [release notes](docs/releases/v0.8.0.md). - Switch overview ranges inside the screen instead of pushing a navigation entry per click; the screen title is now fixed. Thanks to @ChtJT (#4, #5). - Track agents in Paseo 0.12 background workspaces. Paseo 0.12 hides them from default agent and workspace lists, so their turns were closed as canceled with no usage and their workspace names were missing. Catalog lists now request background workspaces and fall back to unfiltered lists on hosts that predate them. A catalog removal is confirmed by agent ID before the agent's open turn is settled. diff --git a/README.md b/README.md index f6a2878..00027a2 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Per-turn LLM token usage and cost for [Paseo](https://paseo.sh) agents — an ag **Sidebar** — a **TokenLedger** row in the sidebar footer shows today's cost (or tokens when no price is known), with an activity icon while a turn is running. "Today" starts at local midnight on the device that shows it. -**Overview** (sidebar footer → TokenLedger, or the command center) — all sessions in one place: grand totals, then per-agent rows grouped by workspace with live-turn indicator, last activity, cost/tokens, and turn count. Pick **Today**, **Last 7 days**, **Last 30 days**, or **All time**. The sidebar row opens it on Today; the command center opens All time. A range lists agents with turns that ended in it, plus agents with a turn in flight. Tapping a row jumps to that agent. +**Overview** (sidebar footer → TokenLedger, or the command center) — all sessions in one place: grand totals, then per-agent rows grouped by workspace with live-turn indicator, last activity, cost/tokens, and turn count. Pick **Today**, **Last 7 days**, **Last 30 days**, or **All time**. The sidebar row opens it on Today; the command center opens All time. Switching ranges stays on the same screen. A range lists agents with turns that ended in it, plus agents with a turn in flight. Tapping a row jumps to that agent.

TokenLedger Today overview with synthetic data, excluded snapshots and a custom-price migration warning @@ -42,11 +42,11 @@ Per-turn LLM token usage and cost for [Paseo](https://paseo.sh) agents — an ag ## Compatibility -TokenLedger **v0.7.0** requires **Paseo 0.11.0 or newer**, on both the daemon and the app, without an upper version limit. It uses the screen and sidebar footer APIs that Paseo 0.11 added. The verified host is **0.11.1**; allowing newer versions is not a claim that every future release has been tested. SDK development dependencies are pinned to 0.11.1, with CI checks against the minimum, the pin, and the latest published SDK. See the [compatibility policy and verification notes](docs/compatibility.md). +TokenLedger **v0.8.0** requires **Paseo 0.11.0 or newer**, on both the daemon and the app, without an upper version limit. It uses the screen and sidebar footer APIs that Paseo 0.11 added. The verified hosts are **0.11.1** and **0.12.0**, including agents in Paseo 0.12 background workspaces; allowing newer versions is not a claim that every future release has been tested. SDK development dependencies are pinned to 0.11.1, with CI checks against the minimum, the pin, 0.12.0, and the latest published SDK. See the [compatibility policy and verification notes](docs/compatibility.md). Paseo 0.9.1–0.10.x users should stay on `v0.6.3`; Paseo 0.8 users on `v0.5.1`; Paseo 0.7 users on `v0.3.1`. Existing `ledger.jsonl` records remain readable; no migration or deletion is required. See the [v0.6.0 release notes](docs/releases/v0.6.0.md) for the 0.9.1 migration details. -The unreleased accounting fixes were also exercised on Paseo 0.11.2. After updating, reload/reopen the client so it uses the new nullable summary fields, scope metadata and pricing warnings. Existing JSONL records are reinterpreted on read, not rewritten. The screenshots above use synthetic data on an isolated host. +The v0.8.0 accounting fixes were also exercised on Paseo 0.11.2. After updating, reload/reopen the client so it uses the new nullable summary fields, scope metadata and pricing warnings. Existing JSONL records are reinterpreted on read, not rewritten. The screenshots above use synthetic data on an isolated host. ## Install @@ -67,7 +67,7 @@ Or paste either source into **Settings → Plugins → Plugin source**. To install a tagged release from GitHub instead: ```bash -paseo plugin add github:stv1024/token-ledger --ref v0.7.0 +paseo plugin add github:stv1024/token-ledger --ref v0.8.0 ``` Since Paseo 0.11, a bare `owner/name` source means the official plugin registry, which rejects `--ref`. Keep the `github:` prefix for Git installs. On Paseo 0.9.1–0.10.x, use `paseo plugin add stv1024/token-ledger --ref v0.6.3`. diff --git a/docs/issue-omp-turn-usage-settlement.md b/docs/issue-omp-turn-usage-settlement.md deleted file mode 100644 index 76399f5..0000000 --- a/docs/issue-omp-turn-usage-settlement.md +++ /dev/null @@ -1,126 +0,0 @@ -# Issue:OMP 轮次结算丢失/滞后 usage 快照(研究与修复交接说明) - -状态:已修复(见 §8)· 发现日期 2026-10-16 · 发现方式:审计 agent `2b21c659-14fa-454c-b4b6-00fd33e9c4b8`(Paseo 0.11.2,provider `omp`,model `owtoken/mimo-v2.6-pro`)的 token ledger 记录,与 omp 原生 session 日志逐调用比对。 - -## 1. 现象 - -OMP 轮次的 ledger 记录经常**赶不上最后一个模型调用的 usage**,表现为两种形态: - -1. **记录停在上一个快照**:`source=last_observation`,数值只覆盖到本轮倒数第二次调用(甚至更早),轮次末尾的调用被静默丢掉。 -2. **整轮空记录**:`source=none, quality=unavailable, modelCalls=0`,token/cost 全为 `null`,但该轮实际发生过带 usage 的模型调用。 - -## 2. 实证(审计样本,账本 vs 原生 session 真值) - -真值来源:`C:\Users\PC\.omp\agent\sessions\--D--projects-motion-lab--\2026-10-16T05-57-12-258Z_01a14349-2fc2-74e4-983e-e57369a51f4f.jsonl`, -其中 assistant 消息逐调用携带 `usage = { input, output, cacheRead, cacheWrite, totalTokens, cost }`(`cacheRead` = cached input)。 -账本来源:`~/.paseo/plugins/token-ledger/ledger.jsonl`。 - -### 样本 A — 轮次 `83010c14`(记录 id `d5eaf749-8896-4964-97e6-46713e9718c2`) - -账本记录:`05:57:12.377 → 05:57:49.215, source=last_observation, quality=partial, modelCalls=2, -input=2078, cached=9984, output=484, sessionCostUsd=0.00943752` - -真值(本轮 2 次模型调用): - -| 调用时间 | input | cacheRead | output | cost | -|---|---|---|---|---| -| 05:57:28.080 | 2078 | 9984 | 484 | 0.00943752 | -| 05:57:49.198 | 673 | 12032 | 619 | 0.00609396 | - -记录数值(2078/9984/484,cost 0.00943752)**恰好等于第 1 次调用后的累计快照**,第 2 次调用完全未反映。 -第 2 次调用的 usage 时间戳(05:57:49.198)距轮次结算(05:57:49.215)只有 **17ms**。 - -### 样本 B — 轮次 `afd69f4c`(记录 id `67f1e246-aa14-427d-b5e6-1506528edda2`) - -账本记录:`05:58:30.208 → 05:58:45.631, source=none, quality=unavailable, modelCalls=0`,全部计数为 `null`。 - -真值:本轮含 1 次模型调用,05:58:45.622,usage `input=455, cacheRead=12672, output=307`。 -调用结束距轮次结算仅 **9ms**。本轮账本为零,但调用真实存在。 - -### 交叉验证(说明快照本身是准的) - -该 session 前 10 次调用逐调用求和 = `input 30131 / cached 165632 / output 4091`, -与 agent 元数据 `C:\Users\PC\.paseo\agents\D-projects-motion-lab\2b21c659-....json` 的 `lastUsage` 逐字段相等。 -即 OMP 的 `lastUsage` 是**精确的 session 累计值**;问题不在 usage 数据本身,而在**结算时机**。 - -## 3. 机制分析(对应代码) - -数据流(`server/turns.ts`、`shared/aggregate.ts`): - -- 观测(observation)唯一来源是 agent 快照的 `lastUsage`,由 `noteUsage()` 收集进 `open.observations`; - 与 `state.lastObservation` 做 `sameTokens` 去重——**重复的旧快照不入列**。 -- 轮次终止事件到达时 `closeTurn()` **立即**用已收集的 observations 调 `finalizeTurn()` 落库; - `finalUsage` 取自终止事件本身,而 OMP 的终止事件不带 usage → 降级链落到 - `observations.at(-1)`(`last_observation`)或空(`none`)。 -- `closeTurn` 之后到达的快照被有意丢弃(`noteUsage` 注释 "Cumulative/unknown late observations remain visible as snapshots only", - 另有 latest-snapshot card 展示)。**没有结算后的补齐/升级机制。** - -结论:**timeline 事件流与 usage 快照流是两条异步通道,顺序无保证**(仓库 AGENTS.md 已记录同类问题), -而轮次结算不等待 usage 快照的"最终排空"(final drain)。两个样本的 9ms/17ms 时间差正是这个竞争窗口。 - -附带问题:`modelCalls: observations.length`(`aggregate.ts`)对 OMP 而言数的是**快照数,不是模型调用数**。 -样本 A `modelCalls=2` 只是巧合(快照数恰好=调用数,但数值只覆盖 1 次调用);样本 B `modelCalls=0` 而真值是 1 次调用。 - -## 4. 影响面(ledger.jsonl,截至 2026-10-16,共 2005 条) - -- provider `omp`:`last_observation` 146 条(全部 modelCalls≥1,quality 全为 partial), - `none` 23 条(其中 status=completed 18 条)。**OMP 没有任何一条 exact 级别记录。** -- 其他 provider 的 `completed+none`(可能另有原因,不在本 issue 范围):deepseek-harness 33、claude-tokenrouter 3、kimi-tokenrouter 2。 -- 注意设计现状:`providerSemantics('omp')` = `{ tokens: 'session', cost: 'unknown' }`, - OMP 的 session 累计值本来就**不计入轮次合计/日期区间合计**(`tokenScope: 'session'`,见 README/OVERVIEW)。 - 所以账面金额没有因此错算;损失的是**轮次级归因**(panel 的 Turn history 行显示不全或显示空)。 - -## 5. 研究问题(修复 agent 需要回答) - -1. Paseo 0.11.2 的公开插件 API(见 `docs/paseo-usage-api.md`)现在能否提供 OMP 的 - per-turn / per-request usage 或终止事件附带 usage?若能,直接走 `turn_usage` 路径。 -2. 若不能:结算是否应该做 **final drain**——终止事件到达后短暂等待(或等到 - `activeTurn` 消失且快照时间戳晚于最后一次调用)再 `closeTurn`? - 延迟落库对 checkpoint/恢复逻辑(`tracker-state.json`、`lastClosedTurnId` 防重放)有什么影响? -3. 或者**两段式结算**:先落库,末快照到达后升级记录。与既有约束"磁盘原始值不重写"冲突吗? - (旧账本里存在 `turnId:endedAt` 后缀的修订 id 形态,可评估该模式。) -4. `modelCalls` 在 session/unknown scope 下的语义要不要修正(快照数≠调用数),避免误导。 -5. 快照是精确累计值(§2 已验证),同一 session/branch epoch 内首尾差值即可还原轮次增量。 - 但 `semantics.ts` 注释警告:OMP 分支视图总量可回退、无 branch epoch、无 final-drain 顺序保证。 - **在拿到 epoch 证据之前不要对累计值做差分归因**——这是设计红线,先解决观测到的竞争,再评估差分。 - -## 6. 建议验收标准 - -- 复现:构造/捕捉"终止事件先于末次 usage 快照到达"的时序(可用现有样本 A/B 的事件序列做回归用例), - 修复前记录缺末次调用或整轮为空,修复后记录覆盖到末次调用(或显式 `usageGap` 标记,不得静默丢数)。 -- `modelCalls` 不再把快照数当调用数虚报(或改名/留空)。 -- 不重复计数:相邻轮次不得都吸收同一累计快照;OMP 数值仍不进入轮次合计(scope 语义不变)。 -- 既有测试全绿(`shared/aggregate.test.ts`、`server/turns.test.ts`、`server/tracker.test.ts`、`server/recovery.test.ts`); - 新增确定性单测覆盖该竞争时序。 -- 尊重仓库 AGENTS.md 既有约定:turnId 非全局唯一、终止事件后快照可能回放 activeTurn、时间线不回填、未知不估算。 - -## 7. 验证用真值提取方法 - -逐调用 usage(真值): - -```python -import json -p = r"C:\Users\PC\.omp\agent\sessions\\_.jsonl" -for line in open(p, encoding="utf-8"): - o = json.loads(line) - if o.get("type") == "message" and o["message"].get("role") == "assistant" and o["message"].get("usage"): - u = o["message"]["usage"] - print(o["timestamp"], u["input"], u["cacheRead"], u["cacheWrite"], u["output"], u["cost"]["total"]) -``` - -- session 累计快照(对照):`~/.paseo/agents//.json` → `lastUsage`。 -- 账本:`~/.paseo/plugins/token-ledger/ledger.jsonl`,按 `agentId` 过滤。 -- 已知可复算不变量:同一 session 内逐调用 usage 求和 == 末次 `lastUsage`(样本 session 已验证)。 - -## 8. 修复记录(2026-10-16) - -§5 结论: -1. 不能。0.11.2 协议没有 `usage_updated` 推给插件;OMP 的 `turn_completed` 不带 usage(`agent/providers/omp/agent.js` `completeTurn`)。 -2. 采用 final drain。`OmpUsagePoller.completeTurn()` 异步读 stats 后才发 `usage_updated`,此时 `activeTurnId` 仍指向刚结束的轮;usage 未变化时不发。 - 因此 session scope 的 completed 轮记 `open.ending`,在以下任一条件下结算:第一个新观察快照、下一轮开始、2s 超时、插件停止/agent 退役。 - `ending` 随 journal 持久化;结算的 status/endedAt 取 ending,不被超时或中断改写。`lastClosedTurnId` 防重放不变。 -3. 不采用两段式结算。记录 id 为稳定 UUID,磁盘记录不重写。 -4. `modelCalls` 在 session/unknown scope 下为 null(新记录写 null,旧记录读时归一为 null)。 -5. 未做累计差分。OMP 记录仍是 session 快照,不计入轮次合计,`costUsd` 仍为 null。 - -实测:OMP agent `16838577…` 一轮 2 次调用,最后调用 07:32:09.242,记录 endedAt 07:32:09.252,`input=38860/output=194` 与原生日志逐调用求和相等。 diff --git a/docs/releases/v0.8.0.md b/docs/releases/v0.8.0.md new file mode 100644 index 0000000..5653a1c --- /dev/null +++ b/docs/releases/v0.8.0.md @@ -0,0 +1,42 @@ +# TokenLedger v0.8.0 + +Released 2026-10-23. Tracks agents in Paseo 0.12 background workspaces and +makes token and cost accounting more accurate and more explicit about what is +unknown. + +## Compatibility + +Requires **Paseo >=0.11.0** on both the daemon and app, without an upper version +limit. The verified hosts are **0.11.1** and **0.12.0**. SDK development +dependencies stay pinned to 0.11.1; CI checks 0.11.0, 0.11.1, 0.12.0, and the +latest SDK. See the [compatibility notes](../compatibility.md). + +Existing `ledger.jsonl` records are reinterpreted on read and are not +rewritten. Existing `pricing.json` files are kept. After updating, reload or +reopen the app so the client uses the new summary fields. + +## Changes + +- **Paseo 0.12 background workspaces.** Paseo 0.12 hides background workspaces + from default agent and workspace lists. Before this release, turns in those + workspaces were closed as canceled with no usage. TokenLedger now requests + background workspaces, falls back on older hosts, and confirms a removal by + agent ID before it settles an open turn. +- **Overview ranges.** Switching ranges stays on the same screen and no longer + adds a navigation entry per click. Thanks to @ChtJT (#4). +- **Usage scope.** Per-turn usage is kept apart from OMP session/branch totals + and unknown-scope snapshots. Snapshots stay visible but are not added to turn + or date-range sums. +- **Unknown is not zero.** Missing counts, cache rates, and costs show as + unknown. A cache hit rate shows only with a complete prompt denominator. The + panel, pill, sidebar, and overview mark incomplete usage and partial + estimates. +- **Pricing.** No private price seeds or fixed currency conversion are built + in; new price files start empty. Prices match full model IDs and explicit + aliases. Ambiguous or formerly unsafe matches block reference fallback and + show a pricing.json warning. + +## Known gaps + +Paseo 0.12.0 still omits cache-write counts and provider-internal subagent +usage. See [usage API requirements](../paseo-usage-api.md). diff --git "a/docs/\351\232\217\347\254\224/omp-drain-workaround-design-notes.md" "b/docs/\351\232\217\347\254\224/omp-drain-workaround-design-notes.md" deleted file mode 100644 index f1100f3..0000000 --- "a/docs/\351\232\217\347\254\224/omp-drain-workaround-design-notes.md" +++ /dev/null @@ -1,79 +0,0 @@ -# OMP drain workaround 设计权衡记录 - -日期:2026-10-16 -背景:修复 issue-omp-turn-usage-settlement.md 中记录的"OMP 轮次结算丢失 usage"问题 -提交:0ac6d72 - -## 问题根源 - -Paseo 0.11.2 的两个异步通道无法对齐: - -1. **timeline** 的 `turn_completed` 不带 usage(OMP 特有) -2. **agent_update** 的 `lastUsage` 快照在终止事件之后才异步到达(9–17ms 实测) -3. 快照里 `activeTurn` 仍然指向刚结束的轮(所以不能用"变 null"当 drain 信号) -4. usage 没变化时 OMP 不发最后一个快照(所以必须有超时后备) - -## 当前方案(0ac6d72) - -session scope 的 completed 轮先记 `open.ending`(持久化进 journal),延迟结算,在以下任一条件触发: - -1. 收到第一个新的 usage 观察(`noteUsage`) -2. 下一轮开始(`streamTurn` 的 turn_started) -3. 2s 超时(`scheduleSettle`) -4. 插件停止或 agent 归档(`stopTracker` / `retireAgent`) - -结算时的 status/endedAt 用 `ending` 记录的值,不被超时改写。 - -## 为什么不优雅 - -1. **状态机多了一层**:turn 生命周期从"open → closed"变成"open → ending → closed"(仅 session scope)。journal 要持久化 ending,恢复时要处理。 -2. **结算触发点分散**:四个触发条件分散在三个函数里,改 turn 生命周期时要检查所有分支。 -3. **超时是猜的**:2s 没有依据(只是"比 3s 轮询周期短,比异步 stats 读慢"),OMP 改行为或网络慢时会失效。 -4. **两套语义共存**:Codex/Claude 立即结算,OMP 延迟结算,`tokenScope` 判断散布在多处。 - -## 考虑过但放弃的方案 - -| 方案 | 为什么不行 | -|---|---| -| 立即用已有观察结算 | 旧代码,丢最后一次调用或整轮为空 | -| 等 activeTurn 消失 | 源码证实最后快照发出时 activeTurn 还在,信号来得比 drain 早 | -| 只在下一轮开始时结算上一轮 | 最后一轮可能永远不结算,面板显示"进行中"会很怪 | -| 两段式结算,后续升级记录 | 与"磁盘原始值不重写"约束冲突,Timeline 也已发过 | -| Codex/Claude 也延迟,统一逻辑 | 它们的终止事件已带 usage,延迟纯属浪费,所有轮次晚 250ms 显示 | -| 标注"OMP 不保证准确"放弃修 | 对用户不诚实,plugin 已发布不能降级承诺 | - -## 长远重构方向(如果上游修了) - -**最理想**:Paseo 让 OMP 的 `turn_completed` 带上 usage(或新增 `turn_usage_finalized` 事件)。 - -届时可以**删除**: -- `open.ending` 字段和相关 schema -- `endLifecycleTurn` 函数 -- `scheduleSettle` 和 `SESSION_DRAIN_MS` -- `noteUsage` / `streamTurn` 里的 `if (open.ending && ...)` 分支 -- `observeEnd` 的 `scheduleSettle` 调用 - -**保留**: -- `tokenScope` 判断(这是语义区分,不是 workaround) -- journal 持久化逻辑(ending 字段可废弃但保留兼容读,不影响) -- `lastClosedTurnId` 防重放(这是所有 provider 都需要的) - -重构后 OMP 回到和 Claude 一样的立即结算路径,代码复杂度回到 2026-10-15 以前的水平。 - -## 判断何时可以重构 - -监控信号: -1. Paseo release notes 提到 OMP turn_completed 带 usage 或相关修复 -2. `@getpaseo/protocol` 的 `AgentStreamEventPayloadSchema` 里 `turn_completed` 的 `usage` 字段从 optional 变 required(对 OMP) -3. 实测 OMP 轮次的 `finalizeTurn` 里 `finalUsage` 不再为 null - -届时翻出这份文档,按"长远重构方向"清单逐项删除 workaround 代码,跑测试,确认 OMP 实测记录的 source 从 `last_observation` 变成 `turn_usage`,即可完成清理。 - -## 当前方案的验收证据 - -- typecheck 通过,72/72 测试通过 -- 新增 6 个确定性测试覆盖竞争时序(samples A/B、无重复计数、ending 恢复、stream 优先) -- 实测 OMP agent `16838577…`,2 次调用,最后调用 07:32:09.242,记录 endedAt 07:32:09.252,token 总和与原生日志一致 -- `modelCalls=null`(不虚报快照数) - -这个方案在**既有约束下**(磁盘不重写、turnId 非全局唯一、timeline 不回填、未知不估算)是能做到的最务实的。 diff --git a/package-lock.json b/package-lock.json index a517ae5..320c6ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "paseo-token-ledger", - "version": "0.7.0", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "paseo-token-ledger", - "version": "0.7.0", + "version": "0.8.0", "license": "MIT", "devDependencies": { "@getpaseo/client": "0.11.1", diff --git a/package.json b/package.json index 49864a0..38c3483 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paseo-token-ledger", - "version": "0.7.0", + "version": "0.8.0", "description": "Paseo plugin: per-turn token usage, cache counts, and cost tracking with a persistent local ledger.", "type": "module", "license": "MIT",