@chenlongapps/opencode-token-usage 0.4.9 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/README.zh-CN.md +7 -4
- package/ROADMAP.md +3 -0
- package/dist/controller.js +18 -3
- package/dist/performance.d.ts +17 -4
- package/dist/performance.js +193 -35
- package/dist/prices.generated.d.ts +23 -1
- package/dist/prices.generated.js +3 -1
- package/dist/pricing.d.ts +1 -1
- package/dist/runtime.d.ts +11 -4
- package/dist/runtime.js +12 -6
- package/dist/source.d.ts +3 -1
- package/dist/source.js +10 -0
- package/dist/tui.js +3 -1
- package/docs/verification.md +68 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Alternatively, add the package to your project's `opencode.json` or `opencode.js
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
> [!NOTE]
|
|
37
|
-
> Requires Node.js 22+ (see the OpenTUI engine note under [Development](#development)). OpenCode 2.0.9 and 2.0.10 have been verified in earlier releases. SDK 2.0.24 integration checks have passed on OpenCode 2.0.11, 2.0.22, 2.0.24, and 2.0.
|
|
37
|
+
> Requires Node.js 22+ (see the OpenTUI engine note under [Development](#development)). OpenCode 2.0.9 and 2.0.10 have been verified in earlier releases. SDK 2.0.24 integration checks have passed on OpenCode 2.0.11, 2.0.22, 2.0.24, 2.0.26, and 2.0.28. Native turn durations and sidebar title clicks were integration-tested on 2.0.26 and 2.0.28; the other listed host checks predate these additions.
|
|
38
38
|
|
|
39
39
|
Restart OpenCode after installation. The panel appears in the native sidebar when `session.sidebar` is set to `auto` and the terminal is wide enough. Click its **Token Usage** title to open the detailed usage dialog. OpenCode hides the sidebar in subagent views, so the plugin keeps one live summary line above the composer with Context, Total, Cost, Time, and TPS. Click the line to open the full statistics in a centered dialog. Press Escape or click **esc** to close it; closing the dialog does not interrupt the subagent.
|
|
40
40
|
|
|
@@ -86,8 +86,8 @@ Costs follow the sidebar's pricing and `partial`/unavailable/free conventions. I
|
|
|
86
86
|
- A fork is a separate session tree. Inherited message copies are attributed only to their original source to prevent double counting.
|
|
87
87
|
- `Context` only searches messages after the most recent compaction with `status === "completed"` and is hidden when reliable usage or a model context limit is unavailable.
|
|
88
88
|
- `Steps` counts every assistant message in the tree, whether or not it reported usage, and reuses the fork-copy de-duplication so inherited history is never counted twice.
|
|
89
|
-
- `Run Time` follows OpenCode 2.0.26's native response footer: assistant completion time minus the first user/synthetic input's creation time after the previous idle marker. Older histories without idle markers use the nearest input;
|
|
90
|
-
- While the viewed session is running, `Run Time` estimates **elapsed time so far**, not final or remaining time, and prefixes the cumulative display with `~` (for example, `~42.5s`). A shared 500 ms clock replaces the current turn's already-counted duration rather than adding an overlapping steer/retry duration. Clock ticks only update memory: history reads remain event-driven. One optional read-only server-plugin clock RPC at startup/reconnection supplies a server-time anchor, with timestamped live events as a fallback; local monotonic time advances it without assuming that a remote server shares the client's clock. Opening an already-running session restores the turn's start from history. Without a trusted anchor or valid timing, native totals remain unchanged.
|
|
89
|
+
- `Run Time` follows OpenCode 2.0.26's native response footer: assistant completion time minus the first user/synthetic input's creation time after the previous idle marker. A new session whose native time metadata records no previous idle uses the first input even before its first idle marker: synthetic deliveries and steers do not reset that turn. Older histories without idle markers use the nearest input; missing session metadata preserves this legacy fallback. Without any input, it uses the assistant's creation time. Each turn contributes only its final completed response duration, not the overlapping duration of every assistant step. Model calls, tools, retries and foreground waits within that span are included; idle time between turns is not. Waiting for a subagent is already part of the parent's span and is never added separately.
|
|
90
|
+
- While the viewed session is running, `Run Time` estimates **elapsed time so far**, not final or remaining time, and prefixes the cumulative display with `~` (for example, `~42.5s`). A shared 500 ms clock replaces the current turn's already-counted duration rather than adding an overlapping steer/retry duration. Clock ticks only update memory: history reads remain event-driven. One optional read-only server-plugin clock RPC at startup/reconnection supplies a server-time anchor, with timestamped live events as a fallback; local monotonic time advances it without assuming that a remote server shares the client's clock. Opening an already-running session restores the turn's start from history. A reconnect history refresh arriving before clock resynchronization keeps the last trusted display for the same retained open turn and unchanged historical subtotal, rather than dropping it to zero. Without a trusted anchor or valid timing, native totals remain unchanged.
|
|
91
91
|
- Completion, failure or interruption stops projection; reported native completion timing replaces the estimate and removes `~`, so a small downward correction is possible. Only native completed durations enter the historical subtotal. Totals use unrounded milliseconds before formatting (milliseconds below one second, one decimal below one minute, then minutes/hours), so they need not equal the sum of rounded footer labels. The existing message-history read restores totals after restarts, including sessions predating installation; no experimental execution-log API or server plugin is required. Missing historical timing data displays `—`, not zero. Read failures freeze the last displayed value and mark it `Not updated` (`stale` in the short summary).
|
|
92
92
|
- `Run Time` / `Time` shows the duration and its estimate/freshness markers, without running-status text or animation. An idle parent's time never advances just because a child is active. Closing or switching views cleans up their estimates, and the shared elapsed-time ticker stops when no view needs it. Unavailable/stale timing keeps its own marker.
|
|
93
93
|
- A confirmed zero `Run Time` / `Time` is hidden in the sidebar, subagent summary/dialog and both `/usage` modes, without leaving an empty row or field. A nonzero live estimate makes the first turn visible even before any response has completed. Nonzero millisecond durations, unavailable values and stale timing remain visible.
|
|
@@ -96,6 +96,7 @@ Costs follow the sidebar's pricing and `partial`/unavailable/free conventions. I
|
|
|
96
96
|
- The checked-in fallback snapshot is generated from [models.dev](https://models.dev/api.json) using only reviewed first-party provider/model families; a small set of manufacturer-verified exceptions is kept separately. It covers priced text models, without downloading prices while the plugin runs. Gateway models match exact manufacturer IDs, documented aliases, and known wrappers; only a terminal `-free` or `:free` can be removed for a second exact lookup.
|
|
97
97
|
- Confirmed free usage displays `$0.00`; positive estimates below `$0.001` display `<$0.001`, estimates through and including `$0.01` use three decimal places, and larger estimates use two. Unavailable prices display `—`; known subtotals with unpriced messages are marked `partial`. The snapshot excludes gateway markups, regional premiums, unlisted discounts, non-text billing, tool fees, and taxes, so Est. Cost is not a provider bill. See [price sources and limitations](docs/pricing.md).
|
|
98
98
|
- Live `TPS` uses a ~2s sliding window, batches deliveries within 100 ms and requires a span of at least 500 ms. The first batch is only a baseline: its bytes are not counted as output generated after its timestamp. Smoothing follows elapsed time rather than delta count. After 2s without a new delta, a shared in-memory timeout expires the estimate without reading history; historical TPS takes over, or the row is hidden if unavailable. Resuming output after expiry starts a fresh baseline and smoothing state.
|
|
99
|
+
- Reconnection resets live TPS windows and reconciles all tracked sessions, including offscreen sessions, with the server's active-session set. Quiet unfinished streams get a shared activity check after 60s, without reading message history. Confirmed inactive streams and their TTFT observations expire after a further 60s. Running tools, retries and pre-first-token waits have no hard lifetime; failed activity reads retain state and retry. Provider `streamed` metadata alone never means that tools have finished. Checks stop when no unfinished streams remain.
|
|
99
100
|
- Live TPS measures observable delivery, not the model's internal token-generation clock. Responses summary bursts, done-only content and transport buffering can still affect estimates; complete hidden reasoning cannot be reconstructed. Without `~`, TPS is `Σ(Output + Reasoning) ÷ Σ(time.streamed − time.created)` in seconds across valid assistant samples in the tree, not just the last request. This historical denominator includes pre-first-token waiting, so it is not the same metric as the recent live estimate. No model-specific multipliers, hard TPS caps or hidden-reasoning guesses are applied.
|
|
100
101
|
- Initial read failures display `Unavailable`. Later failures retain the last complete snapshot, display `Not updated`, and retry automatically.
|
|
101
102
|
|
|
@@ -117,7 +118,9 @@ Failed validation never bumps the version. An unfinished tagged price release is
|
|
|
117
118
|
|
|
118
119
|
For a manual refresh, run `npm run prices:update` in a networked environment, review the generated diff and exceptions, then run the checks above. Builds and plugin refreshes do not contact models.dev.
|
|
119
120
|
|
|
120
|
-
`test:smoke` packages the real artifact and validates loading, refreshes, `/usage`, sidebar title clicks, subagent aggregation, per-message pricing, first-party price fallback, model switching, TPS, TTFT, Responses summary bursts/done-only summaries/quiet expiry, and session-local native turn durations (unfinished responses, idle gaps, interruption, message-history restoration, forks and narrow child views) against an isolated OpenCode instance and local mock providers. It requires Python 3, an available local port, and npm network access. It never modifies your existing OpenCode configuration or calls paid models.
|
|
121
|
+
`test:smoke` packages the real artifact and validates loading, refreshes, `/usage`, sidebar title clicks, subagent aggregation, per-message pricing, first-party price fallback, model switching, TPS, TTFT, Responses summary bursts/done-only summaries/quiet expiry, and session-local native turn durations (unfinished responses, first-turn synthetic continuations, reopening a running TUI, terminal focus events, idle gaps, interruption, message-history restoration, forks and narrow child views) against an isolated OpenCode instance and local mock providers. It requires Python 3, an available local port, and npm network access. It never modifies your existing OpenCode configuration or calls paid models. Terminal focus checks do not establish physical macOS multi-display, App Nap or sleep/wake behavior.
|
|
122
|
+
|
|
123
|
+
Performance lifecycle checks also use the real host's active-session API, with ending events withheld from an offscreen monitor, injected reconnect handshakes and an accelerated 25-hour tool/child wait. They verify preservation while running and reclamation after completion without history reads.
|
|
121
124
|
|
|
122
125
|
By default, smoke uses `opencode` from `PATH`. Stable OpenCode 2 versions from 2.0.9 onward can run; a host not yet verified with the current SDK prints a warning and continues through the actual compatibility checks. Passing the version check alone does not establish compatibility. The SDK dependencies remain pinned independently of your local CLI updates.
|
|
123
126
|
|
package/README.zh-CN.md
CHANGED
|
@@ -34,7 +34,7 @@ opencode plugin add @chenlongapps/opencode-token-usage
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
> [!NOTE]
|
|
37
|
-
> 需要 Node.js 22+(见[开发](#开发)中的 OpenTUI 引擎说明)。此前发布已验证 OpenCode 2.0.9 和 2.0.10;SDK 2.0.24 集成检查已在 OpenCode 2.0.11、2.0.22、2.0.24 和 2.0.
|
|
37
|
+
> 需要 Node.js 22+(见[开发](#开发)中的 OpenTUI 引擎说明)。此前发布已验证 OpenCode 2.0.9 和 2.0.10;SDK 2.0.24 集成检查已在 OpenCode 2.0.11、2.0.22、2.0.24、2.0.26 和 2.0.28 上通过。原生对话耗时与侧边栏标题点击的集成验证使用 2.0.26 和 2.0.28,其余列出的宿主检查早于这些功能加入。
|
|
38
38
|
|
|
39
39
|
安装后重启 OpenCode。终端足够宽且 `session.sidebar` 设为 `auto` 时,面板会显示在原生侧边栏中。点击面板的 **Token Usage** 标题可打开用量详情弹窗。OpenCode 在子代理视图中隐藏侧边栏,因此插件会在输入区上方保留一行实时摘要,显示 Context、Total、Cost、Time 和 TPS。点击摘要可在居中弹窗中查看完整统计;按 Escape 或点击 **esc** 即可关闭,关闭弹窗不会中断子代理。
|
|
40
40
|
|
|
@@ -86,8 +86,8 @@ opencode plugin add @chenlongapps/opencode-token-usage
|
|
|
86
86
|
- 分叉会话是独立会话树。继承消息副本只归原始来源,避免重复计费。
|
|
87
87
|
- `Context` 仅搜索最后一次 `status === "completed"` 的 compaction 之后;没有可靠用量或模型上限时隐藏。
|
|
88
88
|
- `Steps` 统计树中全部 assistant 消息,无论是否已上报用量;复用分叉副本去重,继承历史不会重复计数。
|
|
89
|
-
- `Run Time` 沿用 OpenCode 2.0.26 的原生回答页脚口径:assistant 完成时间减去上一条 idle 标记之后首个 user/synthetic
|
|
90
|
-
- 被查看会话运行中时,`Run Time` 估算的是**截至当前已经过去的时间**,不是最终耗时或剩余时间,累计显示值带 `~`(例如 `~42.5s`)。共享的 500ms 时钟替换当前轮已经计入的时长,不额外叠加重叠的 steer/retry 耗时。时钟 tick 只更新内存,历史读取仍由事件触发。启动/重连时通过一次可选的服务端插件只读时间 RPC
|
|
89
|
+
- `Run Time` 沿用 OpenCode 2.0.26 的原生回答页脚口径:assistant 完成时间减去上一条 idle 标记之后首个 user/synthetic 输入的创建时间。新会话的原生时间元数据尚未记录 idle 时,即使第一条 idle 消息尚未出现,也使用首条输入,同轮 synthetic 投递与 steer 不重置起点。没有 idle 标记的旧历史使用最近一条输入;会话时间元数据缺失时保留这项旧历史回退。没有输入则回退到 assistant 自身的创建时间。每轮只计最终已完成回答的耗时,不把各条 assistant step 的重叠时长重复相加。包含跨度内的模型调用、工具、重试和前台等待,不含轮次之间的空闲;父会话等待子代理的时间已在父会话跨度内,不再额外叠加子代理耗时。
|
|
90
|
+
- 被查看会话运行中时,`Run Time` 估算的是**截至当前已经过去的时间**,不是最终耗时或剩余时间,累计显示值带 `~`(例如 `~42.5s`)。共享的 500ms 时钟替换当前轮已经计入的时长,不额外叠加重叠的 steer/retry 耗时。时钟 tick 只更新内存,历史读取仍由事件触发。启动/重连时通过一次可选的服务端插件只读时间 RPC 取得服务端锚点,带时间戳的实时事件可作为回退;用本地单调时钟推进,不假定远程服务端与客户端时钟一致。中途打开正在运行的会话时,从历史恢复当前轮起点。重连时若历史刷新早于校时完成,仅在同一开放轮次仍保留且历史小计未变时冻结上次可信显示,避免回落到零。没有可信锚点或有效时间时,保留原生累计值。
|
|
91
91
|
- 完成、失败或中断时停止估算;上报的原生完成耗时替换估算值并移除 `~`,因此允许小幅向下校正。历史累计只包含原生已完成耗时。先累加未取整的毫秒值,再格式化(不足一秒显示毫秒,不足一分钟的秒值保留一位小数,其后显示分/小时),因此不一定等于相加各轮已取整的页脚标签。复用现有消息历史读取,重启后可恢复,也适用于安装之前的会话;不依赖实验性执行日志 API 或服务端插件。历史计时数据缺失时显示 `—`,不会显示伪零。读取失败冻结上次显示值并标注 `Not updated`(短摘要中为 `stale`)。
|
|
92
92
|
- `Run Time`/`Time` 仅显示耗时及估算/未更新标记,不附加运行状态文本或动画。已空闲父会话的耗时不会因子代理活跃而增长。关闭视图或切换会话时清理相应估算,无视图需要估算时停止共享耗时 ticker。不可用/未更新的耗时仍保留各自标记。
|
|
93
93
|
- 正常状态下确认累计值为零时,侧边栏、子代理摘要/浮窗及 `/usage` 紧凑/详细模式均隐藏整个 `Run Time`/`Time` 项,不留空行或空字段。当前轮实时估算非零时,首轮回答尚未完成也会显示时间。非零毫秒耗时、不可用值和未更新的耗时仍保留显示。
|
|
@@ -96,6 +96,7 @@ opencode plugin add @chenlongapps/opencode-token-usage
|
|
|
96
96
|
- 内置回退快照从 [models.dev](https://models.dev/api.json) 生成,只采用审核过的原厂 Provider 与自家模型系列;少量经原厂核实的例外单独维护。快照覆盖有价格的文本模型,插件运行时不下载价格。网关模型通过精确原厂 ID、明确别名和已知包装格式匹配;只有终尾 `-free` 或 `:free` 会在再次精确查找前移除。
|
|
97
97
|
- 确认免费时显示 `$0.00`;正费用低于 `$0.001` 时显示 `<$0.001`,不超过 `$0.01` 的费用保留三位小数,更高费用保留两位。价格不可用时显示 `—`;只有部分消息可计算时在已知小计后标注 `partial`。快照不含网关加价、区域溢价、未列明的折扣、非文本计费、工具费和税费;Est. Cost 是估算而非账单。覆盖范围、来源和限制见[价格来源与限制](docs/pricing.md)。
|
|
98
98
|
- 实时 `TPS` 使用约 2 秒滑动窗口,合并 100ms 内的增量,要求至少 500ms 的有效跨度。首批增量仅作基线,其字节不计为该时间戳之后生成的输出;平滑按经过时间而非 delta 条数推进。2 秒没有新增量时,共享的内存超时任务使估算过期,不读取历史;回退到历史 TPS,没有可用历史值则隐藏该行。过期后恢复输出时,重新建立基线与平滑状态。
|
|
99
|
+
- 重连时重置实时 TPS 窗口,并通过服务端活动会话列表对账全部已跟踪会话,包括离屏会话。未结束的流静默 60 秒后进行共享活动检查,不读取消息历史;确认会话已停止后,再保留 60 秒并回收流及其 TTFT 观察。仍在运行的工具、重试及首 token 前等待没有硬性寿命上限;活动查询失败时保留状态并重试。仅有提供商的 `streamed` 元数据不代表工具已结束。没有未结束流时停止检查。
|
|
99
100
|
- 实时 TPS 测量可观察的内容交付,而非模型内部生成 token 的时钟。Responses 摘要突发、仅在 done 中返回的内容和传输缓冲仍可能影响估算,不能还原完整隐藏推理。不带 `~` 时,TPS 为全树有效 assistant 样本的 `Σ(Output + Reasoning) ÷ Σ(time.streamed − time.created)`(换算为秒),不是仅取最近请求;该历史分母包含首 token 前等待,与近期实时估算口径不同。不做模型专属倍率、TPS 硬上限或隐藏 reasoning token 猜测。
|
|
100
101
|
- 首次读取失败显示 `Unavailable`;后续失败保留上次完整快照、标注 `Not updated` 并自动重试。
|
|
101
102
|
|
|
@@ -117,7 +118,9 @@ OpenCode SDK 包精确锁定为 `2.0.24`,OpenTUI 为 `0.5.14`。Node.js 22 下
|
|
|
117
118
|
|
|
118
119
|
手动更新时,在联网的维护环境运行 `npm run prices:update`,审阅生成文件及例外差异,再执行上述检查。构建和插件刷新不会请求 models.dev。
|
|
119
120
|
|
|
120
|
-
`test:smoke` 会打包真实产物,并在隔离的 OpenCode 与本地模拟提供商中验证加载、刷新、`/usage`、侧边栏标题点击、子代理累计、逐消息计价、原厂价格补全、模型切换、TPS、TTFT、Responses 摘要突发/done-only
|
|
121
|
+
`test:smoke` 会打包真实产物,并在隔离的 OpenCode 与本地模拟提供商中验证加载、刷新、`/usage`、侧边栏标题点击、子代理累计、逐消息计价、原厂价格补全、模型切换、TPS、TTFT、Responses 摘要突发/done-only 摘要/静默过期,以及会话自身的原生对话耗时(未完成回答、首轮 synthetic 延续、重开运行中 TUI、终端焦点事件、空闲、中断、消息历史恢复、分叉和窄终端子代理视图)。它需要 Python 3、可用的本地端口和 npm 网络访问,不会修改现有 OpenCode 配置或调用付费模型。终端焦点检查不代表已验证物理 macOS 多屏切换、App Nap 或睡眠/唤醒行为。
|
|
122
|
+
|
|
123
|
+
性能生命周期检查还会使用真实宿主的活动会话 API,对离屏监视器过滤结束事件、注入重连握手,并加速模拟 25 小时的工具/子代理等待,确认运行时保留状态、结束后无需读取历史即可回收。
|
|
121
124
|
|
|
122
125
|
默认使用 `PATH` 中的 `opencode`。OpenCode 2.0.9 及之后的稳定版 2.x 可以运行;尚未与当前 SDK 验证的宿主会输出警告,然后继续实际兼容性检查。仅通过版本检查不代表兼容性已验证。SDK 依赖仍独立锁定,不随本机 CLI 更新而自动升级。
|
|
123
126
|
|
package/ROADMAP.md
CHANGED
|
@@ -115,6 +115,8 @@ Steps 1,234
|
|
|
115
115
|
* [x] 运行中以 `~` 显示当前轮已耗时估算,首轮非零估算可见;共享 500ms 单调时钟,不定时读取 API,结束后校正为原生值
|
|
116
116
|
* [x] 时间缺失显示不可用,读取失败保留旧值并标注未更新;分叉副本不继承耗时,已提交回退随删除历史扣除
|
|
117
117
|
* [x] 移除动画后通过类型检查、205 项测试、构建及真实 OpenCode 2.0.26 完整打包集成复验
|
|
118
|
+
* [x] 首轮尚无 idle 消息时依据原生会话时间保持起点,synthetic/steer 不误重置;重连校时期间保留同一开放轮次的可信显示,247 项测试及真实 OpenCode 2.0.28 完整打包验证通过
|
|
119
|
+
* [ ] macOS 物理多屏切换导致耗时倒退的实机复验(终端焦点事件、视图重建与模拟重连已覆盖,不代表已验证物理切屏、App Nap 或睡眠/唤醒)
|
|
118
120
|
|
|
119
121
|
## 后续评估
|
|
120
122
|
|
|
@@ -123,3 +125,4 @@ Steps 1,234
|
|
|
123
125
|
## 待解决问题
|
|
124
126
|
|
|
125
127
|
- [x] TPS 显示机制修改:100ms 突发合并、首批基线、500ms 最小跨度、时间加权平滑与静默过期;209 项测试及真实 OpenCode 2.0.26 本地 Responses SSE 打包集成验证通过
|
|
128
|
+
- [x] 异常流与 TTFT 回收:离屏会话的共享活动检查、重连对账、丢失结束事件回收及异步竞态保护;长工具/重试等待不按静默时长误清理,234 项测试及真实 OpenCode 2.0.26 打包集成验证通过(长等待采用加速时钟)
|
package/dist/controller.js
CHANGED
|
@@ -57,7 +57,7 @@ export class UsageController {
|
|
|
57
57
|
this.performanceDelay = performanceDelay;
|
|
58
58
|
this.detailed = detailed;
|
|
59
59
|
this.ownsPerformance = performance === undefined;
|
|
60
|
-
this.performance = performance ?? new PerformanceMonitor(subscribe);
|
|
60
|
+
this.performance = performance ?? new PerformanceMonitor(subscribe, { active: source.active });
|
|
61
61
|
this.unsubscribePerformance = this.performance.listen(sessionID => {
|
|
62
62
|
if (this.sessions.has(sessionID))
|
|
63
63
|
this.refreshPerformance();
|
|
@@ -231,15 +231,30 @@ export class UsageController {
|
|
|
231
231
|
const history = viewedMessages(snapshot);
|
|
232
232
|
const session = snapshot.sessions.get(snapshot.viewedID);
|
|
233
233
|
const revertMessageID = session.revert?.messageID;
|
|
234
|
-
|
|
234
|
+
const previousRuntime = this.preparedRuntime;
|
|
235
|
+
this.preparedRuntime = prepareRuntime(history, !!session.fork, session.time);
|
|
235
236
|
this.runtimeFresh = true;
|
|
237
|
+
let runtime = this.runtimeSummary();
|
|
238
|
+
const estimated = this.state.runtime?.estimatedMilliseconds;
|
|
239
|
+
const turn = this.preparedRuntime.turn, previousTurn = previousRuntime?.turn;
|
|
240
|
+
// A reconnect can finish reading history before its optional clock RPC.
|
|
241
|
+
// Keep the last trusted projection only for the same retained open turn
|
|
242
|
+
// and unchanged historical subtotal. This is not a maximum-value latch:
|
|
243
|
+
// native completion, reverts and replacement histories remain authoritative.
|
|
244
|
+
if (runtime.status === "ready" && runtime.estimatedMilliseconds === undefined && estimated !== undefined
|
|
245
|
+
&& turn && previousTurn && turn.started === previousTurn.started
|
|
246
|
+
&& runtime.milliseconds !== undefined && previousRuntime?.summary.milliseconds !== undefined
|
|
247
|
+
&& runtime.milliseconds - turn.milliseconds === previousRuntime.summary.milliseconds - previousTurn.milliseconds
|
|
248
|
+
&& runtime.milliseconds <= estimated && snapshot.messages.get(snapshot.viewedID)?.has(previousTurn.messageID)) {
|
|
249
|
+
runtime = { ...runtime, estimatedMilliseconds: estimated };
|
|
250
|
+
}
|
|
236
251
|
this.update({
|
|
237
252
|
status: "ready",
|
|
238
253
|
summary: summarize(messages, snapshot.model.catalog),
|
|
239
254
|
context: contextUsage(history, snapshot.model.context, revertMessageID),
|
|
240
255
|
...(this.detailed ? { details: { request: requestDetails(history, revertMessageID), models: summarizeModels(messages, snapshot.model.catalog), sessionTitle: session.title } } : {}),
|
|
241
256
|
performance: this.performance.summaryPrepared(this.prepared, this.sessions),
|
|
242
|
-
runtime
|
|
257
|
+
runtime,
|
|
243
258
|
model: snapshot.model.label,
|
|
244
259
|
});
|
|
245
260
|
this.refreshRuntime();
|
package/dist/performance.d.ts
CHANGED
|
@@ -26,6 +26,14 @@ export declare const LIVE_MIN_SPAN_MS = 500;
|
|
|
26
26
|
export declare const LIVE_SAMPLE_MS = 100;
|
|
27
27
|
export declare const LIVE_EWMA_ALPHA = 0.35;
|
|
28
28
|
export declare const LIVE_BYTES_PER_TOKEN = 4;
|
|
29
|
+
/** Quiet streams need a server liveness check, not a maximum tool-wait duration. */
|
|
30
|
+
export declare const ACTIVITY_CHECK_MS = 60000;
|
|
31
|
+
export type ReadActiveSessions = (signal: AbortSignal) => Promise<ReadonlySet<string>>;
|
|
32
|
+
export interface PerformanceOptions {
|
|
33
|
+
retention?: number;
|
|
34
|
+
monotonic?: () => number;
|
|
35
|
+
active?: ReadActiveSessions | undefined;
|
|
36
|
+
}
|
|
29
37
|
/** The first batch is a counter baseline, not bytes generated after its timestamp. */
|
|
30
38
|
export declare function estimatedLiveTps(samples: readonly StreamSample[]): number | undefined;
|
|
31
39
|
/** Time-based EWMA: elapsed time determines the weight, never delta count. */
|
|
@@ -44,22 +52,27 @@ export declare function historicalPerformance(messages: Iterable<UsageMessage>,
|
|
|
44
52
|
export declare function preparePerformance(messages: Iterable<UsageMessage>): PreparedPerformance;
|
|
45
53
|
/** Captures streaming performance for every session for one plugin lifetime. */
|
|
46
54
|
export declare class PerformanceMonitor {
|
|
47
|
-
private readonly retention;
|
|
48
|
-
private readonly now;
|
|
49
|
-
private readonly monotonic;
|
|
50
55
|
private active;
|
|
51
56
|
private observed;
|
|
52
57
|
private listeners;
|
|
53
58
|
private cleanupTimer;
|
|
59
|
+
private synchronization;
|
|
54
60
|
private disposed;
|
|
55
61
|
private readonly unsubscribe;
|
|
56
|
-
|
|
62
|
+
private readonly retention;
|
|
63
|
+
private readonly monotonic;
|
|
64
|
+
private readonly readActive;
|
|
65
|
+
constructor(subscribe?: PerformanceSubscribe, options?: PerformanceOptions);
|
|
57
66
|
listen(listener: PerformanceListener): () => void;
|
|
58
67
|
dispose(): void;
|
|
59
68
|
private key;
|
|
60
69
|
private publish;
|
|
61
70
|
private deleteSession;
|
|
62
71
|
private resetLive;
|
|
72
|
+
private touch;
|
|
73
|
+
private finish;
|
|
74
|
+
/** One shared, history-free read covers offscreen sessions too. Failures are not idle. */
|
|
75
|
+
private checkActivity;
|
|
63
76
|
private prune;
|
|
64
77
|
private scheduleCleanup;
|
|
65
78
|
handle(event: PerformanceEvent): boolean;
|
package/dist/performance.js
CHANGED
|
@@ -4,6 +4,8 @@ export const LIVE_MIN_SPAN_MS = 500;
|
|
|
4
4
|
export const LIVE_SAMPLE_MS = 100;
|
|
5
5
|
export const LIVE_EWMA_ALPHA = 0.35;
|
|
6
6
|
export const LIVE_BYTES_PER_TOKEN = 4;
|
|
7
|
+
/** Quiet streams need a server liveness check, not a maximum tool-wait duration. */
|
|
8
|
+
export const ACTIVITY_CHECK_MS = 60_000;
|
|
7
9
|
/** The first batch is a counter baseline, not bytes generated after its timestamp. */
|
|
8
10
|
export function estimatedLiveTps(samples) {
|
|
9
11
|
if (samples.length < 2)
|
|
@@ -116,19 +118,20 @@ function performanceWithObservations(prepared, observations) {
|
|
|
116
118
|
}
|
|
117
119
|
/** Captures streaming performance for every session for one plugin lifetime. */
|
|
118
120
|
export class PerformanceMonitor {
|
|
119
|
-
retention;
|
|
120
|
-
now;
|
|
121
|
-
monotonic;
|
|
122
121
|
active = new Map();
|
|
123
122
|
observed = new Map();
|
|
124
123
|
listeners = new Set();
|
|
125
124
|
cleanupTimer;
|
|
125
|
+
synchronization;
|
|
126
126
|
disposed = false;
|
|
127
127
|
unsubscribe;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
retention;
|
|
129
|
+
monotonic;
|
|
130
|
+
readActive;
|
|
131
|
+
constructor(subscribe, options = {}) {
|
|
132
|
+
this.retention = options.retention ?? 60_000;
|
|
133
|
+
this.monotonic = options.monotonic ?? (() => performance.now());
|
|
134
|
+
this.readActive = options.active;
|
|
132
135
|
this.unsubscribe = subscribe?.(event => this.handle(event));
|
|
133
136
|
}
|
|
134
137
|
listen(listener) {
|
|
@@ -142,6 +145,7 @@ export class PerformanceMonitor {
|
|
|
142
145
|
return;
|
|
143
146
|
this.disposed = true;
|
|
144
147
|
this.unsubscribe?.();
|
|
148
|
+
this.synchronization?.abort();
|
|
145
149
|
clearTimeout(this.cleanupTimer);
|
|
146
150
|
this.cleanupTimer = undefined;
|
|
147
151
|
this.listeners.clear();
|
|
@@ -170,13 +174,93 @@ export class PerformanceMonitor {
|
|
|
170
174
|
delete step.liveDuration;
|
|
171
175
|
delete step.smoothedAt;
|
|
172
176
|
}
|
|
177
|
+
touch(step, created) {
|
|
178
|
+
step.revision++;
|
|
179
|
+
step.checkedAt = this.monotonic();
|
|
180
|
+
if (finite(created))
|
|
181
|
+
step.latest = Math.max(step.latest, created);
|
|
182
|
+
// A live event wins over an inactive snapshot taken just before it arrived.
|
|
183
|
+
if (step.inactive) {
|
|
184
|
+
delete step.finishedAt;
|
|
185
|
+
delete step.inactive;
|
|
186
|
+
const sample = this.observed.get(this.key(step.sessionID, step.messageID));
|
|
187
|
+
if (sample)
|
|
188
|
+
delete sample.expiresAt;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
finish(step, inactive = false) {
|
|
192
|
+
if (step.finishedAt !== undefined && (inactive || !step.inactive))
|
|
193
|
+
return;
|
|
194
|
+
step.finishedAt ??= this.monotonic();
|
|
195
|
+
step.inactive = inactive;
|
|
196
|
+
step.revision++;
|
|
197
|
+
const key = this.key(step.sessionID, step.messageID);
|
|
198
|
+
const sample = this.observed.get(key);
|
|
199
|
+
if (sample)
|
|
200
|
+
sample.expiresAt = step.finishedAt + this.retention;
|
|
201
|
+
this.resetLive(step);
|
|
202
|
+
this.publish(step.sessionID);
|
|
203
|
+
}
|
|
204
|
+
/** One shared, history-free read covers offscreen sessions too. Failures are not idle. */
|
|
205
|
+
async checkActivity(force = false) {
|
|
206
|
+
if (this.disposed || !this.readActive)
|
|
207
|
+
return;
|
|
208
|
+
if (this.synchronization) {
|
|
209
|
+
if (!force)
|
|
210
|
+
return;
|
|
211
|
+
this.synchronization.abort();
|
|
212
|
+
this.synchronization = undefined;
|
|
213
|
+
}
|
|
214
|
+
const now = this.monotonic();
|
|
215
|
+
const unfinished = [...this.active.values()].filter(step => step.finishedAt === undefined);
|
|
216
|
+
if (!unfinished.length || (!force && !unfinished.some(step => now - step.checkedAt >= ACTIVITY_CHECK_MS)))
|
|
217
|
+
return;
|
|
218
|
+
// The endpoint is process-wide: use each read for every tracked unfinished step,
|
|
219
|
+
// rather than issuing one request per staggered session deadline.
|
|
220
|
+
const candidates = new Map(unfinished.map(step => [step, step.revision]));
|
|
221
|
+
const request = new AbortController();
|
|
222
|
+
this.synchronization = request;
|
|
223
|
+
const signal = AbortSignal.any([request.signal, AbortSignal.timeout(5_000)]);
|
|
224
|
+
let onAbort = () => { };
|
|
225
|
+
try {
|
|
226
|
+
// Also bound readers that fail to cooperate with cancellation.
|
|
227
|
+
const active = await Promise.race([
|
|
228
|
+
this.readActive(signal),
|
|
229
|
+
new Promise((_, reject) => {
|
|
230
|
+
onAbort = () => reject(signal.reason);
|
|
231
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
232
|
+
if (signal.aborted)
|
|
233
|
+
onAbort();
|
|
234
|
+
}),
|
|
235
|
+
]);
|
|
236
|
+
if (this.disposed || signal.aborted || this.synchronization !== request)
|
|
237
|
+
return;
|
|
238
|
+
for (const [step, revision] of candidates) {
|
|
239
|
+
if (this.active.get(this.key(step.sessionID, step.messageID)) !== step || step.revision !== revision)
|
|
240
|
+
continue;
|
|
241
|
+
if (!active.has(step.sessionID))
|
|
242
|
+
this.finish(step, true);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch { /* Preserve quiet tools/retries when server activity is unknown. */ }
|
|
246
|
+
finally {
|
|
247
|
+
signal.removeEventListener("abort", onAbort);
|
|
248
|
+
request.abort();
|
|
249
|
+
if (this.synchronization === request) {
|
|
250
|
+
this.synchronization = undefined;
|
|
251
|
+
for (const [step, revision] of candidates) {
|
|
252
|
+
if (step.revision === revision)
|
|
253
|
+
step.checkedAt = this.monotonic();
|
|
254
|
+
}
|
|
255
|
+
this.scheduleCleanup();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
173
259
|
prune() {
|
|
174
|
-
const now = this.now();
|
|
175
260
|
const monotonic = this.monotonic();
|
|
176
261
|
const changed = new Set();
|
|
177
262
|
for (const [key, step] of this.active) {
|
|
178
|
-
if (
|
|
179
|
-
&& (step.finishedAt ?? step.reconciledAt) + this.retention <= now) {
|
|
263
|
+
if (step.finishedAt !== undefined && step.finishedAt + this.retention <= monotonic) {
|
|
180
264
|
this.active.delete(key);
|
|
181
265
|
changed.add(step.sessionID);
|
|
182
266
|
}
|
|
@@ -186,7 +270,7 @@ export class PerformanceMonitor {
|
|
|
186
270
|
}
|
|
187
271
|
}
|
|
188
272
|
for (const [key, sample] of this.observed) {
|
|
189
|
-
if (sample.expiresAt !== undefined && sample.expiresAt <=
|
|
273
|
+
if (sample.expiresAt !== undefined && sample.expiresAt <= monotonic) {
|
|
190
274
|
this.observed.delete(key);
|
|
191
275
|
changed.add(sample.sessionID);
|
|
192
276
|
}
|
|
@@ -199,20 +283,21 @@ export class PerformanceMonitor {
|
|
|
199
283
|
if (this.disposed)
|
|
200
284
|
return;
|
|
201
285
|
let next = Infinity;
|
|
202
|
-
const now = this.now();
|
|
203
286
|
const monotonic = this.monotonic();
|
|
204
287
|
const retain = Number.isFinite(this.retention) && this.retention >= 0;
|
|
205
288
|
for (const step of this.active.values()) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
289
|
+
if (retain && step.finishedAt !== undefined)
|
|
290
|
+
next = Math.min(next, step.finishedAt + this.retention - monotonic);
|
|
291
|
+
if (step.finishedAt === undefined && this.readActive && !this.synchronization) {
|
|
292
|
+
next = Math.min(next, step.checkedAt + ACTIVITY_CHECK_MS - monotonic);
|
|
293
|
+
}
|
|
209
294
|
// Local monotonic age, never client epoch minus a remote event timestamp.
|
|
210
295
|
if (step.observedAt !== undefined)
|
|
211
296
|
next = Math.min(next, step.observedAt + LIVE_WINDOW_MS - monotonic);
|
|
212
297
|
}
|
|
213
298
|
for (const sample of this.observed.values()) {
|
|
214
299
|
if (retain && sample.expiresAt !== undefined)
|
|
215
|
-
next = Math.min(next, sample.expiresAt -
|
|
300
|
+
next = Math.min(next, sample.expiresAt - monotonic);
|
|
216
301
|
}
|
|
217
302
|
if (!Number.isFinite(next))
|
|
218
303
|
return;
|
|
@@ -220,6 +305,7 @@ export class PerformanceMonitor {
|
|
|
220
305
|
this.cleanupTimer = undefined;
|
|
221
306
|
for (const sessionID of this.prune())
|
|
222
307
|
this.publish(sessionID);
|
|
308
|
+
void this.checkActivity();
|
|
223
309
|
this.scheduleCleanup();
|
|
224
310
|
}, Math.max(0, Math.ceil(next)));
|
|
225
311
|
this.cleanupTimer.unref?.();
|
|
@@ -229,6 +315,16 @@ export class PerformanceMonitor {
|
|
|
229
315
|
return false;
|
|
230
316
|
for (const sessionID of this.prune())
|
|
231
317
|
this.publish(sessionID);
|
|
318
|
+
if (event.type === "server.connected") {
|
|
319
|
+
// Delivery across a connection gap cannot share a throughput window.
|
|
320
|
+
for (const step of this.active.values()) {
|
|
321
|
+
this.resetLive(step);
|
|
322
|
+
this.publish(step.sessionID);
|
|
323
|
+
}
|
|
324
|
+
void this.checkActivity(true);
|
|
325
|
+
this.scheduleCleanup();
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
232
328
|
const data = (event.data && typeof event.data === "object" ? event.data : {});
|
|
233
329
|
const sessionID = typeof data.sessionID === "string" ? data.sessionID : undefined;
|
|
234
330
|
if (event.type === "session.deleted") {
|
|
@@ -236,8 +332,30 @@ export class PerformanceMonitor {
|
|
|
236
332
|
this.deleteSession(sessionID);
|
|
237
333
|
return false;
|
|
238
334
|
}
|
|
335
|
+
if (!sessionID)
|
|
336
|
+
return false;
|
|
337
|
+
if (["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", "session.idle"].includes(event.type)
|
|
338
|
+
|| (event.type === "session.status" && data.status?.type === "idle")) {
|
|
339
|
+
for (const step of this.active.values()) {
|
|
340
|
+
if (step.sessionID !== sessionID || !finite(event.created) || event.created < step.latest)
|
|
341
|
+
continue;
|
|
342
|
+
step.latest = event.created;
|
|
343
|
+
this.finish(step);
|
|
344
|
+
}
|
|
345
|
+
this.scheduleCleanup();
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (event.type === "session.execution.started"
|
|
349
|
+
|| (event.type === "session.status" && ["busy", "retry"].includes(String(data.status?.type)))) {
|
|
350
|
+
for (const step of this.active.values()) {
|
|
351
|
+
if (step.sessionID === sessionID && finite(event.created) && event.created >= step.latest)
|
|
352
|
+
this.touch(step, event.created);
|
|
353
|
+
}
|
|
354
|
+
this.scheduleCleanup();
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
239
357
|
const messageID = typeof data.assistantMessageID === "string" ? data.assistantMessageID : undefined;
|
|
240
|
-
if (!
|
|
358
|
+
if (!messageID)
|
|
241
359
|
return false;
|
|
242
360
|
const key = this.key(sessionID, messageID);
|
|
243
361
|
if (event.type === "session.step.started") {
|
|
@@ -245,29 +363,65 @@ export class PerformanceMonitor {
|
|
|
245
363
|
return false;
|
|
246
364
|
if (this.active.has(key))
|
|
247
365
|
return false;
|
|
366
|
+
// A session's foreground steps are serial; missing endings must not accumulate
|
|
367
|
+
// forever just because the same session continues to run later steps.
|
|
368
|
+
for (const step of this.active.values()) {
|
|
369
|
+
if (step.sessionID !== sessionID)
|
|
370
|
+
continue;
|
|
371
|
+
if (step.started > data.started)
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
for (const step of this.active.values()) {
|
|
375
|
+
if (step.sessionID !== sessionID)
|
|
376
|
+
continue;
|
|
377
|
+
step.latest = Math.max(step.latest, data.started);
|
|
378
|
+
this.finish(step);
|
|
379
|
+
}
|
|
248
380
|
this.active.set(key, {
|
|
249
381
|
messageID, sessionID, started: data.started, bytes: 0, samples: [], events: new Set(),
|
|
382
|
+
checkedAt: this.monotonic(), revision: 0, latest: Math.max(data.started, finite(event.created) ? event.created : data.started),
|
|
250
383
|
});
|
|
384
|
+
this.scheduleCleanup();
|
|
251
385
|
return false;
|
|
252
386
|
}
|
|
253
387
|
const step = this.active.get(key);
|
|
254
388
|
if (!step)
|
|
255
389
|
return false;
|
|
390
|
+
if (event.type === "session.retry.scheduled" || (event.type.startsWith("session.tool.") && event.type !== "session.tool.input.delta")) {
|
|
391
|
+
if (!finite(event.created) || event.created < step.latest)
|
|
392
|
+
return false;
|
|
393
|
+
if (event.type === "session.retry.scheduled") {
|
|
394
|
+
delete step.finishedAt;
|
|
395
|
+
delete step.inactive;
|
|
396
|
+
delete step.reconciledAt;
|
|
397
|
+
delete step.streamed;
|
|
398
|
+
this.resetLive(step);
|
|
399
|
+
const sample = this.observed.get(key);
|
|
400
|
+
if (sample)
|
|
401
|
+
delete sample.expiresAt;
|
|
402
|
+
}
|
|
403
|
+
this.touch(step, event.created);
|
|
404
|
+
this.scheduleCleanup();
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
256
407
|
if (event.type === "session.step.streamed") {
|
|
257
408
|
if (!finite(event.created) || event.created < step.started)
|
|
258
409
|
return false;
|
|
410
|
+
if (step.streamed !== undefined && event.created <= step.streamed)
|
|
411
|
+
return false;
|
|
259
412
|
step.streamed = event.created;
|
|
413
|
+
this.touch(step, event.created);
|
|
414
|
+
this.scheduleCleanup();
|
|
260
415
|
if (step.bytes > 0)
|
|
261
416
|
this.publish(sessionID);
|
|
262
417
|
return step.bytes > 0;
|
|
263
418
|
}
|
|
264
419
|
if (event.type === "session.step.ended" || event.type === "session.step.failed") {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
this.active.delete(key);
|
|
420
|
+
if (finite(event.created) && event.created < step.latest)
|
|
421
|
+
return false;
|
|
422
|
+
if (finite(event.created))
|
|
423
|
+
step.latest = event.created;
|
|
424
|
+
this.finish(step);
|
|
271
425
|
this.scheduleCleanup();
|
|
272
426
|
return false;
|
|
273
427
|
}
|
|
@@ -280,8 +434,11 @@ export class PerformanceMonitor {
|
|
|
280
434
|
return false;
|
|
281
435
|
if (event.id && step.events.has(event.id))
|
|
282
436
|
return false;
|
|
437
|
+
if (step.finishedAt !== undefined && !step.inactive)
|
|
438
|
+
return false;
|
|
283
439
|
if (event.id)
|
|
284
440
|
step.events.add(event.id);
|
|
441
|
+
this.touch(step, event.created);
|
|
285
442
|
if (step.reconciledAt !== undefined) {
|
|
286
443
|
// A later delta means the snapshot did not represent the final stream.
|
|
287
444
|
delete step.reconciledAt;
|
|
@@ -330,23 +487,24 @@ export class PerformanceMonitor {
|
|
|
330
487
|
}
|
|
331
488
|
reconcile(messages, sessionIDs) {
|
|
332
489
|
const sessions = new Set(sessionIDs);
|
|
333
|
-
|
|
490
|
+
if (this.disposed)
|
|
491
|
+
return;
|
|
492
|
+
const represented = new Map();
|
|
334
493
|
for (const message of messages) {
|
|
335
494
|
if (message.type === "assistant" && (message.tokens || message.time?.streamed !== undefined
|
|
336
495
|
|| message.time?.completed !== undefined))
|
|
337
|
-
represented.
|
|
496
|
+
represented.set(message.id, message);
|
|
338
497
|
}
|
|
339
|
-
for (const
|
|
498
|
+
for (const step of this.active.values()) {
|
|
340
499
|
if (sessions.has(step.sessionID) && represented.has(step.messageID)) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
sample.expiresAt ??= step.reconciledAt + this.retention;
|
|
500
|
+
// streamed/tokens describe the provider boundary, not tool settlement.
|
|
501
|
+
// Only completion or an authoritative inactive session permits expiry;
|
|
502
|
+
// a stale pre-retry completion cannot settle newer activity.
|
|
503
|
+
step.reconciledAt ??= this.monotonic();
|
|
504
|
+
const completed = represented.get(step.messageID)?.time?.completed;
|
|
505
|
+
if (finite(completed) && completed >= step.latest) {
|
|
506
|
+
step.latest = completed;
|
|
507
|
+
this.finish(step);
|
|
350
508
|
}
|
|
351
509
|
}
|
|
352
510
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const GENERATED_PRICE_SNAPSHOT: {
|
|
2
|
-
readonly verified: "2026-10-
|
|
2
|
+
readonly verified: "2026-10-14";
|
|
3
3
|
readonly source: "https://models.dev/api.json";
|
|
4
4
|
readonly entries: readonly [{
|
|
5
5
|
readonly providerID: "ai21";
|
|
@@ -1129,6 +1129,17 @@ export declare const GENERATED_PRICE_SNAPSHOT: {
|
|
|
1129
1129
|
};
|
|
1130
1130
|
}];
|
|
1131
1131
|
readonly released: "2026-09-02";
|
|
1132
|
+
}, {
|
|
1133
|
+
readonly providerID: "google";
|
|
1134
|
+
readonly id: "gemini-4-argon";
|
|
1135
|
+
readonly prices: readonly [{
|
|
1136
|
+
readonly input: 2;
|
|
1137
|
+
readonly output: 10;
|
|
1138
|
+
readonly cache: {
|
|
1139
|
+
readonly read: 0.1;
|
|
1140
|
+
};
|
|
1141
|
+
}];
|
|
1142
|
+
readonly released: "2026-10-13";
|
|
1132
1143
|
}, {
|
|
1133
1144
|
readonly providerID: "google";
|
|
1134
1145
|
readonly id: "gemini-flash-latest";
|
|
@@ -1250,6 +1261,17 @@ export declare const GENERATED_PRICE_SNAPSHOT: {
|
|
|
1250
1261
|
};
|
|
1251
1262
|
}];
|
|
1252
1263
|
readonly released: "2026-09-02";
|
|
1264
|
+
}, {
|
|
1265
|
+
readonly providerID: "meta";
|
|
1266
|
+
readonly id: "muse-wonder-1.0";
|
|
1267
|
+
readonly prices: readonly [{
|
|
1268
|
+
readonly input: 4;
|
|
1269
|
+
readonly output: 20;
|
|
1270
|
+
readonly cache: {
|
|
1271
|
+
readonly read: 0.2;
|
|
1272
|
+
};
|
|
1273
|
+
}];
|
|
1274
|
+
readonly released: "2026-10-12";
|
|
1253
1275
|
}, {
|
|
1254
1276
|
readonly providerID: "minimax";
|
|
1255
1277
|
readonly id: "minimax-m2";
|
package/dist/prices.generated.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const GENERATED_PRICE_SNAPSHOT = {
|
|
2
|
-
verified: "2026-10-
|
|
2
|
+
verified: "2026-10-14",
|
|
3
3
|
source: "https://models.dev/api.json",
|
|
4
4
|
entries: [
|
|
5
5
|
{ "providerID": "ai21", "id": "jamba-large", "prices": [{ "input": 2, "output": 8 }], "released": "2025-07-01" },
|
|
@@ -106,6 +106,7 @@ export const GENERATED_PRICE_SNAPSHOT = {
|
|
|
106
106
|
{ "providerID": "google", "id": "gemini-3.6-flash", "prices": [{ "input": 0.75, "output": 3.75, "cache": { "read": 0.075 } }], "released": "2026-07-21" },
|
|
107
107
|
{ "providerID": "google", "id": "gemini-3.7-flash", "prices": [{ "input": 0.75, "output": 3.75, "cache": { "read": 0.075 } }], "released": "2026-08-13" },
|
|
108
108
|
{ "providerID": "google", "id": "gemini-3.8-flash", "prices": [{ "input": 0.75, "output": 3.75, "cache": { "read": 0.075 } }], "released": "2026-09-02" },
|
|
109
|
+
{ "providerID": "google", "id": "gemini-4-argon", "prices": [{ "input": 2, "output": 10, "cache": { "read": 0.1 } }], "released": "2026-10-13" },
|
|
109
110
|
{ "providerID": "google", "id": "gemini-flash-latest", "prices": [{ "input": 0.75, "output": 3.75, "cache": { "read": 0.075 } }], "released": "2026-08-13" },
|
|
110
111
|
{ "providerID": "google", "id": "gemini-flash-lite-latest", "prices": [{ "input": 0.3, "output": 2.5, "cache": { "read": 0.03 } }], "released": "2026-07-21" },
|
|
111
112
|
{ "providerID": "inception", "id": "mercury-2", "prices": [{ "input": 0.25, "output": 0.75, "cache": { "read": 0.025 } }], "released": "2026-02-24" },
|
|
@@ -117,6 +118,7 @@ export const GENERATED_PRICE_SNAPSHOT = {
|
|
|
117
118
|
{ "providerID": "meta", "id": "muse-spark-1.2-contributor", "prices": [{ "input": 0.1, "output": 0.2, "cache": { "read": 0.002 } }], "released": "2026-08-05" },
|
|
118
119
|
{ "providerID": "meta", "id": "muse-spark-1.3", "prices": [{ "input": 1.25, "output": 4.25, "cache": { "read": 0.15 } }], "released": "2026-09-02" },
|
|
119
120
|
{ "providerID": "meta", "id": "muse-spark-1.3-contributor", "prices": [{ "input": 0.1, "output": 0.2, "cache": { "read": 0.002 } }], "released": "2026-09-02" },
|
|
121
|
+
{ "providerID": "meta", "id": "muse-wonder-1.0", "prices": [{ "input": 4, "output": 20, "cache": { "read": 0.2 } }], "released": "2026-10-12" },
|
|
120
122
|
{ "providerID": "minimax", "id": "minimax-m2", "prices": [{ "input": 0.3, "output": 1.2 }], "released": "2025-10-27" },
|
|
121
123
|
{ "providerID": "minimax", "id": "minimax-m2.1", "prices": [{ "input": 0.3, "output": 1.2, "cache": { "read": 0.03, "write": 0.375 } }], "released": "2025-12-23" },
|
|
122
124
|
{ "providerID": "minimax", "id": "minimax-m2.5", "prices": [{ "input": 0.3, "output": 1.2, "cache": { "read": 0.03, "write": 0.375 } }], "released": "2026-02-12" },
|
package/dist/pricing.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface OfficialPriceEntry {
|
|
|
14
14
|
/** Checked-in data: updating prices is explicit, not a network request at
|
|
15
15
|
* startup, during a refresh, or during an npm build. */
|
|
16
16
|
export declare const OFFICIAL_PRICE_SNAPSHOT: {
|
|
17
|
-
verified: "2026-10-
|
|
17
|
+
verified: "2026-10-14";
|
|
18
18
|
source: "https://models.dev/api.json";
|
|
19
19
|
entries: OfficialPriceEntry[];
|
|
20
20
|
};
|
package/dist/runtime.d.ts
CHANGED
|
@@ -15,16 +15,23 @@ export interface PreparedRuntime {
|
|
|
15
15
|
milliseconds: number;
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
+
/** Native session metadata from the existing session read; omitted metadata is unknown. */
|
|
19
|
+
export interface RuntimeTime {
|
|
20
|
+
created: number;
|
|
21
|
+
idle?: number;
|
|
22
|
+
}
|
|
18
23
|
/**
|
|
19
24
|
* OpenCode 2.0.26's TUI turnDuration/inputIndex: completed assistant time minus
|
|
20
25
|
* the first user/synthetic input after the previous idle marker, or the nearest
|
|
21
|
-
* input in legacy histories with no idle markers.
|
|
22
|
-
*
|
|
26
|
+
* input in legacy histories with no idle markers. Native session metadata with
|
|
27
|
+
* no previous idle identifies the first turn, even before its first marker.
|
|
28
|
+
* With no input it falls back to the assistant's creation time.
|
|
29
|
+
* Take only the final response in each turn:
|
|
23
30
|
* intermediate footer durations overlap and must never be added separately.
|
|
24
31
|
* No execution-log API, client clock, child history, or additional reads.
|
|
25
32
|
*/
|
|
26
|
-
export declare function summarizeRuntime(messages: readonly UsageMessage[], fork?: boolean): RuntimeSummary;
|
|
33
|
+
export declare function summarizeRuntime(messages: readonly UsageMessage[], fork?: boolean, time?: RuntimeTime): RuntimeSummary;
|
|
27
34
|
/** Scan history once per snapshot, keeping the current turn's overlapping subtotal separate. */
|
|
28
|
-
export declare function prepareRuntime(messages: readonly UsageMessage[], fork?: boolean): PreparedRuntime;
|
|
35
|
+
export declare function prepareRuntime(messages: readonly UsageMessage[], fork?: boolean, time?: RuntimeTime): PreparedRuntime;
|
|
29
36
|
/** Replace, rather than add to, an already-counted footer in the same steered turn. */
|
|
30
37
|
export declare function projectRuntime(prepared: PreparedRuntime, now: number | undefined): RuntimeSummary;
|
package/dist/runtime.js
CHANGED
|
@@ -3,18 +3,24 @@ const timestamp = (value) => typeof value === "number" && value >= 0
|
|
|
3
3
|
/**
|
|
4
4
|
* OpenCode 2.0.26's TUI turnDuration/inputIndex: completed assistant time minus
|
|
5
5
|
* the first user/synthetic input after the previous idle marker, or the nearest
|
|
6
|
-
* input in legacy histories with no idle markers.
|
|
7
|
-
*
|
|
6
|
+
* input in legacy histories with no idle markers. Native session metadata with
|
|
7
|
+
* no previous idle identifies the first turn, even before its first marker.
|
|
8
|
+
* With no input it falls back to the assistant's creation time.
|
|
9
|
+
* Take only the final response in each turn:
|
|
8
10
|
* intermediate footer durations overlap and must never be added separately.
|
|
9
11
|
* No execution-log API, client clock, child history, or additional reads.
|
|
10
12
|
*/
|
|
11
|
-
export function summarizeRuntime(messages, fork = false) {
|
|
12
|
-
return prepareRuntime(messages, fork).summary;
|
|
13
|
+
export function summarizeRuntime(messages, fork = false, time) {
|
|
14
|
+
return prepareRuntime(messages, fork, time).summary;
|
|
13
15
|
}
|
|
14
16
|
/** Scan history once per snapshot, keeping the current turn's overlapping subtotal separate. */
|
|
15
|
-
export function prepareRuntime(messages, fork = false) {
|
|
17
|
+
export function prepareRuntime(messages, fork = false, time) {
|
|
18
|
+
// A new session has not recorded an idle yet. Merely missing a message marker
|
|
19
|
+
// must not turn synthetic deliveries/steers into new turns during that run.
|
|
20
|
+
// Unknown metadata retains the native legacy fallback, as does a past idle.
|
|
21
|
+
const firstTurn = timestamp(time?.created) && time?.idle === undefined;
|
|
16
22
|
// Even a copied marker identifies the host's turn semantics for an open fork.
|
|
17
|
-
const legacy = !messages.some(message => message.type === "idle");
|
|
23
|
+
const legacy = !firstTurn && !messages.some(message => message.type === "idle");
|
|
18
24
|
// Fork copies belong to the original session, including copied turn boundaries.
|
|
19
25
|
const history = fork ? messages.filter(message => !/_\d+$/.test(message.id)) : messages;
|
|
20
26
|
let milliseconds = 0, unavailable = false;
|
package/dist/source.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { OpenCodeClient, SessionInfo } from "@opencode/client";
|
|
2
2
|
import type { ContextSources } from "./context-sources.js";
|
|
3
|
+
import type { ReadActiveSessions } from "./performance.js";
|
|
3
4
|
import type { PriceCatalog, UsageMessage } from "./usage.js";
|
|
4
|
-
export type Session = Pick<SessionInfo, "id" | "parentID" | "fork" | "model" | "location" | "title" | "revert"
|
|
5
|
+
export type Session = Pick<SessionInfo, "id" | "parentID" | "fork" | "model" | "location" | "title" | "revert"> & Partial<Pick<SessionInfo, "time">>;
|
|
5
6
|
export interface Page<T> {
|
|
6
7
|
data: T[];
|
|
7
8
|
cursor: {
|
|
@@ -21,6 +22,7 @@ export interface UsageSource {
|
|
|
21
22
|
messages(id: string, cursor: string | undefined, signal: AbortSignal): Promise<Page<UsageMessage>>;
|
|
22
23
|
model(session: Session, signal: AbortSignal): Promise<ActiveModel>;
|
|
23
24
|
composition?(session: Session, signal: AbortSignal): Promise<ContextSources | undefined>;
|
|
25
|
+
active?: ReadActiveSessions;
|
|
24
26
|
}
|
|
25
27
|
export declare function createSource(client: OpenCodeClient): UsageSource;
|
|
26
28
|
export declare function pages<T>(read: (cursor?: string) => Promise<Page<T>>, signal: AbortSignal): Promise<T[]>;
|
package/dist/source.js
CHANGED
|
@@ -3,6 +3,16 @@ import { parseContextSources } from "./context-sources.js";
|
|
|
3
3
|
import { modelKey } from "./usage.js";
|
|
4
4
|
export function createSource(client) {
|
|
5
5
|
return {
|
|
6
|
+
async active(signal) {
|
|
7
|
+
// V2 returns the complete process-wide foreground-drain set, already unwrapped
|
|
8
|
+
// by the Promise client. Cached TUI status can still be stale after reconnect.
|
|
9
|
+
const active = await client.session.active({ signal });
|
|
10
|
+
if (!active || typeof active !== "object" || Array.isArray(active)
|
|
11
|
+
|| Object.values(active).some(value => !value || value.type !== "running")) {
|
|
12
|
+
throw new Error("Invalid active session snapshot");
|
|
13
|
+
}
|
|
14
|
+
return new Set(Object.keys(active));
|
|
15
|
+
},
|
|
6
16
|
session: (sessionID, signal) => client.session.get({ sessionID }, { signal }),
|
|
7
17
|
children: (parentID, cursor, signal) => client.session.list(cursor ? { cursor } : { parentID, order: "asc", limit: 100 }, { signal }),
|
|
8
18
|
// Unlike session.context(), message.list() includes pre-compaction history.
|
package/dist/tui.js
CHANGED
|
@@ -960,7 +960,9 @@ export default Plugin.define({
|
|
|
960
960
|
setup(context) {
|
|
961
961
|
const performance = new PerformanceMonitor(listener => context.data.listen(({
|
|
962
962
|
details
|
|
963
|
-
}) => listener(details))
|
|
963
|
+
}) => listener(details)), {
|
|
964
|
+
active: createSource(context.client).active
|
|
965
|
+
});
|
|
964
966
|
const runtime = new RuntimeMonitor(listener => context.data.listen(({
|
|
965
967
|
details
|
|
966
968
|
}) => listener(details)));
|
package/docs/verification.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# 验证记录
|
|
2
2
|
|
|
3
|
+
## 2026-10-14 · 首轮 Run Time 起点重置与重连归零(未发布)
|
|
4
|
+
|
|
5
|
+
无 idle 消息并不一定是旧历史:新会话的第一轮在执行结束前也没有该标记。此前每次 synthetic/steer 输入都被当成 legacy 轮次边界,工具延续时丢失首轮之前已经过去的时间;重新读取历史或重建视图可以重新暴露该少算。匿名固定夹具复现原先 `~1m 56s` 与正确 `~18m 57s` 的差异,不依赖屏幕操作。
|
|
6
|
+
|
|
7
|
+
`src/runtime.ts` 接收既有会话读取的原生时间元数据:有效 `time.created` 且尚未设置 `time.idle` 时,首轮使用首个 user/synthetic 输入,即使尚未产生第一条 idle 消息。工具、retry 与 steer 保持同轮,完成页脚替换重叠时长;首次 idle 前后的 native 小计一致。已有原生 idle 时间(包括零)或元数据未知的无标记旧历史仍使用最近输入。会话创建时间只用于区分边界语义,不代替缺失的输入时间、不会按会话年龄计时。`src/source.ts` 保留原有 `session.get()` 返回的时间字段,控制器复用它,没有新增请求、周期读取或持久化。
|
|
8
|
+
|
|
9
|
+
另修复 `src/controller.ts` 的重连窗口:历史读取可以早于可选 clock RPC 返回,不能把同一仍保留的开放轮次的可信 `~` 值替换成原生零小计。只有起点一致、此前 assistant 仍在历史中且不含当前轮的历史小计未变时保留冻结显示;等待可信锚点期间停止 ticker。完成/中断、回退删除及替换历史仍使用新历史的权威数值,允许合法向下校正,不使用最大值锁掩盖差异。
|
|
10
|
+
|
|
11
|
+
已核对 V2 插件/CLI/Client/API 文档、SDK 2.0.24 的 `SessionInfo.time`,以及版本固定的宿主 [`2.0.9 projector`](https://github.com/anomalyco/opencode/blob/v2.0.9/packages/core/src/session/projector.ts)、[`2.0.28 projector`](https://github.com/anomalyco/opencode/blob/v2.0.28/packages/core/src/session/projector.ts) 与 [`2.0.28 info`](https://github.com/anomalyco/opencode/blob/v2.0.28/packages/core/src/session/info.ts):新建/分叉不继承 `time_idle`,原生执行结束记录它;未设置时 API 不输出 `time.idle`。2.0.28 的 `turnDuration`/`inputIndex`、分叉副本 ID、完成 compaction 与升序历史语义已复核;首轮的元数据修正避免其首次 idle 前临时误用 legacy 回退。
|
|
12
|
+
|
|
13
|
+
环境:macOS、Node.js v22.23.2、npm 10.9.8,插件包 0.4.9、SDK 2.0.24、OpenTUI 0.5.14、真实 OpenCode 2.0.28。此前 2.0.11/2.0.22/2.0.24/2.0.26 的验证记录保留,不宣称这些宿主本次全部重跑。
|
|
14
|
+
|
|
15
|
+
| 检查 | 结果 |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `npm run typecheck` | 通过 |
|
|
18
|
+
| `npm test` | 247 项通过,新增 13 项首轮、恢复及适配器回归测试 |
|
|
19
|
+
| `npm run build` | 通过 |
|
|
20
|
+
| `node --check scripts/smoke.mjs`、`git diff --check` | 通过 |
|
|
21
|
+
| `npm run test:smoke` | 真实 OpenCode 2.0.28 打包/隔离安装的完整检查通过,含首轮延续场景 |
|
|
22
|
+
|
|
23
|
+
自动化测试覆盖多次 synthetic 投递造成的约 17 分钟少算、首次 idle 前后完成结算、首轮 steer/retry 页脚替换、无 marker 的分叉、旧历史最近输入、缺失/无效消息时间、同一视图刷新、sidebar/详情同时重建、暂停回调后一次补齐两分钟、慢校时期间冻结及恢复、新监视器的一次 clock 读取,以及历史替换/小计变化不能继承旧冻结值。暂停、重连和慢 RPC 由注入的事件、单调时钟与可控 Promise 验证,不是实际断网或操作系统暂停。
|
|
24
|
+
|
|
25
|
+
新增真实宿主首轮场景:本地模拟提供商先返回 subagent 工具调用,两个原生 synthetic inbox 项在同一轮投递,继续输出保持门控等待。确认消息中没有 idle、原生 `time.idle` 也未设置;打包算法与侧边栏均从最初输入计时,完成之前 native 小计保持零。可见耗时为投递前 `~4.4s`、投递后 `~7.2s`、新 TUI 恢复 `~8.7s`、终端失焦/恢复 `~9.2s`、紧凑/详细 `/usage` `~9.4s`,各实时值与服务端 clock 减原始输入相符。结束后两个 TUI 均恢复原生 9,564ms;原有上下文、定价、TPS/TTFT、Responses、子代理、分叉及 compaction 的完整检查也通过。模拟提供商只在本地,不调用付费模型、不修改现有 OpenCode 配置。
|
|
26
|
+
|
|
27
|
+
验证产物:`/private/var/folders/rw/bmx6c8hd737brl55m0_ff_b80000gn/T/opencode/token-usage-smoke-DIvmzk`。`13-first-turn-*.txt`/`.ansi` 保存首轮各入口,`result.json` 的 `firstTurnRuntime` 保存最初输入、投递项、实测参考与显示值及最终 native 时长。2.0.28 已加入实际验证宿主记录;Node 22 的既有 OpenTUI 引擎警告与 MockTimers 实验警告保留。
|
|
28
|
+
|
|
29
|
+
用户报告的 **macOS 物理多屏切换** 尚未实机复验:上述终端焦点事件、新 TUI 恢复和模拟重连不代表已确认物理切屏是原因,也不代表已验证 App Nap 或睡眠/唤醒。中英文 README、产品约定与路线图已同步;版本、SDK 与价格快照未变,未升级版本、提交、推送或发布。
|
|
30
|
+
|
|
31
|
+
## 2026-10-11 · 性能监视器异常流回收(未发布)
|
|
32
|
+
|
|
33
|
+
`PerformanceMonitor` 在 `server.connected` 后重置近期 TPS 窗口,并通过 V2 `session.active()` 对账全部已跟踪的未结束流,包括离屏会话。未结束流静默 60 秒后进行共享活动检查,每次请求覆盖全部未结束流;确认 inactive 后保留 60 秒,再释放流、去重事件集合和 TTFT 观察。没有未结束流时停止活动查询,增量及 2 秒 TPS 窗口过期仍使用内存,不读取历史。状态请求有 5 秒超时;失败保留并重试,请求身份及逐流 revision 防止迟到响应覆盖新活动、重连及销毁。
|
|
34
|
+
|
|
35
|
+
历史 tokens/`time.streamed` 只表示提供商已交付,不能证明工具已完成,不再单独触发 TTFT/流状态回收。有效历史 `time.completed`、step/execution 结束或后继 step 可以结算,重复结束不延长保留期;旧结束/重试不能影响新 step,重试可撤销此前失败的回收。回收与静默检查都用本地单调时钟。服务端仍 running 的长工具等待、首 token 前等待和重试没有硬性寿命上限。
|
|
36
|
+
|
|
37
|
+
环境:macOS、Node.js v22.23.2、npm 10.9.8,插件包 0.4.9、SDK 2.0.24、OpenTUI 0.5.14。已复核 V2 插件、CLI 插件与官方 OpenAPI 文档,以及已安装 SDK 的 `session.active()` Promise 解包形状、execution/step/retry/tool 事件;活动集合覆盖本进程的前台执行,缺席才表示 inactive,读取异常不能代替空集合。
|
|
38
|
+
|
|
39
|
+
| 检查 | 结果 |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `npm run typecheck` | 通过 |
|
|
42
|
+
| `npm test` | 234 项通过,新增 29 项生命周期、控制器及适配器回归测试 |
|
|
43
|
+
| `npm run build` | 通过 |
|
|
44
|
+
| `node --check scripts/smoke.mjs`、`git diff --check` | 通过 |
|
|
45
|
+
| `npm run test:smoke` | 真实 OpenCode 2.0.26 打包/隔离安装的完整复验通过,包含新增异常流场景 |
|
|
46
|
+
|
|
47
|
+
自动化测试覆盖无视图/无结束事件/无后续历史的自主回收、首 token 缺失、session 结束兜底、连续 step 替代、长工具等待及 streamed-only 历史、长重试、跨树重连、交错会话的共享查询、失败/超时重试、新 delta/工具进度/retry/execution 活动与慢查询竞态、重复重连及取消失效、旧状态响应、重复结束不续期、单调时钟、删除/销毁与空监视器停止查询。关闭全部控制器后回收离屏流,再打开对应会话不会恢复过期 TTFT。
|
|
48
|
+
|
|
49
|
+
首轮真实 OpenCode 2.0.26 的原有整套检查通过,新增场景因测试要求父会话必须有实时 TTFT 而超时。事件记录确认宿主可以直接交付完整工具参数、没有 `session.tool.input.delta`,父会话因此没有可测实时 TTFT;其流状态仍应保留。测试已改为同时确认父/子流存在,并保留实际可测的子会话 TTFT,不伪造父会话样本。首轮记录:`/private/var/folders/rw/bmx6c8hd737brl55m0_ff_b80000gn/T/opencode/token-usage-smoke-nvGQ95`。
|
|
50
|
+
|
|
51
|
+
完整复验使用真实宿主执行 subagent 工具、由本地模拟提供商暂缓子会话输出;独立的打包监视器没有挂载视图,也没有接收历史对账。测试过滤其 step/execution 结束事件,注入与宿主相同的无 `created` 重连握手,并将该监视器的单调时钟加速至 1 小时、累计 25 小时。两次真实活动查询都包含父/子会话,两条流一直保留,子会话 TTFT 始终为 418ms;结束后第三次活动集合为空,监视器回收全部流与观察并停止定时任务。此处验证的是加速年龄及注入的事件丢失/重连路径,不是实际等待 25 小时或切断真实网络连接。
|
|
52
|
+
|
|
53
|
+
通过记录:`/private/var/folders/rw/bmx6c8hd737brl55m0_ff_b80000gn/T/opencode/token-usage-smoke-WOFN5d`。`result.json` 的 `performanceLifecycle` 保存会话、90,000,000ms 加速年龄、可测 TTFT 与三次真实活动集合;`stream-events.json` 保存包括被过滤结束事件在内的宿主事件。原有侧边栏、详情、子代理、耗时、TPS 突发/过期、分叉和压缩检查全部通过。中英文 README、产品约定及路线图已同步;插件版本仍为 0.4.9,未升级版本、提交、推送或发布。
|
|
54
|
+
|
|
55
|
+
## 2026-10-11 · 构建清理与打包文件一致性
|
|
56
|
+
|
|
57
|
+
`scripts/build.mjs` 在 TypeScript 编译前递归清理 `dist`,目录不存在时也可构建;`npm pack` 的既有 `prepack` 钩子复用该流程。`scripts/smoke.mjs` 的打包断言改为从当前 `src` 推导完整 `.js`/`.d.ts` 清单,精确比对包内 `dist` 文件,统一检测残留、缺失以及未清理的 JSX 中间产物。
|
|
58
|
+
|
|
59
|
+
环境:macOS、Node.js v22.23.2、npm 10.9.8,插件包 0.4.9。本次执行本地构建与打包专项校验。
|
|
60
|
+
|
|
61
|
+
| 检查 | 结果 |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `npm run typecheck`、`npm test` | 通过,205 项测试全部通过 |
|
|
64
|
+
| `npm run build` | 通过,原有孤儿 `dist/popover.js`/`dist/popover.d.ts` 已清理 |
|
|
65
|
+
| 实际 `npm pack --json` | 预置孤儿 JS、声明及嵌套残留后,`prepack` 全部清理;16 个源码模块对应 32 个 `dist` 文件,tarball 共 41 个文件,归档实际清单与 npm 清单一致 |
|
|
66
|
+
| 重复打包 | 连续两次完整 `prepack`/打包的 SHA-512 integrity 一致 |
|
|
67
|
+
| `node --check scripts/smoke.mjs` | 通过 |
|
|
68
|
+
|
|
69
|
+
打包验证产物:`/private/var/folders/rw/bmx6c8hd737brl55m0_ff_b80000gn/T/opencode/token-usage-pack-EJkcwj/chenlongapps-opencode-token-usage-0.4.9.tgz`。
|
|
70
|
+
|
|
3
71
|
## 2026-10-10 · 移除 Run Time 运行状态动画(未发布)
|
|
4
72
|
|
|
5
73
|
侧边栏、子代理摘要/完整浮窗以及 `/usage` 紧凑/详细模式直接显示耗时,不再附加 `Running.`/`Running..`/`Running...`。移除 `src/running.ts`、动画专用计时器及其 5 项测试,清理仅供动画使用的会话 ID 传递;各入口的原生状态仍用于控制 `~` 实时估算,共享 500ms 耗时 ticker 与原生累计算法未改动。零值隐藏、不可用/未更新标记及面板末行顺序保持不变。
|