@tokentop/ttop 0.2.2 → 0.4.0
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 +2 -5
- package/package.json +8 -5
- package/src/agents/aggregator.ts +39 -19
- package/src/agents/costing.ts +11 -9
- package/src/agents/index.ts +7 -9
- package/src/agents/types.ts +2 -2
- package/src/cli.ts +28 -23
- package/src/config/schema.test.ts +42 -40
- package/src/config/schema.ts +26 -26
- package/src/demo/simulator.ts +161 -122
- package/src/plugins/agents/index.ts +2 -2
- package/src/plugins/auth-sources.ts +42 -38
- package/src/plugins/lifecycle.ts +42 -38
- package/src/plugins/loader.test.ts +49 -49
- package/src/plugins/loader.ts +67 -63
- package/src/plugins/notification-bus.ts +21 -16
- package/src/plugins/notifications/index.ts +2 -2
- package/src/plugins/notifications/terminal-bell.ts +36 -23
- package/src/plugins/notifications/visual-flash.ts +31 -19
- package/src/plugins/npm-installer.ts +23 -25
- package/src/plugins/plugin-context-factory.ts +10 -10
- package/src/plugins/plugin-host.ts +12 -5
- package/src/plugins/providers/anthropic.ts +64 -57
- package/src/plugins/providers/antigravity.ts +81 -69
- package/src/plugins/providers/codex.ts +42 -42
- package/src/plugins/providers/gemini.ts +106 -87
- package/src/plugins/providers/github-copilot.ts +49 -45
- package/src/plugins/providers/index.ts +10 -12
- package/src/plugins/providers/minimax.ts +50 -44
- package/src/plugins/providers/openai-api.ts +82 -62
- package/src/plugins/providers/opencode-zen.ts +46 -39
- package/src/plugins/providers/perplexity.ts +55 -46
- package/src/plugins/providers/zai.ts +68 -57
- package/src/plugins/registry.ts +80 -71
- package/src/plugins/sandbox-guard.test.ts +45 -48
- package/src/plugins/sandbox-guard.ts +20 -25
- package/src/plugins/sandbox.ts +26 -48
- package/src/plugins/themes/catppuccin-latte.ts +29 -29
- package/src/plugins/themes/catppuccin-mocha.ts +29 -29
- package/src/plugins/themes/claude-code.ts +29 -29
- package/src/plugins/themes/dracula.ts +29 -29
- package/src/plugins/themes/github-light.ts +29 -29
- package/src/plugins/themes/gruvbox-dark.ts +29 -29
- package/src/plugins/themes/gruvbox-light.ts +29 -29
- package/src/plugins/themes/index.ts +15 -15
- package/src/plugins/themes/kanagawa.ts +29 -29
- package/src/plugins/themes/nord.ts +29 -29
- package/src/plugins/themes/one-dark.ts +29 -29
- package/src/plugins/themes/opencode.ts +31 -31
- package/src/plugins/themes/rose-pine-dawn.ts +29 -29
- package/src/plugins/themes/rose-pine.ts +29 -29
- package/src/plugins/themes/solarized-light.ts +29 -29
- package/src/plugins/themes/tokyo-night.ts +29 -29
- package/src/plugins/types/agent.ts +5 -5
- package/src/plugins/types/base.ts +35 -24
- package/src/plugins/types/index.ts +9 -9
- package/src/plugins/types/notification.ts +12 -12
- package/src/plugins/types/provider.ts +12 -8
- package/src/plugins/types/theme.ts +31 -23
- package/src/plugins/update-checker.ts +6 -6
- package/src/pricing/estimator.test.ts +39 -44
- package/src/pricing/estimator.ts +7 -13
- package/src/pricing/fallback.test.ts +30 -34
- package/src/pricing/fallback.ts +54 -27
- package/src/pricing/index.ts +18 -12
- package/src/pricing/models-dev.ts +6 -6
- package/src/storage/database.ts +56 -46
- package/src/storage/db.ts +12 -12
- package/src/storage/index.ts +44 -45
- package/src/storage/migrations/index.ts +2 -2
- package/src/storage/paths.ts +13 -13
- package/src/storage/persistence-service.ts +206 -0
- package/src/storage/repos/agentSessions.ts +33 -23
- package/src/storage/repos/providerSnapshots.ts +5 -5
- package/src/storage/repos/usageEvents.ts +40 -28
- package/src/storage/retention.ts +146 -0
- package/src/storage/types.ts +5 -5
- package/src/tui/App.tsx +180 -137
- package/src/tui/components/CommandPalette.tsx +46 -34
- package/src/tui/components/DebugConsole.tsx +39 -33
- package/src/tui/components/DebugPanel.tsx +314 -183
- package/src/tui/components/FooterHints.tsx +2 -2
- package/src/tui/components/GhostProviderCard.tsx +37 -35
- package/src/tui/components/Header.tsx +68 -35
- package/src/tui/components/HelpOverlay.tsx +99 -29
- package/src/tui/components/InlineGauge.tsx +10 -7
- package/src/tui/components/InlineSparkline.tsx +17 -11
- package/src/tui/components/KpiStrip.tsx +128 -98
- package/src/tui/components/LimitGauge.tsx +96 -59
- package/src/tui/components/ModalBackdrop.tsx +4 -4
- package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
- package/src/tui/components/ProviderCard.tsx +232 -226
- package/src/tui/components/ProviderDetailPanel.tsx +133 -58
- package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
- package/src/tui/components/ProvidersList.tsx +215 -126
- package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
- package/src/tui/components/SessionsTable.tsx +320 -169
- package/src/tui/components/SettingsModal.tsx +593 -269
- package/src/tui/components/SidebarBreakdown.tsx +30 -16
- package/src/tui/components/Skeleton.tsx +24 -17
- package/src/tui/components/SmartSidebar.tsx +360 -258
- package/src/tui/components/Sparkline.tsx +64 -52
- package/src/tui/components/Spinner.tsx +3 -3
- package/src/tui/components/StatusBar.tsx +27 -27
- package/src/tui/components/ThemePicker.tsx +67 -66
- package/src/tui/components/Toast.tsx +9 -6
- package/src/tui/components/UsageGauge.tsx +10 -13
- package/src/tui/components/index.ts +5 -5
- package/src/tui/contexts/AgentSessionContext.tsx +164 -151
- package/src/tui/contexts/ConfigContext.tsx +15 -13
- package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
- package/src/tui/contexts/DemoModeContext.tsx +22 -15
- package/src/tui/contexts/DrawerContext.tsx +11 -9
- package/src/tui/contexts/InputContext.tsx +2 -2
- package/src/tui/contexts/LogContext.tsx +62 -47
- package/src/tui/contexts/PluginContext.tsx +238 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +95 -149
- package/src/tui/contexts/ThemeContext.tsx +21 -16
- package/src/tui/contexts/TimeWindowContext.tsx +72 -61
- package/src/tui/contexts/ToastContext.tsx +10 -7
- package/src/tui/contexts/index.ts +9 -5
- package/src/tui/createApp.tsx +8 -12
- package/src/tui/debug/captureFrame.ts +25 -28
- package/src/tui/debug/snapshot.tsx +179 -162
- package/src/tui/driver/assertions.ts +30 -22
- package/src/tui/driver/cli.ts +231 -221
- package/src/tui/driver/coverage.ts +27 -44
- package/src/tui/driver/demo-snapshot.ts +33 -33
- package/src/tui/driver/diff.ts +26 -32
- package/src/tui/driver/driver.ts +43 -48
- package/src/tui/driver/index.ts +36 -40
- package/src/tui/driver/recorder.ts +37 -48
- package/src/tui/driver/test-workflow.ts +51 -51
- package/src/tui/driver/test.ts +25 -25
- package/src/tui/hooks/index.ts +24 -20
- package/src/tui/hooks/useAnimatedValue.ts +13 -18
- package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
- package/src/tui/hooks/useDashboardState.ts +26 -24
- package/src/tui/hooks/useEmaActivity.ts +82 -74
- package/src/tui/hooks/useEntranceAnimation.ts +11 -11
- package/src/tui/hooks/useExitAnimation.ts +7 -3
- package/src/tui/hooks/usePulse.ts +4 -4
- package/src/tui/hooks/useSafeRenderer.ts +5 -5
- package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
- package/src/tui/hooks/useValueFlash.ts +10 -14
- package/src/tui/index.tsx +29 -6
- package/src/tui/shutdown.ts +19 -0
- package/src/tui/utils/providerColor.ts +1 -1
- package/src/tui/views/Dashboard.tsx +164 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +495 -243
- package/src/tui/views/RealTimeDashboard.tsx +176 -116
- package/src/tui/views/SettingsView.tsx +225 -189
- package/src/tui/views/index.ts +2 -2
- package/src/types/opentui-test-utils.d.ts +7 -7
- package/src/utils/clipboard.ts +29 -29
- package/src/utils/currency.test.ts +53 -53
- package/src/utils/currency.ts +35 -32
- package/src/version.ts +1 -1
package/src/storage/db.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Database } from
|
|
2
|
-
import * as fs from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import { applyMigrations } from "./migrations/index.ts";
|
|
4
|
+
import { PATHS } from "./paths.ts";
|
|
5
5
|
|
|
6
6
|
let db: Database | null = null;
|
|
7
7
|
let currentAppRunId: number | null = null;
|
|
@@ -13,10 +13,10 @@ export async function initDatabase(): Promise<Database> {
|
|
|
13
13
|
|
|
14
14
|
db = new Database(PATHS.data.database, { create: true });
|
|
15
15
|
|
|
16
|
-
db.exec(
|
|
17
|
-
db.exec(
|
|
18
|
-
db.exec(
|
|
19
|
-
db.exec(
|
|
16
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
17
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
18
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
19
|
+
db.exec("PRAGMA busy_timeout = 2000");
|
|
20
20
|
|
|
21
21
|
applyMigrations(db);
|
|
22
22
|
|
|
@@ -27,9 +27,9 @@ export async function initDatabase(): Promise<Database> {
|
|
|
27
27
|
|
|
28
28
|
const result = stmt.run(
|
|
29
29
|
Date.now(),
|
|
30
|
-
process.env.npm_package_version ??
|
|
30
|
+
process.env.npm_package_version ?? "0.0.0",
|
|
31
31
|
process.pid,
|
|
32
|
-
require(
|
|
32
|
+
require("os").hostname(),
|
|
33
33
|
);
|
|
34
34
|
|
|
35
35
|
currentAppRunId = Number(result.lastInsertRowid);
|
|
@@ -39,7 +39,7 @@ export async function initDatabase(): Promise<Database> {
|
|
|
39
39
|
|
|
40
40
|
export function getDatabase(): Database {
|
|
41
41
|
if (!db) {
|
|
42
|
-
throw new Error(
|
|
42
|
+
throw new Error("Database not initialized. Call initDatabase() first.");
|
|
43
43
|
}
|
|
44
44
|
return db;
|
|
45
45
|
}
|
|
@@ -50,7 +50,7 @@ export function getAppRunId(): number | null {
|
|
|
50
50
|
|
|
51
51
|
export function closeDatabase(): void {
|
|
52
52
|
if (db && currentAppRunId) {
|
|
53
|
-
db.prepare(
|
|
53
|
+
db.prepare("UPDATE app_runs SET ended_at = ? WHERE id = ?").run(Date.now(), currentAppRunId);
|
|
54
54
|
}
|
|
55
55
|
if (db) {
|
|
56
56
|
db.close();
|
package/src/storage/index.ts
CHANGED
|
@@ -1,66 +1,65 @@
|
|
|
1
|
-
export { PATHS } from './paths.ts';
|
|
2
|
-
|
|
3
1
|
export {
|
|
4
|
-
initDatabase,
|
|
5
|
-
getDatabase,
|
|
6
2
|
closeDatabase,
|
|
7
|
-
isDatabaseInitialized,
|
|
8
3
|
getAppRunId,
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
getDatabase,
|
|
5
|
+
initDatabase,
|
|
6
|
+
isDatabaseInitialized,
|
|
7
|
+
} from "./db.ts";
|
|
8
|
+
export { PATHS } from "./paths.ts";
|
|
9
|
+
export type { LatestStreamTotals } from "./repos/agentSessions.ts";
|
|
11
10
|
export {
|
|
11
|
+
getAgentSession,
|
|
12
|
+
getLatestStreamTotalsForAllSessions,
|
|
13
|
+
getRecentSessions,
|
|
14
|
+
getSessionsByProject,
|
|
15
|
+
insertAgentSessionSnapshot,
|
|
16
|
+
upsertAgentSession,
|
|
17
|
+
} from "./repos/agentSessions.ts";
|
|
18
|
+
export {
|
|
19
|
+
getLatestProviderSnapshot,
|
|
12
20
|
insertProviderSnapshot,
|
|
13
21
|
insertProviderSnapshotBatch,
|
|
14
22
|
queryProviderSnapshots,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
} from "./repos/providerSnapshots.ts";
|
|
24
|
+
export type {
|
|
25
|
+
ModelDailyCost,
|
|
26
|
+
ProjectDailyCost,
|
|
27
|
+
ProviderDailyCost,
|
|
28
|
+
SessionActivityPoint,
|
|
29
|
+
} from "./repos/usageEvents.ts";
|
|
18
30
|
export {
|
|
31
|
+
calculateBurnRate,
|
|
32
|
+
getSessionActivityTimeline,
|
|
33
|
+
getTotalUsageInWindow,
|
|
19
34
|
insertUsageEvent,
|
|
20
35
|
insertUsageEventBatch,
|
|
21
|
-
queryUsageTimeSeries,
|
|
22
|
-
queryProviderDailyCosts,
|
|
23
36
|
queryModelDailyCosts,
|
|
24
37
|
queryProjectDailyCosts,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} from './repos/usageEvents.ts';
|
|
29
|
-
|
|
30
|
-
export type { SessionActivityPoint, ProviderDailyCost, ModelDailyCost, ProjectDailyCost } from './repos/usageEvents.ts';
|
|
31
|
-
|
|
32
|
-
export {
|
|
33
|
-
upsertAgentSession,
|
|
34
|
-
insertAgentSessionSnapshot,
|
|
35
|
-
getAgentSession,
|
|
36
|
-
getRecentSessions,
|
|
37
|
-
getSessionsByProject,
|
|
38
|
-
getLatestStreamTotalsForAllSessions,
|
|
39
|
-
} from './repos/agentSessions.ts';
|
|
40
|
-
|
|
41
|
-
export type { LatestStreamTotals } from './repos/agentSessions.ts';
|
|
38
|
+
queryProviderDailyCosts,
|
|
39
|
+
queryUsageTimeSeries,
|
|
40
|
+
} from "./repos/usageEvents.ts";
|
|
42
41
|
|
|
43
42
|
export type {
|
|
44
|
-
UsageEventSource,
|
|
45
|
-
SessionStatus,
|
|
46
|
-
CostSource,
|
|
47
|
-
PricingSource,
|
|
48
|
-
AppRunRow,
|
|
49
|
-
ProviderSnapshotRow,
|
|
50
|
-
ProviderSnapshotInsert,
|
|
51
43
|
AgentSessionDim,
|
|
52
|
-
AgentSessionUpsert,
|
|
53
|
-
AgentSessionSnapshotRow,
|
|
54
44
|
AgentSessionSnapshotInsert,
|
|
45
|
+
AgentSessionSnapshotRow,
|
|
55
46
|
AgentSessionStreamSnapshotRow,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
AgentSessionUpsert,
|
|
48
|
+
AppRunRow,
|
|
49
|
+
CostSource,
|
|
59
50
|
DailyAggregateRow,
|
|
60
|
-
|
|
51
|
+
HourlyAggregateRow,
|
|
52
|
+
PricingSource,
|
|
53
|
+
ProviderSnapshotInsert,
|
|
54
|
+
ProviderSnapshotRow,
|
|
55
|
+
SessionStatus,
|
|
56
|
+
StreamTotals,
|
|
61
57
|
TimeSeriesFilters,
|
|
58
|
+
TimeSeriesPoint,
|
|
59
|
+
UsageEventInsert,
|
|
60
|
+
UsageEventRow,
|
|
61
|
+
UsageEventSource,
|
|
62
62
|
UsageQueryOptions,
|
|
63
|
-
|
|
64
|
-
} from './types.ts';
|
|
63
|
+
} from "./types.ts";
|
|
65
64
|
|
|
66
|
-
export { computeStreamDelta } from
|
|
65
|
+
export { computeStreamDelta } from "./types.ts";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Database } from
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
2
|
|
|
3
3
|
export interface Migration {
|
|
4
4
|
version: number;
|
|
@@ -167,7 +167,7 @@ export const migrations: Migration[] = [
|
|
|
167
167
|
];
|
|
168
168
|
|
|
169
169
|
export function getCurrentVersion(db: Database): number {
|
|
170
|
-
const result = db.prepare(
|
|
170
|
+
const result = db.prepare("PRAGMA user_version").get() as { user_version: number } | null;
|
|
171
171
|
return result?.user_version ?? 0;
|
|
172
172
|
}
|
|
173
173
|
|
package/src/storage/paths.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import * as
|
|
1
|
+
import * as os from "os";
|
|
2
|
+
import * as path from "path";
|
|
3
3
|
|
|
4
4
|
const HOME = os.homedir();
|
|
5
5
|
|
|
6
6
|
export const PATHS = {
|
|
7
7
|
config: {
|
|
8
|
-
dir: path.join(HOME,
|
|
9
|
-
file: path.join(HOME,
|
|
10
|
-
fileJsonc: path.join(HOME,
|
|
11
|
-
plugins: path.join(HOME,
|
|
8
|
+
dir: path.join(HOME, ".config/tokentop"),
|
|
9
|
+
file: path.join(HOME, ".config/tokentop/config.json"),
|
|
10
|
+
fileJsonc: path.join(HOME, ".config/tokentop/config.jsonc"),
|
|
11
|
+
plugins: path.join(HOME, ".config/tokentop/plugins"),
|
|
12
12
|
},
|
|
13
13
|
data: {
|
|
14
|
-
dir: path.join(HOME,
|
|
15
|
-
database: path.join(HOME,
|
|
16
|
-
sessions: path.join(HOME,
|
|
17
|
-
cache: path.join(HOME,
|
|
18
|
-
logs: path.join(HOME,
|
|
14
|
+
dir: path.join(HOME, ".local/share/tokentop"),
|
|
15
|
+
database: path.join(HOME, ".local/share/tokentop/usage.db"),
|
|
16
|
+
sessions: path.join(HOME, ".local/share/tokentop/sessions"),
|
|
17
|
+
cache: path.join(HOME, ".local/share/tokentop/cache"),
|
|
18
|
+
logs: path.join(HOME, ".local/share/tokentop/logs"),
|
|
19
19
|
},
|
|
20
20
|
cache: {
|
|
21
|
-
dir: path.join(HOME,
|
|
22
|
-
nodeModules: path.join(HOME,
|
|
21
|
+
dir: path.join(HOME, ".cache/tokentop"),
|
|
22
|
+
nodeModules: path.join(HOME, ".cache/tokentop/node_modules"),
|
|
23
23
|
},
|
|
24
24
|
} as const;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level session persistence service.
|
|
3
|
+
*
|
|
4
|
+
* All mutable state is stored on globalThis to survive bun --hot HMR
|
|
5
|
+
* reloads and React component remounts. This decouples persistence
|
|
6
|
+
* from React's lifecycle entirely.
|
|
7
|
+
*
|
|
8
|
+
* Call persistSessions() on every poll tick (1s). The service internally:
|
|
9
|
+
* Gate 1: Skips sessions whose usage data hasn't changed (fingerprint)
|
|
10
|
+
* Gate 2: Throttles writes to at most once per SNAPSHOT_INTERVAL_MS
|
|
11
|
+
*
|
|
12
|
+
* Both gates update their state only on successful writes, ensuring
|
|
13
|
+
* deferred writes when the throttle expires.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { isDatabaseInitialized } from "./db.ts";
|
|
17
|
+
import {
|
|
18
|
+
getLatestStreamTotalsForAllSessions,
|
|
19
|
+
insertAgentSessionSnapshot,
|
|
20
|
+
upsertAgentSession,
|
|
21
|
+
} from "./repos/agentSessions.ts";
|
|
22
|
+
import { insertUsageEventBatch } from "./repos/usageEvents.ts";
|
|
23
|
+
import type {
|
|
24
|
+
AgentSessionSnapshotInsert,
|
|
25
|
+
AgentSessionStreamSnapshotRow,
|
|
26
|
+
AgentSessionUpsert,
|
|
27
|
+
StreamTotals,
|
|
28
|
+
UsageEventInsert,
|
|
29
|
+
} from "./types.ts";
|
|
30
|
+
import { computeStreamDelta } from "./types.ts";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Configuration
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/** Minimum interval between DB writes for the same session */
|
|
37
|
+
const SNAPSHOT_INTERVAL_MS = 300_000; // 5 minutes
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Global state (survives bun --hot HMR and React remounts)
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
interface PersistenceState {
|
|
44
|
+
/** Timestamp of last successful DB write per session key */
|
|
45
|
+
lastWriteTimestamps: Map<string, number>;
|
|
46
|
+
/** Usage-based fingerprint per session key (updated only on successful write) */
|
|
47
|
+
sessionFingerprints: Map<string, string>;
|
|
48
|
+
/** Previous stream totals for delta computation */
|
|
49
|
+
previousStreamTotals: Map<string, StreamTotals>;
|
|
50
|
+
/** Whether startup seeding has completed */
|
|
51
|
+
seeded: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare global {
|
|
55
|
+
// eslint-disable-next-line no-var
|
|
56
|
+
var __tokentopPersistence: PersistenceState | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getState(): PersistenceState {
|
|
60
|
+
if (!globalThis.__tokentopPersistence) {
|
|
61
|
+
globalThis.__tokentopPersistence = {
|
|
62
|
+
lastWriteTimestamps: new Map(),
|
|
63
|
+
sessionFingerprints: new Map(),
|
|
64
|
+
previousStreamTotals: new Map(),
|
|
65
|
+
seeded: false,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return globalThis.__tokentopPersistence;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Fingerprinting
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Compute a fingerprint based on usage data only (not lastActivityAt).
|
|
77
|
+
* This ensures inactive sessions with stable usage don't trigger writes,
|
|
78
|
+
* while active sessions that consume tokens do.
|
|
79
|
+
*/
|
|
80
|
+
function computeFingerprint(snapshot: Omit<AgentSessionSnapshotInsert, "agentSessionId">): string {
|
|
81
|
+
return `${snapshot.totalCostUsd}:${snapshot.requestCount}:${snapshot.totalInputTokens}:${snapshot.totalOutputTokens}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Public API
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
export interface SessionPersistData {
|
|
89
|
+
session: AgentSessionUpsert;
|
|
90
|
+
snapshot: Omit<AgentSessionSnapshotInsert, "agentSessionId">;
|
|
91
|
+
streams: Omit<AgentSessionStreamSnapshotRow, "agentSessionSnapshotId">[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Persist session data to the database with per-session throttling.
|
|
96
|
+
*
|
|
97
|
+
* Safe to call on every poll tick (every 1s). The service internally:
|
|
98
|
+
* 1. Skips sessions whose usage data hasn't changed (fingerprint gate)
|
|
99
|
+
* 2. Throttles writes to at most once per SNAPSHOT_INTERVAL_MS per session
|
|
100
|
+
*
|
|
101
|
+
* @returns Number of sessions actually written to DB
|
|
102
|
+
*/
|
|
103
|
+
export function persistSessions(sessions: SessionPersistData[]): number {
|
|
104
|
+
if (!isDatabaseInitialized()) return 0;
|
|
105
|
+
|
|
106
|
+
const state = getState();
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
let persistedCount = 0;
|
|
109
|
+
|
|
110
|
+
for (const { session, snapshot, streams } of sessions) {
|
|
111
|
+
const sessionKey = `${session.agentId}:${session.sessionId}`;
|
|
112
|
+
|
|
113
|
+
// Gate 1: Skip sessions whose usage data hasn't changed since last write
|
|
114
|
+
const fp = computeFingerprint(snapshot);
|
|
115
|
+
if (state.sessionFingerprints.get(sessionKey) === fp) continue;
|
|
116
|
+
|
|
117
|
+
// Gate 2: Throttle writes to once per interval per session
|
|
118
|
+
const lastTs = state.lastWriteTimestamps.get(sessionKey) ?? 0;
|
|
119
|
+
if (now - lastTs < SNAPSHOT_INTERVAL_MS) continue;
|
|
120
|
+
|
|
121
|
+
// Both gates passed — write to DB
|
|
122
|
+
try {
|
|
123
|
+
const agentSessionId = upsertAgentSession(session);
|
|
124
|
+
insertAgentSessionSnapshot({ ...snapshot, agentSessionId }, streams);
|
|
125
|
+
|
|
126
|
+
// Compute and emit usage event deltas
|
|
127
|
+
const usageEvents: UsageEventInsert[] = [];
|
|
128
|
+
for (const stream of streams) {
|
|
129
|
+
const streamKey = `${sessionKey}:${stream.provider}:${stream.model}`;
|
|
130
|
+
const current: StreamTotals = {
|
|
131
|
+
inputTokens: stream.inputTokens,
|
|
132
|
+
outputTokens: stream.outputTokens,
|
|
133
|
+
cacheReadTokens: stream.cacheReadTokens,
|
|
134
|
+
cacheWriteTokens: stream.cacheWriteTokens,
|
|
135
|
+
costUsd: stream.costUsd,
|
|
136
|
+
requestCount: stream.requestCount,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const previous = state.previousStreamTotals.get(streamKey);
|
|
140
|
+
const delta = computeStreamDelta(current, previous);
|
|
141
|
+
|
|
142
|
+
if (delta) {
|
|
143
|
+
usageEvents.push({
|
|
144
|
+
timestamp: now,
|
|
145
|
+
source: "agent",
|
|
146
|
+
provider: stream.provider,
|
|
147
|
+
model: stream.model,
|
|
148
|
+
agentId: session.agentId,
|
|
149
|
+
sessionId: session.sessionId,
|
|
150
|
+
projectPath: session.projectPath ?? null,
|
|
151
|
+
inputTokens: delta.inputTokens,
|
|
152
|
+
outputTokens: delta.outputTokens,
|
|
153
|
+
cacheReadTokens: delta.cacheReadTokens,
|
|
154
|
+
cacheWriteTokens: delta.cacheWriteTokens,
|
|
155
|
+
costUsd: delta.costUsd,
|
|
156
|
+
requestCount: delta.requestCount,
|
|
157
|
+
pricingSource: stream.pricingSource ?? null,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
state.previousStreamTotals.set(streamKey, current);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (usageEvents.length > 0) {
|
|
165
|
+
insertUsageEventBatch(usageEvents);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Update tracking state ONLY on successful write
|
|
169
|
+
state.lastWriteTimestamps.set(sessionKey, now);
|
|
170
|
+
state.sessionFingerprints.set(sessionKey, fp);
|
|
171
|
+
persistedCount++;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error("Failed to persist session:", err);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return persistedCount;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Seed previous stream totals from the database.
|
|
182
|
+
* Called once at startup after DB initialization to ensure
|
|
183
|
+
* accurate delta computation from the first persistence call.
|
|
184
|
+
*/
|
|
185
|
+
export function seedPreviousTotals(): void {
|
|
186
|
+
const state = getState();
|
|
187
|
+
if (state.seeded) return;
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const latestTotals = getLatestStreamTotalsForAllSessions();
|
|
191
|
+
for (const row of latestTotals) {
|
|
192
|
+
const streamKey = `${row.agentId}:${row.sessionId}:${row.provider}:${row.model}`;
|
|
193
|
+
state.previousStreamTotals.set(streamKey, {
|
|
194
|
+
inputTokens: row.inputTokens,
|
|
195
|
+
outputTokens: row.outputTokens,
|
|
196
|
+
cacheReadTokens: row.cacheReadTokens,
|
|
197
|
+
cacheWriteTokens: row.cacheWriteTokens,
|
|
198
|
+
costUsd: row.costUsd,
|
|
199
|
+
requestCount: row.requestCount,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
state.seeded = true;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error("Failed to seed previous totals from DB:", err);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { getDatabase } from
|
|
1
|
+
import { getDatabase } from "../db.ts";
|
|
2
2
|
import type {
|
|
3
3
|
AgentSessionDim,
|
|
4
|
-
AgentSessionUpsert,
|
|
5
4
|
AgentSessionSnapshotInsert,
|
|
6
5
|
AgentSessionStreamSnapshotRow,
|
|
7
|
-
|
|
6
|
+
AgentSessionUpsert,
|
|
7
|
+
} from "../types.ts";
|
|
8
8
|
|
|
9
9
|
const UPSERT_SESSION_SQL = `
|
|
10
10
|
INSERT INTO agent_sessions (agent_id, session_id, project_path, started_at, first_seen_at, last_seen_at)
|
|
@@ -36,21 +36,23 @@ export function upsertAgentSession(session: AgentSessionUpsert): number {
|
|
|
36
36
|
const db = getDatabase();
|
|
37
37
|
const now = Date.now();
|
|
38
38
|
|
|
39
|
-
const row = db
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
const row = db
|
|
40
|
+
.prepare(UPSERT_SESSION_SQL)
|
|
41
|
+
.get(
|
|
42
|
+
session.agentId,
|
|
43
|
+
session.sessionId,
|
|
44
|
+
session.projectPath ?? null,
|
|
45
|
+
session.startedAt ?? null,
|
|
46
|
+
session.firstSeenAt ?? now,
|
|
47
|
+
session.lastSeenAt,
|
|
48
|
+
) as { id: number };
|
|
47
49
|
|
|
48
50
|
return row.id;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export function insertAgentSessionSnapshot(
|
|
52
54
|
snapshot: AgentSessionSnapshotInsert,
|
|
53
|
-
streams: Omit<AgentSessionStreamSnapshotRow,
|
|
55
|
+
streams: Omit<AgentSessionStreamSnapshotRow, "agentSessionSnapshotId">[],
|
|
54
56
|
): number {
|
|
55
57
|
const db = getDatabase();
|
|
56
58
|
|
|
@@ -70,7 +72,7 @@ export function insertAgentSessionSnapshot(
|
|
|
70
72
|
snapshot.totalCacheReadTokens,
|
|
71
73
|
snapshot.totalCacheWriteTokens,
|
|
72
74
|
snapshot.totalCostUsd,
|
|
73
|
-
snapshot.requestCount
|
|
75
|
+
snapshot.requestCount,
|
|
74
76
|
);
|
|
75
77
|
|
|
76
78
|
snapshotId = Number(result.lastInsertRowid);
|
|
@@ -86,7 +88,7 @@ export function insertAgentSessionSnapshot(
|
|
|
86
88
|
stream.cacheWriteTokens,
|
|
87
89
|
stream.costUsd,
|
|
88
90
|
stream.requestCount,
|
|
89
|
-
stream.pricingSource ?? null
|
|
91
|
+
stream.pricingSource ?? null,
|
|
90
92
|
);
|
|
91
93
|
}
|
|
92
94
|
})();
|
|
@@ -97,9 +99,11 @@ export function insertAgentSessionSnapshot(
|
|
|
97
99
|
export function getAgentSession(agentId: string, sessionId: string): AgentSessionDim | null {
|
|
98
100
|
const db = getDatabase();
|
|
99
101
|
|
|
100
|
-
const row = db
|
|
102
|
+
const row = db
|
|
103
|
+
.prepare(`
|
|
101
104
|
SELECT * FROM agent_sessions WHERE agent_id = ? AND session_id = ?
|
|
102
|
-
`)
|
|
105
|
+
`)
|
|
106
|
+
.get(agentId, sessionId) as DbSessionRow | null;
|
|
103
107
|
|
|
104
108
|
return row ? mapSessionRow(row) : null;
|
|
105
109
|
}
|
|
@@ -107,11 +111,13 @@ export function getAgentSession(agentId: string, sessionId: string): AgentSessio
|
|
|
107
111
|
export function getRecentSessions(limit = 50): AgentSessionDim[] {
|
|
108
112
|
const db = getDatabase();
|
|
109
113
|
|
|
110
|
-
const rows = db
|
|
114
|
+
const rows = db
|
|
115
|
+
.prepare(`
|
|
111
116
|
SELECT * FROM agent_sessions
|
|
112
117
|
ORDER BY last_seen_at DESC
|
|
113
118
|
LIMIT ?
|
|
114
|
-
`)
|
|
119
|
+
`)
|
|
120
|
+
.all(limit) as DbSessionRow[];
|
|
115
121
|
|
|
116
122
|
return rows.map(mapSessionRow);
|
|
117
123
|
}
|
|
@@ -119,12 +125,14 @@ export function getRecentSessions(limit = 50): AgentSessionDim[] {
|
|
|
119
125
|
export function getSessionsByProject(projectPath: string, limit = 50): AgentSessionDim[] {
|
|
120
126
|
const db = getDatabase();
|
|
121
127
|
|
|
122
|
-
const rows = db
|
|
128
|
+
const rows = db
|
|
129
|
+
.prepare(`
|
|
123
130
|
SELECT * FROM agent_sessions
|
|
124
131
|
WHERE project_path = ?
|
|
125
132
|
ORDER BY last_seen_at DESC
|
|
126
133
|
LIMIT ?
|
|
127
|
-
`)
|
|
134
|
+
`)
|
|
135
|
+
.all(projectPath, limit) as DbSessionRow[];
|
|
128
136
|
|
|
129
137
|
return rows.map(mapSessionRow);
|
|
130
138
|
}
|
|
@@ -167,7 +175,8 @@ export interface LatestStreamTotals {
|
|
|
167
175
|
export function getLatestStreamTotalsForAllSessions(): LatestStreamTotals[] {
|
|
168
176
|
const db = getDatabase();
|
|
169
177
|
|
|
170
|
-
const rows = db
|
|
178
|
+
const rows = db
|
|
179
|
+
.prepare(`
|
|
171
180
|
SELECT
|
|
172
181
|
s.agent_id, s.session_id,
|
|
173
182
|
ss.provider, ss.model,
|
|
@@ -182,7 +191,8 @@ export function getLatestStreamTotalsForAllSessions(): LatestStreamTotals[] {
|
|
|
182
191
|
FROM agent_session_snapshots snap2
|
|
183
192
|
GROUP BY snap2.agent_session_id
|
|
184
193
|
)
|
|
185
|
-
`)
|
|
194
|
+
`)
|
|
195
|
+
.all() as Array<{
|
|
186
196
|
agent_id: string;
|
|
187
197
|
session_id: string;
|
|
188
198
|
provider: string;
|
|
@@ -195,7 +205,7 @@ export function getLatestStreamTotalsForAllSessions(): LatestStreamTotals[] {
|
|
|
195
205
|
request_count: number;
|
|
196
206
|
}>;
|
|
197
207
|
|
|
198
|
-
return rows.map(r => ({
|
|
208
|
+
return rows.map((r) => ({
|
|
199
209
|
agentId: r.agent_id,
|
|
200
210
|
sessionId: r.session_id,
|
|
201
211
|
provider: r.provider,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getDatabase } from
|
|
2
|
-
import type { ProviderSnapshotInsert, ProviderSnapshotRow
|
|
1
|
+
import { getDatabase } from "../db.ts";
|
|
2
|
+
import type { CostSource, ProviderSnapshotInsert, ProviderSnapshotRow } from "../types.ts";
|
|
3
3
|
|
|
4
4
|
const INSERT_SQL = `
|
|
5
5
|
INSERT INTO provider_snapshots (
|
|
@@ -36,7 +36,7 @@ export function insertProviderSnapshot(snapshot: ProviderSnapshotInsert): number
|
|
|
36
36
|
snapshot.tokensOutput ?? null,
|
|
37
37
|
snapshot.costUsd ?? null,
|
|
38
38
|
snapshot.costSource ?? null,
|
|
39
|
-
snapshot.rawJson ?? null
|
|
39
|
+
snapshot.rawJson ?? null,
|
|
40
40
|
);
|
|
41
41
|
|
|
42
42
|
return Number(result.lastInsertRowid);
|
|
@@ -60,7 +60,7 @@ export function insertProviderSnapshotBatch(snapshots: ProviderSnapshotInsert[])
|
|
|
60
60
|
s.tokensOutput ?? null,
|
|
61
61
|
s.costUsd ?? null,
|
|
62
62
|
s.costSource ?? null,
|
|
63
|
-
s.rawJson ?? null
|
|
63
|
+
s.rawJson ?? null,
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
66
|
});
|
|
@@ -72,7 +72,7 @@ export function queryProviderSnapshots(
|
|
|
72
72
|
provider: string,
|
|
73
73
|
startMs: number,
|
|
74
74
|
endMs: number,
|
|
75
|
-
limit = 100
|
|
75
|
+
limit = 100,
|
|
76
76
|
): ProviderSnapshotRow[] {
|
|
77
77
|
const db = getDatabase();
|
|
78
78
|
const rows = db.prepare(QUERY_BY_PROVIDER_SQL).all(provider, startMs, endMs, limit) as DbRow[];
|