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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions changes/1908-usage-partial-total.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
section: Fixed
---

## en

- The Overview's token usage no longer shows "Unknown" for the whole week when some usage went unreported, for example rows kept from before an upgrade. The total now counts what was reported and is marked with **+**, and each day's bar shows its reported usage ([#1908](https://github.com/BotHarness/DeepSeekBot/pull/1908)).

## zh

- 总览的 token 用量不再因为部分用量未上报(例如升级前保留的旧记录)而整周显示“未知”。总量现在只计入已上报的部分并标记 **+**,每天的柱子也显示已上报的用量([#1908](https://github.com/BotHarness/DeepSeekBot/pull/1908))。
2 changes: 1 addition & 1 deletion docs/architecture/botharness-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ application-defined `botharness/channelActivityToday` 查询由 Channel owner

总览优先排列需 Human 行动的 Bot,并先展示 canonical 行动表单,再显示执行中的 Session。显式全部已读命令先固定每个 Human 可见 Channel 的消息位置,再推进既有已读游标;之后到达的消息仍未读,不改变请求解决、Inbox 移除或 Bot attention。统计复用锁定版本的 TanStack 图表与主题,以前端偏好记忆折叠状态,并保留可访问的发送者明细。 ([#705](https://github.com/BotHarness/BotHarness/issues/705)).

总览 token 统计(#709)通过应用定义的 `overviewUsage(period, after?)` Typert 查询消费既有保留 Usage 权威。今日或含今日的七个 Host 本地日期返回完整整体总量/每日分项,与有界 Bot 明细分页独立;缺失报告保持可空未知,核对和历史基线状态明确。当前 Registry 名称标记行;不在当前 Bot 列表中的保留统计仍可见,但不虚构身份或 Profile 入口。Client 复用 Profile UsageChart/主题,以可清理的轮询读取已结算事实,并通过既有 DM 导航打开当前 Bot Profile。不增加账本、Channel token 归因或模型/wake 策略。
总览 token 统计(#709)通过应用定义的 `overviewUsage(period, after?)` Typert 查询消费既有保留 Usage 权威。今日或含今日的七个 Host 本地日期返回完整整体总量/每日分项,与有界 Bot 明细分页独立;缺失报告不再让整段合计变成未知:每个桶返回已上报部分的合计,只有完全没有上报值的桶才是空;总量缺失的行用其已上报分项作下限,并以 `partial` 标记该合计不完整,Web 与手机显示为“N+”;核对和历史基线状态明确。当前 Registry 名称标记行;不在当前 Bot 列表中的保留统计仍可见,但不虚构身份或 Profile 入口。Client 复用 Profile UsageChart/主题,以可清理的轮询读取已结算事实,并通过既有 DM 导航打开当前 Bot Profile。不增加账本、Channel token 归因或模型/wake 策略。

总览 Memory 统计(#716)通过应用定义的 `overviewMemory(after?)` Typert 查询读取现有 Memory Service 与 Registry。每页至多十个当前 Bot;每个仓库的所有非恢复/stash 引用可达的普通 Git 提交按 committer 时间归入含今日的七个 Host 本地日期,共享提交仅计一次。当前 staged/unstaged/untracked 状态单独显示,不推断文件作者或审批需求;缺失、无效、超时或读取失败的仓库显示不可用,不伪装为零提交或 clean。仓库 Git 查询有超时并异步执行,避免阻塞共享 Host;查询关闭可选 Git 锁与 fsmonitor,不 stage、commit、reconcile 或建立 checkpoint;沿用 ADR-0068 的仓库权威,不增持久统计账本。Client 复用 Profile 紧凑卡片及 TanStack 主题;可展开每日值,保留已加载分页刷新,折叠 Statistics/退出视图时清理查询资源。

Expand Down
2 changes: 1 addition & 1 deletion docs/bot-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ deepseekbot group-activity <group-channel>

`model-catalog` lists the models the Host can run and the Profile default, as the model pickers do. `model-plan-set` replaces a Bot's whole plan with stdin `{"orchestrator":{...},"assignmentDefault":{...},"assignmentModels":[...]}`; `model-plan-assignments-set` takes the same object without `orchestrator`. A route is `{"provider","model","reasoningEffort"?}` and an Assignment model is `{"provider","model","allowedEfforts":[...],"defaultEffort","note"?}`. `model-plan-customize` changes only the Orchestrator of a Bot that already has a plan. `model-plan-inherit` drops the Bot's own model so it follows the Profile default. `model-preset-update` changes a preset's name or routes; flags you leave out keep the current values, and an optional `--plan-stdin` `{"assignmentModels":[...]}` replaces its Assignment models. The Host validates every route against its catalog. Plan and preset writes check a revision: `--expected-revision` makes that check explicit; without it the command reads the current revision first, like `memory-save` does with `--expected-head`. A plan changed meanwhile fails with the Host's `invalid-input`.

`overview-usage` (`--period today|week`, default `today`) and `overview-memory` return the Overview page's per-Bot rows; pass `nextCursor` back as `--after` for more. `activity-overview` shows what every Bot is doing and `channel-activity-today` today's messages per Channel. The `bot-*` readers address a Bot by id and read the Profile panels through its DM: `bot-usage` needs a non-future `--start`/`--end` range shorter than 182 days, `bot-session-usage` groups usage by Session and Assignment, and `bot-activity-history` returns the activity calendar (`--weeks` 1–104). `group-activity` takes a group Channel id.
`overview-usage` (`--period today|week`, default `today`) and `overview-memory` return the Overview page's per-Bot rows; pass `nextCursor` back as `--after` for more. In `overview-usage`, each bucket is the sum of what providers reported and is `null` only when nothing in it was reported; `partial: true` on the totals, a day or a Bot means some usage in it went unreported, so its `totalTokens` is a lower bound. `activity-overview` shows what every Bot is doing and `channel-activity-today` today's messages per Channel. The `bot-*` readers address a Bot by id and read the Profile panels through its DM: `bot-usage` needs a non-future `--start`/`--end` range shorter than 182 days, `bot-session-usage` groups usage by Session and Assignment, and `bot-activity-history` returns the activity calendar (`--weeks` 1–104). `group-activity` takes a group Channel id.

### Marketplace, releases, usage statistics, Git and pairing review

Expand Down
3 changes: 2 additions & 1 deletion packages/client/src/client/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5296,7 +5296,8 @@ export async function loadOverviewUsage(
row !== undefined &&
['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'totalTokens'].every(
(key) => row[key] === null || (Number.isSafeInteger(row[key]) && Number(row[key]) >= 0),
)
) &&
(row['partial'] === undefined || typeof row['partial'] === 'boolean')
);
};
if (
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/client/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ export const zh = {
'overviewUsage.reconciling': '正在核对用量,数据尚未完整。',
'overviewUsage.degraded': '用量核对失败,当前统计可能不完整。',
'overviewUsage.legacy': '历史基线尚未完全归类。',
'overviewUsage.partial': '部分 token 桶或总量未知;图表仅绘制已确认的用量。',
'overviewUsage.partial': '部分用量未上报;带 + 的总量和图表只计入已上报的用量。',
'overviewUsage.trend': '每日 token 用量趋势',
'overviewUsage.daily': '每日精确值',
'overviewUsage.bots': '按 Bot',
Expand Down Expand Up @@ -4173,7 +4173,7 @@ export const en = {
'overviewUsage.degraded': 'Usage reconciliation failed; statistics may be incomplete.',
'overviewUsage.legacy': 'Historical baseline is not fully classified.',
'overviewUsage.partial':
'Some token buckets or totals are unknown; the chart shows only confirmed usage.',
'Some usage was not reported; totals marked + and the chart count only reported usage.',
'overviewUsage.trend': 'Daily token usage trend',
'overviewUsage.daily': 'Exact daily values',
'overviewUsage.bots': 'By Bot',
Expand Down
9 changes: 7 additions & 2 deletions packages/client/src/client/overview-usage-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,20 @@ export function OverviewUsageView({
</span>
<strong className="bh-profile-card-total" aria-live="polite">
{t('overviewUsage.total')}:{' '}
<span data-usage-total>{count(value.totals.totalTokens)}</span> tokens
<span data-usage-total>
{count(value.totals.totalTokens)}
{value.totals.partial && value.totals.totalTokens !== null ? '+' : ''}
</span>{' '}
tokens
</strong>
{value.freshness !== 'ready' ? (
<p className="bh-note" role="status">
{t(`overviewUsage.${value.freshness}`)}
</p>
) : null}
{value.legacyBaseline ? <p className="bh-note">{t('overviewUsage.legacy')}</p> : null}
{value.days.some((row) => Object.values(row).some((amount) => amount === null)) ? (
{value.totals.partial ||
value.days.some((row) => Object.values(row).some((amount) => amount === null)) ? (
<p className="bh-note">{t('overviewUsage.partial')}</p>
) : null}
{rows.some((row) => row.totalTokens !== null && row.totalTokens > 0) ? (
Expand Down
32 changes: 32 additions & 0 deletions packages/client/test/overview-usage-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function result(period: 'today' | 'week', total = 155): OverviewUsage {
cacheReadTokens: 0,
cacheWriteTokens: 0,
totalTokens: total,
partial: false,
};
return {
period,
Expand Down Expand Up @@ -181,6 +182,37 @@ it('drops a delayed Today refresh after changing range and surfaces unavailable
container.remove();
}
});
it('marks a total that leaves out unreported usage as a lower bound', async () => {
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
const value = result('today');
value.totals.partial = true;
const call: BridgeCall = async () => ({ ok: true, value });
const container = document.createElement('div');
document.body.append(container);
const root = createRoot(container);
try {
await act(async () =>
root.render(
createElement(OverviewUsageView, {
actions: createActions(call, createStore()),
t: zhTranslate,
}),
),
);
expect(container.querySelector('[data-usage-total]')?.textContent).toBe('155+');
expect(container.textContent).toContain(zhTranslate('overviewUsage.partial'));
} finally {
await act(async () => root.unmount());
container.remove();
}
await expect(
loadOverviewUsage(
async () => ({ ok: true, value: { ...value, totals: { ...value.totals, partial: 'yes' } } }),
'today',
),
).rejects.toThrow('invalid Overview usage');
});

it('preserves unknown totals and rejects malformed usage instead of reporting zero', async () => {
const value = result('today');
value.totals.totalTokens = null;
Expand Down
45 changes: 30 additions & 15 deletions packages/core/src/usage/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface UsageOverviewBuckets {
cacheReadTokens: number | null;
cacheWriteTokens: number | null;
totalTokens: number | null;
partial: boolean;
}
export interface UsageOverviewResult {
period: UsageOverviewPeriod;
Expand All @@ -27,23 +28,33 @@ function dayOf(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
const columns = [
['input_tokens', 'unknown_input', 'inputTokens'],
['output_tokens', 'unknown_output', 'outputTokens'],
['cache_read_tokens', 'unknown_cache_read', 'cacheReadTokens'],
['cache_write_tokens', 'unknown_cache_write', 'cacheWriteTokens'],
['total_tokens', 'unknown_total', 'totalTokens'],
]
.map(
...[
['input_tokens', 'unknown_input', 'inputTokens'],
['output_tokens', 'unknown_output', 'outputTokens'],
['cache_read_tokens', 'unknown_cache_read', 'cacheReadTokens'],
['cache_write_tokens', 'unknown_cache_write', 'cacheWriteTokens'],
[
'CASE WHEN unknown_total > 0 THEN MAX(total_tokens, input_tokens + output_tokens + cache_read_tokens + cache_write_tokens) ELSE total_tokens END',
'unknown_total',
'totalTokens',
],
].map(
([value, unknown, alias]) =>
`CASE WHEN SUM(${unknown}) > 0 THEN NULL ELSE COALESCE(SUM(${value}), 0) END AS ${alias}`,
)
.join(', ');
`CASE WHEN COALESCE(SUM(${value}), 0) = 0 AND SUM(${unknown}) > 0 THEN NULL ELSE COALESCE(SUM(${value}), 0) END AS ${alias}`,
),
'COALESCE(SUM(unknown_total), 0) > 0 AS partial',
].join(', ');
type Row = Omit<UsageOverviewBuckets, 'partial'> & { partial: number };
function bucketsOf<T extends Row>(row: T): Omit<T, 'partial'> & { partial: boolean } {
return { ...row, partial: row.partial > 0 };
}
const empty: UsageOverviewBuckets = {
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
totalTokens: 0,
partial: false,
};
export function queryOverviewUsage(
db: DatabaseSync,
Expand All @@ -61,19 +72,23 @@ export function queryOverviewUsage(
date.setDate(date.getDate() + 1);
} while (days.at(-1) !== end);
const start = days[0]!;
const totals = db
.prepare(`SELECT ${columns} FROM usage_daily WHERE day >= ? AND day <= ?`)
.get(start, end) as unknown as UsageOverviewBuckets;
const totals = bucketsOf(
db
.prepare(`SELECT ${columns} FROM usage_daily WHERE day >= ? AND day <= ?`)
.get(start, end) as unknown as Row,
);
const daily = db
.prepare(
`SELECT day, ${columns} FROM usage_daily WHERE day >= ? AND day <= ? GROUP BY day ORDER BY day`,
)
.all(start, end) as unknown as UsageOverviewResult['days'];
.all(start, end)
.map((row) => bucketsOf(row as unknown as Row & { day: string }));
const bots = db
.prepare(
`SELECT bot_slug AS slug, ${columns} FROM usage_daily WHERE day >= ? AND day <= ? AND bot_slug > ? GROUP BY bot_slug ORDER BY bot_slug LIMIT 21`,
)
.all(start, end, after ?? '') as unknown as UsageOverviewResult['bots'];
.all(start, end, after ?? '')
.map((row) => bucketsOf(row as unknown as Row & { slug: string }));
return {
period,
start,
Expand Down
31 changes: 30 additions & 1 deletion packages/core/test/overview-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { expect, it, vi } from 'vitest';

import { createBridgeMethods } from '../src/bridge/methods.js';
import { createChannelStore } from '../src/channels/store.js';
import { mountOperationalDatabase } from '../src/database/owner.js';
import { attachOperationalModule, mountOperationalDatabase } from '../src/database/owner.js';
import { BOT_HARNESS_SCHEMA_PLAN } from '../src/database/schema-plan.js';
import { createBotStateTracker } from '../src/state/bot-state.js';
import { createUsageProjection, usageLocalDay } from '../src/usage/usage.js';
Expand Down Expand Up @@ -281,6 +281,35 @@ it('distinguishes a ready empty interval from missing provider usage and an unav
expect(unknown.ok && unknown.value.days[0]?.inputTokens).toBeNull();
});

it('counts reported usage beside an unreported total and marks the sum as partial', () => {
const { methods, fold, owner, time } = setup();
fold({ inputTokens: 100, outputTokens: 40, totalTokens: 155 });
attachOperationalModule(owner, 'usage').transaction((db) =>
db
.prepare(
`INSERT INTO usage_daily
(bot_slug, day, provider, model, purpose, input_tokens, output_tokens, cache_read_tokens,
cache_write_tokens, total_tokens, unknown_cache_read, unknown_cache_write, unknown_total)
VALUES ('legacy', ?, 'actual', 'old', 'orchestrator', 300, 60, 0, 0, 0, 1, 1, 1)`,
)
.run(usageLocalDay(time)),
);
const result = methods.overviewUsage({ period: 'week' });
if (!result.ok) throw new Error(result.error.message);
expect(result.value.totals).toMatchObject({
inputTokens: 400,
outputTokens: 100,
cacheReadTokens: null,
totalTokens: 515,
partial: true,
});
expect(result.value.days.at(-1)).toMatchObject({ totalTokens: 515, partial: true });
expect(result.value.days[0]).toMatchObject({ totalTokens: 0, partial: false });
expect(
Object.fromEntries(result.value.bots.map((bot) => [bot.slug, [bot.totalTokens, bot.partial]])),
).toEqual({ ada: [155, false], legacy: [360, true] });
});

it('uses seven local calendar dates across a daylight-saving change', () => {
vi.stubEnv('TZ', 'America/New_York');
try {
Expand Down
Loading