mixdog 0.9.166 → 0.9.167

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.
Files changed (221) hide show
  1. package/package.json +1 -1
  2. package/scripts/audit-usage-ledger.mjs +86 -0
  3. package/scripts/generate-voice-runtime-manifest.mjs +22 -17
  4. package/scripts/memory-chunk-quality.mjs +128 -0
  5. package/scripts/memory-layer2-check.mjs +133 -0
  6. package/scripts/model-catalog-audit.mjs +6 -0
  7. package/scripts/native-tool-download.mjs +14 -0
  8. package/scripts/prepare-native-assets.mjs +2 -7
  9. package/scripts/tool-failures.mjs +7 -2
  10. package/scripts/verify-release-assets.mjs +48 -90
  11. package/src/defaults/skills/browser-use/SKILL.md +33 -18
  12. package/src/defaults/skills/computer-use/SKILL.md +16 -8
  13. package/src/lib/keychain-cjs.cjs +0 -1
  14. package/src/lib/rules-builder.cjs +4 -1
  15. package/src/rules/agent/40-cycle1-agent.md +21 -13
  16. package/src/rules/lead/01-general.md +8 -3
  17. package/src/rules/shared/10-tool-workflow.md +20 -11
  18. package/src/rules/shared/30-exploration.md +11 -4
  19. package/src/rules/shared/40-editing.md +2 -3
  20. package/src/rules/shared/50-execution.md +6 -3
  21. package/src/rules/shared/60-verification.md +3 -2
  22. package/src/rules/shared/70-delivery.md +0 -4
  23. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +51 -45
  24. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +3 -4
  25. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -7
  26. package/src/runtime/agent/orchestrator/agent-trace.mjs +9 -0
  27. package/src/runtime/agent/orchestrator/config.mjs +9 -4
  28. package/src/runtime/agent/orchestrator/context/collect.mjs +25 -1055
  29. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +272 -0
  30. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +296 -0
  31. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +523 -0
  32. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +7 -2
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +13 -14
  34. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +214 -0
  35. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +220 -116
  36. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +74 -44
  37. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +191 -29
  38. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +13 -12
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +1 -9
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +5 -2
  41. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +48 -25
  42. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +28 -2
  43. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -1
  44. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +1 -8
  45. package/src/runtime/agent/orchestrator/providers/lib/action-input-contract.mjs +32 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +21 -6
  47. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +61 -13
  48. package/src/runtime/agent/orchestrator/providers/model-catalog-projection.mjs +6 -1
  49. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +123 -118
  50. package/src/runtime/agent/orchestrator/providers/model-pricing-rates.mjs +54 -0
  51. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +5 -2
  52. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +34 -3
  53. package/src/runtime/agent/orchestrator/providers/openai-codex-endpoints.mjs +21 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +154 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -1
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +322 -0
  57. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +243 -702
  58. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +2 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-startup-prewarm.mjs +221 -0
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +19 -0
  63. package/src/runtime/agent/orchestrator/providers/provider-model-identities.mjs +40 -0
  64. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +120 -710
  65. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +36 -15
  66. package/src/runtime/agent/orchestrator/session/cache/util.mjs +9 -0
  67. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +17 -4
  68. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +36 -31
  69. package/src/runtime/agent/orchestrator/session/compact.mjs +4 -5
  70. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +6 -5
  71. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +8 -6
  72. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +121 -0
  73. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +2 -3
  74. package/src/runtime/agent/orchestrator/session/loop/diagnostics.mjs +74 -0
  75. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +44 -17
  76. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +266 -0
  77. package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +111 -0
  78. package/src/runtime/agent/orchestrator/session/loop/request-projection.mjs +99 -0
  79. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +91 -0
  80. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +36 -0
  81. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +3 -3
  82. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  83. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +20 -11
  84. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +3 -5
  85. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -3
  86. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +39 -0
  87. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +3 -2
  88. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +21 -3
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +62 -10
  93. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +17 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +61 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +13 -9
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +12 -2
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +3 -2
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +8 -3
  99. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +69 -6
  100. package/src/runtime/agent/orchestrator/tools/builtin.mjs +14 -17
  101. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  102. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  104. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  105. package/src/runtime/bridge-discovery.mjs +8 -0
  106. package/src/runtime/browser-bridge/action-schema.mjs +37 -43
  107. package/src/runtime/browser-bridge/client.mjs +111 -99
  108. package/src/runtime/browser-bridge/input-fields.mjs +187 -0
  109. package/src/runtime/browser-bridge/tool-defs.mjs +12 -227
  110. package/src/runtime/computer-bridge/action-schema.mjs +2 -46
  111. package/src/runtime/computer-bridge/client.mjs +69 -77
  112. package/src/runtime/computer-bridge/core-actions.mjs +7 -5
  113. package/src/runtime/computer-bridge/error-recovery.mjs +41 -37
  114. package/src/runtime/computer-bridge/tool-defs.mjs +1 -1
  115. package/src/runtime/github/commands.mjs +4 -1
  116. package/src/runtime/github/contract.mjs +1 -1
  117. package/src/runtime/github/tool.mjs +2 -2
  118. package/src/runtime/media/adapters/antigravity-image.mjs +72 -0
  119. package/src/runtime/media/adapters/gemini-image.mjs +20 -11
  120. package/src/runtime/media/auth.mjs +12 -0
  121. package/src/runtime/media/catalog.mjs +30 -10
  122. package/src/runtime/media/jobs.mjs +4 -0
  123. package/src/runtime/media/lanes.mjs +40 -31
  124. package/src/runtime/memory/index.mjs +2 -9
  125. package/src/runtime/memory/lib/compact-handoff.mjs +26 -18
  126. package/src/runtime/memory/lib/memory-chunk-audit.mjs +41 -0
  127. package/src/runtime/memory/lib/memory-chunk-quality.mjs +388 -0
  128. package/src/runtime/memory/lib/memory-cycle1.mjs +95 -394
  129. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +29 -4
  130. package/src/runtime/memory/lib/memory-recall-store.mjs +8 -12
  131. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -11
  132. package/src/runtime/memory/lib/memory.mjs +2 -0
  133. package/src/runtime/memory/lib/query-handlers.mjs +98 -144
  134. package/src/runtime/memory/lib/query-ranking.mjs +23 -9
  135. package/src/runtime/memory/lib/recall-format.mjs +37 -27
  136. package/src/runtime/memory/lib/session-ingest-runtime.mjs +14 -8
  137. package/src/runtime/memory/lib/session-ingest.mjs +45 -84
  138. package/src/runtime/memory/lib/transcript-ingest.mjs +19 -21
  139. package/src/runtime/memory/tool-defs.mjs +2 -2
  140. package/src/runtime/office/authoring/pptx-author-session.mjs +3 -7
  141. package/src/runtime/office/core/office-actions-batch.mjs +2 -8
  142. package/src/runtime/office/core/office-actions-open.mjs +124 -0
  143. package/src/runtime/office/core/office-actions-read.mjs +127 -0
  144. package/src/runtime/office/core/office-core.mjs +80 -1
  145. package/src/runtime/office/core/office-sessions.mjs +116 -138
  146. package/src/runtime/office/index.mjs +18 -271
  147. package/src/runtime/office/pdf/pdf-analysis.mjs +2 -2
  148. package/src/runtime/office/pdf/pdf-limits.mjs +2 -0
  149. package/src/runtime/office/tool-defs.mjs +7 -6
  150. package/src/runtime/shared/agent-route-config.mjs +8 -8
  151. package/src/runtime/shared/child-spawn-gate.mjs +10 -13
  152. package/src/runtime/shared/config.mjs +2 -7
  153. package/src/runtime/shared/env.mjs +3 -3
  154. package/src/runtime/shared/err-text.mjs +1 -1
  155. package/src/runtime/shared/extension-scopes.mjs +3 -2
  156. package/src/runtime/shared/heap-cap.mjs +10 -6
  157. package/src/runtime/shared/llm/cost.mjs +35 -10
  158. package/src/runtime/shared/llm/usage-accounting.mjs +11 -1
  159. package/src/runtime/shared/llm/usage-context.mjs +6 -0
  160. package/src/runtime/shared/llm/usage-ledger-import.mjs +5 -0
  161. package/src/runtime/shared/llm/usage-ledger-repair.mjs +109 -0
  162. package/src/runtime/shared/llm/usage-ledger.mjs +59 -20
  163. package/src/runtime/shared/llm/usage-pricing-refresh.mjs +26 -0
  164. package/src/runtime/shared/llm/usage-rollup.mjs +17 -15
  165. package/src/runtime/shared/native-asset.mjs +3 -4
  166. package/src/runtime/shared/object.mjs +4 -0
  167. package/src/runtime/shared/pg-schema-db.mjs +21 -0
  168. package/src/runtime/shared/plugin-metadata.mjs +3 -1
  169. package/src/runtime/shared/resource-admission.mjs +1 -6
  170. package/src/runtime/shared/schedules-db.mjs +8 -24
  171. package/src/runtime/shared/schema-value-error.d.mts +2 -0
  172. package/src/runtime/shared/schema-value-error.mjs +68 -0
  173. package/src/runtime/shared/singleton-owner.mjs +11 -24
  174. package/src/runtime/shared/staged-update.mjs +1 -10
  175. package/src/runtime/shared/tool-card-model.mjs +8 -5
  176. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  177. package/src/runtime/shared/tool-surface.mjs +1 -5
  178. package/src/runtime/shared/user-data-guard.mjs +1 -4
  179. package/src/runtime/shared/webhooks-db.mjs +8 -24
  180. package/src/runtime/web-search/index.mjs +32 -8
  181. package/src/runtime/web-search/tool-defs.mjs +4 -4
  182. package/src/session-runtime/config-lifecycle.mjs +34 -0
  183. package/src/session-runtime/context-status.mjs +19 -0
  184. package/src/session-runtime/cwd-plugins.mjs +12 -29
  185. package/src/session-runtime/goal-tasks.mjs +2 -2
  186. package/src/session-runtime/goal-tool-defs.mjs +3 -2
  187. package/src/session-runtime/internal-tool-executor.mjs +7 -0
  188. package/src/session-runtime/lifecycle-api.mjs +5 -11
  189. package/src/session-runtime/notification-bus.mjs +41 -0
  190. package/src/session-runtime/prewarm.mjs +22 -0
  191. package/src/session-runtime/runtime-bootstrap.mjs +90 -0
  192. package/src/session-runtime/runtime-core.mjs +88 -202
  193. package/src/session-runtime/runtime-feature-gates.mjs +37 -0
  194. package/src/session-runtime/session-lifecycle.mjs +5 -5
  195. package/src/session-runtime/session-turn-api.mjs +1 -12
  196. package/src/session-runtime/setup-tool/executor.mjs +13 -5
  197. package/src/session-runtime/setup-tool/tool-defs.mjs +52 -46
  198. package/src/session-runtime/tool-defs.mjs +73 -4
  199. package/src/session-runtime/usage-stats-api.mjs +4 -1
  200. package/src/session-runtime/warmup-schedulers.mjs +8 -2
  201. package/src/session-runtime/workflow.mjs +14 -0
  202. package/src/standalone/channel-session-router.mjs +3 -1
  203. package/src/standalone/channel-worker.mjs +18 -9
  204. package/src/standalone/daemon-crash-capture.mjs +534 -0
  205. package/src/standalone/daemon-telemetry.mjs +175 -0
  206. package/src/standalone/daemon.mjs +31 -21
  207. package/src/standalone/session-client.mjs +31 -23
  208. package/src/standalone/usage-dashboard-model.mjs +2 -0
  209. package/src/standalone/usage-stats-hours.mjs +18 -11
  210. package/src/standalone/usage-stats-model.mjs +39 -28
  211. package/src/standalone/usage-stats-period.mjs +43 -45
  212. package/src/tui/app/use-transcript-window.mjs +1 -1
  213. package/src/tui/components/StatusLine.jsx +13 -42
  214. package/src/tui/dist/index.mjs +256 -239
  215. package/src/tui/session/session-api-ext.mjs +44 -41
  216. package/src/tui/session/session-api.mjs +4 -0
  217. package/src/tui/session/tool-card-results.mjs +25 -78
  218. package/src/tui/session/tool-result-status.mjs +94 -1
  219. package/src/tui/session/turn-aggregate-cards.mjs +17 -54
  220. package/src/ui/statusline-format.mjs +6 -26
  221. package/src/workflows/default/WORKFLOW.md +5 -0
@@ -0,0 +1,175 @@
1
+ // Periodic daemon memory + in-flight work telemetry.
2
+ //
3
+ // Owns the single unref'd 30s diagnostics loop (event-loop lag rides
4
+ // onInterval on the same tick) so a crash still has a recent
5
+ // RSS/heap/limit/work record in daemon.log. The V8 heap limit is
6
+ // process-specific (heap_size_limit) and is never inferred from host
7
+ // free RAM. Lines contain only counters — no session bodies, tokens,
8
+ // or env. Retention is the daemon log's existing rotate-and-keep bound.
9
+
10
+ import v8 from 'node:v8';
11
+
12
+ export const DAEMON_TELEMETRY_INTERVAL_MS = 30_000;
13
+
14
+ const WORK_KEYS = Object.freeze([
15
+ 'activeCalls',
16
+ 'queuedCalls',
17
+ 'busySessions',
18
+ 'busyMemoryAgents',
19
+ ]);
20
+ const REASONS = new Set(['boot', 'periodic']);
21
+
22
+ function nonNegativeInt(value) {
23
+ const n = Number(value);
24
+ return Number.isFinite(n) && n >= 0 ? Math.round(n) : 0;
25
+ }
26
+
27
+ function byteCount(value) {
28
+ const n = Number(value);
29
+ return Number.isFinite(n) && n >= 0 ? Math.round(n) : -1;
30
+ }
31
+
32
+ function timestamp(now) {
33
+ try {
34
+ const value = typeof now === 'function' ? now() : now;
35
+ if (value instanceof Date) return value.toISOString();
36
+ if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString();
37
+ if (typeof value === 'string' && value) {
38
+ const parsed = new Date(value);
39
+ if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
40
+ }
41
+ } catch { /* fall through */ }
42
+ return new Date().toISOString();
43
+ }
44
+
45
+ function processId(pid) {
46
+ const n = Number(pid);
47
+ return Number.isInteger(n) ? n : process.pid;
48
+ }
49
+
50
+ function workCounts(work) {
51
+ const source = work && typeof work === 'object' ? work : {};
52
+ const counts = {};
53
+ for (const key of WORK_KEYS) counts[key] = nonNegativeInt(source[key]);
54
+ return counts;
55
+ }
56
+
57
+ function readMemory(memoryUsage) {
58
+ try {
59
+ return memoryUsage?.() || {};
60
+ } catch {
61
+ return {};
62
+ }
63
+ }
64
+
65
+ function readHeapLimit(heapStatistics) {
66
+ try {
67
+ return byteCount(heapStatistics?.()?.heap_size_limit);
68
+ } catch {
69
+ return -1;
70
+ }
71
+ }
72
+
73
+ function reasonToken(reason) {
74
+ const text = String(reason ?? '').toLowerCase();
75
+ return REASONS.has(text) ? text : 'periodic';
76
+ }
77
+
78
+ export function collectDaemonTelemetry({
79
+ now = () => new Date(),
80
+ pid = process.pid,
81
+ memoryUsage = () => process.memoryUsage(),
82
+ heapStatistics = () => v8.getHeapStatistics(),
83
+ work = {},
84
+ } = {}) {
85
+ const usage = readMemory(memoryUsage);
86
+ return {
87
+ ts: timestamp(now),
88
+ pid: processId(pid),
89
+ rssBytes: byteCount(usage.rss),
90
+ heapUsedBytes: byteCount(usage.heapUsed),
91
+ heapTotalBytes: byteCount(usage.heapTotal),
92
+ heapLimitBytes: readHeapLimit(heapStatistics),
93
+ externalBytes: byteCount(usage.external),
94
+ arrayBufferBytes: byteCount(usage.arrayBuffers),
95
+ ...workCounts(work),
96
+ };
97
+ }
98
+
99
+ export function formatDaemonTelemetry(record, reason = 'periodic') {
100
+ const row = record && typeof record === 'object' ? record : collectDaemonTelemetry();
101
+ return [
102
+ 'daemon-telemetry',
103
+ `reason=${reasonToken(reason)}`,
104
+ `ts=${row.ts}`,
105
+ `pid=${row.pid}`,
106
+ `rssBytes=${row.rssBytes}`,
107
+ `heapUsedBytes=${row.heapUsedBytes}`,
108
+ `heapTotalBytes=${row.heapTotalBytes}`,
109
+ `heapLimitBytes=${row.heapLimitBytes}`,
110
+ `externalBytes=${row.externalBytes}`,
111
+ `arrayBufferBytes=${row.arrayBufferBytes}`,
112
+ `activeCalls=${row.activeCalls}`,
113
+ `queuedCalls=${row.queuedCalls}`,
114
+ `busySessions=${row.busySessions}`,
115
+ `busyMemoryAgents=${row.busyMemoryAgents}`,
116
+ ].join(' ');
117
+ }
118
+
119
+ export function createDaemonTelemetry({
120
+ log = () => {},
121
+ getWork = () => ({}),
122
+ onInterval = null,
123
+ now,
124
+ pid,
125
+ memoryUsage,
126
+ heapStatistics,
127
+ setIntervalFn = setInterval,
128
+ clearIntervalFn = clearInterval,
129
+ } = {}) {
130
+ let timer = null;
131
+
132
+ function emit(reason = 'periodic') {
133
+ try {
134
+ let work = {};
135
+ try { work = getWork(); } catch { work = {}; }
136
+ const record = collectDaemonTelemetry({
137
+ now,
138
+ pid,
139
+ memoryUsage,
140
+ heapStatistics,
141
+ work,
142
+ });
143
+ log(formatDaemonTelemetry(record, reason));
144
+ return record;
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ function tick() {
151
+ emit('periodic');
152
+ try { onInterval?.(); } catch { /* lag probes must not kill the sample loop */ }
153
+ }
154
+
155
+ function start(intervalMs = DAEMON_TELEMETRY_INTERVAL_MS) {
156
+ if (timer) return timer;
157
+ const delay = Number(intervalMs);
158
+ const ms = Number.isFinite(delay) && delay > 0 ? delay : DAEMON_TELEMETRY_INTERVAL_MS;
159
+ const handle = setIntervalFn(() => {
160
+ if (timer !== handle) return;
161
+ tick();
162
+ }, ms);
163
+ handle?.unref?.();
164
+ timer = handle;
165
+ return timer;
166
+ }
167
+
168
+ function stop() {
169
+ if (!timer) return;
170
+ clearIntervalFn(timer);
171
+ timer = null;
172
+ }
173
+
174
+ return { emit, start, stop };
175
+ }
@@ -66,6 +66,7 @@ import { getStandaloneMemoryRuntime } from './memory-runtime-proxy.mjs';
66
66
  import { createBootPhaseProfiler } from './boot-phase-profiler.mjs';
67
67
  import { createDaemonBootCoordinator } from './daemon-boot-coordinator.mjs';
68
68
  import { createDaemonLog } from './daemon-log.mjs';
69
+ import { createDaemonTelemetry } from './daemon-telemetry.mjs';
69
70
  import {
70
71
  compareRuntimeVersions,
71
72
  SESSION_CAPABILITY_FINGERPRINT,
@@ -113,8 +114,8 @@ let shutdownRecheckTimer = null;
113
114
  let replacementRequested = null;
114
115
  const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
115
116
  eventLoopDelay.enable();
116
- let eventLoopLagTimer = null;
117
117
  let idleGc = null;
118
+ let daemonTelemetry = null;
118
119
 
119
120
  function registerMemoryRuntimeLazy() {
120
121
  // Register the shared proxy immediately so daemon shutdown owns its
@@ -181,7 +182,7 @@ async function shutdown(reason, code = 0) {
181
182
  try { await closeProviderStreamJsonPool(reason); } catch (e) { log(`stream parser stop failed: ${e?.message || e}`); }
182
183
  try { await transport?.stop?.(); } catch (e) { log(`transport.stop failed: ${e?.message || e}`); }
183
184
  idleGc?.disarm();
184
- if (eventLoopLagTimer) { clearInterval(eventLoopLagTimer); eventLoopLagTimer = null; }
185
+ daemonTelemetry?.stop();
185
186
  eventLoopDelay.disable();
186
187
  for (const discoveryPath of [DAEMON_DISCOVERY_PATH]) {
187
188
  try { rmSync(discoveryPath, { force: true }); } catch {}
@@ -202,6 +203,29 @@ function inFlightWork() {
202
203
  };
203
204
  }
204
205
 
206
+ daemonTelemetry = createDaemonTelemetry({
207
+ log,
208
+ getWork: inFlightWork,
209
+ onInterval() {
210
+ const status = eventLoopStatus();
211
+ if (status.eventLoopP99Ms >= 250) {
212
+ log(`event-loop lag p95=${status.eventLoopP95Ms}ms p99=${status.eventLoopP99Ms}ms max=${status.eventLoopMaxMs}ms`);
213
+ }
214
+ // Legacy external runtime hosts may still report shard-local lag. The
215
+ // production in-process host is already covered by daemon loop telemetry.
216
+ const shards = sessionRuntimeHost?.status?.shards || [];
217
+ const lagging = shards.filter((shard) => Number(shard?.lag?.p99Ms) >= 250 || shard?.degraded);
218
+ if (lagging.length > 0) {
219
+ log(`session runtime shard lag ${lagging.map((shard) => (
220
+ `#${shard.index}${shard.degraded ? '(quarantined)' : ''}`
221
+ + ` p95=${shard.lag?.p95Ms ?? -1}ms p99=${shard.lag?.p99Ms ?? -1}ms`
222
+ + ` max=${shard.lag?.maxMs ?? -1}ms runtimes=${shard.runtimes}`
223
+ )).join(' ')}`);
224
+ }
225
+ eventLoopDelay.reset();
226
+ },
227
+ });
228
+
205
229
  function daemonHasWorkInFlight() {
206
230
  const work = inFlightWork();
207
231
  return work.activeCalls > 0 || work.queuedCalls > 0
@@ -711,25 +735,11 @@ async function main() {
711
735
  }
712
736
  log(`ready port=${port} pid=${process.pid} in ${(performance.now() - startedAt).toFixed(0)}ms`);
713
737
  bootPhases.mark('daemon-ready');
714
- eventLoopLagTimer = setInterval(() => {
715
- const status = eventLoopStatus();
716
- if (status.eventLoopP99Ms >= 250) {
717
- log(`event-loop lag p95=${status.eventLoopP95Ms}ms p99=${status.eventLoopP99Ms}ms max=${status.eventLoopMaxMs}ms`);
718
- }
719
- // Legacy external runtime hosts may still report shard-local lag. The
720
- // production in-process host is already covered by daemon loop telemetry.
721
- const shards = sessionRuntimeHost?.status?.shards || [];
722
- const lagging = shards.filter((shard) => Number(shard?.lag?.p99Ms) >= 250 || shard?.degraded);
723
- if (lagging.length > 0) {
724
- log(`session runtime shard lag ${lagging.map((shard) => (
725
- `#${shard.index}${shard.degraded ? '(quarantined)' : ''}`
726
- + ` p95=${shard.lag?.p95Ms ?? -1}ms p99=${shard.lag?.p99Ms ?? -1}ms`
727
- + ` max=${shard.lag?.maxMs ?? -1}ms runtimes=${shard.runtimes}`
728
- )).join(' ')}`);
729
- }
730
- eventLoopDelay.reset();
731
- }, 30_000);
732
- eventLoopLagTimer.unref?.();
738
+ // One unref'd 30s loop: memory/work samples plus event-loop lag. A crash
739
+ // still has a recent RSS/heap/limit record in daemon.log; the 10-minute
740
+ // memory-pressure file is too sparse to be that record.
741
+ daemonTelemetry.emit('boot');
742
+ daemonTelemetry.start();
733
743
 
734
744
  // Automation may spawn the shared daemon; keep schedules/webhooks live.
735
745
  // A TUI-spawned daemon keeps the historical eager start (its channels client
@@ -2,7 +2,6 @@
2
2
  // createSession remains the stable public facade; its projection lives in
3
3
  // session-proxy.mjs.
4
4
  import http from 'node:http';
5
- import os from 'node:os';
6
5
  import path from 'node:path';
7
6
  import { fork } from 'node:child_process';
8
7
  import { readFileSync } from 'node:fs';
@@ -20,6 +19,7 @@ import { readSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
20
19
  import { isPidAlive } from '../runtime/shared/pid-liveness.mjs';
21
20
  import { resolveRuntimeRoot } from '../runtime/shared/runtime-root.mjs';
22
21
  import { withHeapCap } from '../runtime/shared/heap-cap.mjs';
22
+ import { beginDaemonSpawnCapture, daemonDataDir } from './daemon-crash-capture.mjs';
23
23
  import { createSessionProxyFactory } from './session-proxy.mjs';
24
24
 
25
25
  function runtimeRoot() {
@@ -31,10 +31,7 @@ export function sessionDiscoveryPath() {
31
31
  }
32
32
 
33
33
  function daemonOwnerPath() {
34
- const dataDir = process.env.MIXDOG_DATA_DIR
35
- ? path.resolve(process.env.MIXDOG_DATA_DIR)
36
- : path.join(process.env.MIXDOG_HOME || path.join(os.homedir(), '.mixdog'), 'data');
37
- return path.join(dataDir, 'daemon-owner.json');
34
+ return path.join(daemonDataDir(), 'daemon-owner.json');
38
35
  }
39
36
 
40
37
  function daemonEntry() {
@@ -238,29 +235,40 @@ async function replaceLowerDaemon(discovery, initialHealth, { log }) {
238
235
 
239
236
  /** Fork one daemon candidate DETACHED (it outlives this client — machine
240
237
  * global) and resolve when it reports ready OR exits (race loss/crash); the
241
- * caller then re-reads discovery and attaches to whoever won. */
242
- function spawnDaemonCandidate({ cwd, log, timeoutMs = 30_000 }) {
238
+ * caller then re-reads discovery and attaches to whoever won.
239
+ *
240
+ * fd 2 is a capture FILE, not a pipe: a V8 fatal abort (heap OOM) is written
241
+ * by the runtime below every JS hook, and a pipe dies with this launcher.
242
+ * The exit listener outlives the ready handoff so the daemon's code/signal is
243
+ * recorded against its pid, boot time and ready state instead of discarded.
244
+ * `entry` is a test seam; production always forks the real daemon. */
245
+ export function spawnDaemonCandidate({ cwd, log, timeoutMs = 30_000, entry = daemonEntry() }) {
243
246
  return new Promise((resolve) => {
244
247
  const t0 = performance.now();
245
248
  const at = () => Math.round(performance.now() - t0);
246
249
  let settled = false;
247
250
  let timer = null;
251
+ const capture = beginDaemonSpawnCapture({ launcher: 'session-client', log });
248
252
  const done = () => {
249
253
  if (settled) return;
250
254
  settled = true;
251
255
  if (timer) clearTimeout(timer);
256
+ // Boot diagnostics written to fd 2 reach this log exactly once, even when
257
+ // the candidate never reached ready.
258
+ capture.mirror();
252
259
  resolve();
253
260
  };
261
+ const detached = daemonShouldDetach();
262
+ // Inherit this launcher's flags (fork's default). withHeapCap('daemon')
263
+ // adds --max-old-space-size only when MIXDOG_DAEMON_HEAP_MB is set.
264
+ const execArgv = withHeapCap('daemon', process.execArgv);
254
265
  let child;
255
266
  try {
256
- child = fork(daemonEntry(), [], {
267
+ child = fork(entry, [], {
257
268
  cwd,
258
- // Inherit this launcher's flags (fork's default) and add the daemon's
259
- // old-space cap on top: uncapped, a long-lived daemon stays resident
260
- // far above its live set.
261
- execArgv: withHeapCap('daemon', process.execArgv),
262
- stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
263
- detached: daemonShouldDetach(),
269
+ execArgv,
270
+ stdio: ['ignore', 'ignore', capture.stderrStdio, 'ipc'],
271
+ detached,
264
272
  windowsHide: true,
265
273
  env: {
266
274
  ...process.env,
@@ -274,30 +282,30 @@ function spawnDaemonCandidate({ cwd, log, timeoutMs = 30_000 }) {
274
282
  },
275
283
  });
276
284
  } catch (err) {
285
+ capture.noteSpawnError(err);
277
286
  log(`daemon spawn failed: ${err?.message || err}`);
278
287
  done();
279
288
  return;
280
289
  }
281
290
  const forkMs = at();
291
+ capture.track(child, { detached, execArgv });
282
292
  child.once('spawn', () => { log(`daemon fork: forkMs=${forkMs} spawnEventMs=${at()}`); });
283
- let firstStderrMs = -1;
284
- const mirror = (chunk) => {
285
- if (firstStderrMs < 0) { firstStderrMs = at(); log(`daemon first stderr at=${firstStderrMs}ms`); }
286
- const text = String(chunk || '').trimEnd();
287
- if (text) log(text);
288
- };
289
- child.stderr?.on('data', mirror);
290
293
  child.once('message', (msg) => {
291
294
  if (msg?.type !== 'ready') return;
295
+ capture.noteReady();
292
296
  log(`daemon ready at=${at()}ms`);
293
- try { child.stderr?.off?.('data', mirror); } catch {}
294
297
  try { child.disconnect?.(); } catch {}
295
298
  try { child.unref?.(); } catch {}
296
299
  try { child.stderr?.unref?.(); } catch {}
297
300
  done();
298
301
  });
299
302
  child.once('exit', done);
300
- child.once('error', (err) => { log(`daemon spawn error: ${err?.message || err}`); done(); });
303
+ child.once('error', (err) => {
304
+ // An async spawn failure may never emit 'exit'; the sidecar still gets it.
305
+ capture.noteSpawnError(err);
306
+ log(`daemon spawn error: ${err?.message || err}`);
307
+ done();
308
+ });
301
309
  timer = setTimeout(done, Math.max(1, timeoutMs));
302
310
  timer.unref?.();
303
311
  });
@@ -171,6 +171,7 @@ function providerDescription(id, group) {
171
171
  case 'openai-oauth': return 'OpenAI OAuth subscription quota';
172
172
  case 'anthropic-oauth': return 'Anthropic OAuth subscription quota';
173
173
  case 'grok-oauth': return 'Grok Build subscription quota';
174
+ case 'antigravity-oauth': return 'Antigravity subscription quota';
174
175
  case 'opencode-go': return 'OpenCode Go subscription quota';
175
176
  case 'openai': return 'OpenAI API billing';
176
177
  case 'anthropic': return 'Anthropic API billing';
@@ -263,6 +264,7 @@ function providerRank(row) {
263
264
  'anthropic-oauth': 20,
264
265
  'grok-oauth': 30,
265
266
  'cursor-oauth': 35,
267
+ 'antigravity-oauth': 36,
266
268
  'opencode-go': 40,
267
269
  openai: 50,
268
270
  anthropic: 60,
@@ -6,38 +6,45 @@ const amount = (value) => Number.isFinite(Number(value)) && Number(value) > 0 ?
6
6
  /** Only retained timestamps establish an hour. Legacy day totals stay unallocated. */
7
7
  export function hourlySeries(records, period, conversationOnly = false) {
8
8
  const make = (key, label, future = false) => ({
9
- key, label, future, turns: 0, tokens: 0, costUsd: 0, unmeasuredTurns: 0, providers: new Map(),
9
+ key, label, future, turns: 0, tokens: 0, costUsd: 0, costKnownTurns: 0, unmeasuredTurns: 0, providers: new Map(),
10
10
  });
11
11
  const buckets = [];
12
- // Real elapsed hours also handle 23/25-hour daylight-saving days.
13
- for (let ts = period.fromMs; ts <= period.endMs; ts += HOUR_MS) {
14
- const label = `${String(new Date(ts).getHours()).padStart(2, '0')}:00`;
15
- buckets.push(make(String(ts), label, ts > period.toMs));
12
+ // Rolling windows always contain 24 elapsed hours, even across DST changes.
13
+ for (let ts = period.fromMs; ts < period.endMs; ts += HOUR_MS) {
14
+ const date = new Date(ts);
15
+ const label = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
16
+ buckets.push({ ...make(String(ts), label, ts > period.toMs),
17
+ fromMs: ts, toMs: Math.min(ts + HOUR_MS, period.toMs) });
16
18
  }
17
- const add = (bucket, provider, turns, tokens, costUsd, unmeasuredTurns = 0) => {
19
+ const add = (bucket, provider, turns, tokens, costUsd, unmeasuredTurns = 0, costKnownTurns = 0) => {
18
20
  bucket.turns += turns;
19
21
  bucket.tokens += tokens;
20
22
  bucket.costUsd += costUsd;
23
+ bucket.costKnownTurns += costKnownTurns;
21
24
  bucket.unmeasuredTurns += unmeasuredTurns;
22
- const slice = bucket.providers.get(provider) || { provider, turns: 0, tokens: 0, costUsd: 0, unmeasuredTurns: 0 };
25
+ const slice = bucket.providers.get(provider) || { provider, turns: 0, tokens: 0, costUsd: 0, costKnownTurns: 0, unmeasuredTurns: 0 };
23
26
  slice.turns += turns;
24
27
  slice.tokens += tokens;
25
28
  slice.costUsd += costUsd;
29
+ slice.costKnownTurns += costKnownTurns;
26
30
  slice.unmeasuredTurns += unmeasuredTurns;
27
31
  bucket.providers.set(provider, slice);
28
32
  };
29
33
  for (const row of records?.rows || []) {
30
34
  if (row.ts < period.fromMs || row.ts > period.toMs) continue;
31
35
  if (conversationOnly && !isConversationUsageSource(row.source_type)) continue;
32
- const bucket = buckets[Math.floor((row.ts - period.fromMs) / HOUR_MS)];
33
- if (bucket) add(bucket, row.provider, 1, amount(row.input) + amount(row.output), amount(row.cost_usd),
34
- amount(row.unmeasuredTurns));
36
+ const bucket = buckets[Math.min(buckets.length - 1, Math.floor((row.ts - period.fromMs) / HOUR_MS))];
37
+ if (bucket) add(bucket, row.provider, 1,
38
+ amount(row.input) + amount(row.output) + amount(row.cache_read) + amount(row.cache_write), amount(row.cost_usd),
39
+ amount(row.unmeasuredTurns), row.cost_usd != null && !row.unmeasuredTurns ? 1 : 0);
35
40
  }
36
41
  const unknown = { ...make('unknown-time', ''), unknown: true };
37
42
  for (const route of records?.unallocated || []) {
38
43
  const usage = conversationOnly ? route.conversation : route;
39
44
  if (usage) add(unknown, route.provider, amount(usage.turns),
40
- amount(usage.input) + amount(usage.output), amount(usage.costUsd), amount(usage.unmeasuredTurns));
45
+ amount(usage.input) + amount(usage.output) + amount(usage.cacheRead) + amount(usage.cacheWrite),
46
+ amount(usage.costUsd), amount(usage.unmeasuredTurns),
47
+ usage.costKnownTurns == null ? (amount(usage.costUsd) > 0 ? amount(usage.turns) : 0) : amount(usage.costKnownTurns));
41
48
  }
42
49
  if (unknown.turns || unknown.tokens || unknown.costUsd) buckets.push(unknown);
43
50
  return buckets.map((bucket) => ({
@@ -151,6 +151,8 @@ function addUsage(target, usage) {
151
151
  target.cacheWrite += usage.cacheWrite;
152
152
  target.costUsd += usage.costUsd;
153
153
  target.costKnownTurns = (target.costKnownTurns || 0) + (usage.costKnownTurns || 0);
154
+ target.costBilled = (target.costBilled || 0) + (usage.costBilled || 0);
155
+ target.costEstimated = (target.costEstimated || 0) + (usage.costEstimated || 0);
154
156
  target.unmeasuredTurns = (target.unmeasuredTurns || 0) + (usage.unmeasuredTurns || 0);
155
157
  }
156
158
 
@@ -170,6 +172,9 @@ function foldRoute(state, providerId, modelId, kind, usage) {
170
172
  state.cacheRead += usage.cacheRead;
171
173
  state.cacheWrite += usage.cacheWrite;
172
174
  state.costUsd += usage.costUsd;
175
+ state.costKnownTurns += usage.costKnownTurns || 0;
176
+ state.costBilled += usage.costBilled || 0;
177
+ state.costEstimated += usage.costEstimated || 0;
173
178
  state.unmeasuredTurns += usage.unmeasuredTurns || 0;
174
179
  }
175
180
 
@@ -216,9 +221,6 @@ function foldRollupDay(state, key, day, conversationOnly) {
216
221
  if (day?.restored === true) state.historyDays += 1;
217
222
  if (day?.importedPartial === true) state.partialDays += 1;
218
223
  if (conversationOnly && day?.conversationPartial === true) state.partialDays += 1;
219
- state.costKnownTurns += num(totals.costKnownTurns);
220
- state.costBilled += num(totals.costBilled);
221
- state.costEstimated += num(totals.costEstimated);
222
224
  state.durationMs += num(totals.durationMs);
223
225
  state.durationTurns += num(totals.durationTurns);
224
226
  state.sessionsDropped += num(day?.sessionsDropped);
@@ -241,6 +243,9 @@ function foldRollupDay(state, key, day, conversationOnly) {
241
243
  costUsd: num(route.costUsd),
242
244
  costKnownTurns: route.costKnownTurns == null
243
245
  ? (num(route.costUsd) > 0 ? num(route.turns) : 0) : num(route.costKnownTurns),
246
+ costBilled: num(route.costBilled),
247
+ costEstimated: route.costEstimated == null
248
+ ? Math.max(0, num(route.costUsd) - num(route.costBilled)) : num(route.costEstimated),
244
249
  sessions: route.sessions,
245
250
  sessionsComplete: route.sessionsComplete,
246
251
  unmeasuredTurns: num(route.unmeasuredTurns),
@@ -277,6 +282,9 @@ function foldEvent(state, key, event, conversationOnly) {
277
282
  const cacheRead = num(event?.cacheReadTokens);
278
283
  const cacheWrite = num(event?.cacheWriteTokens);
279
284
  const sessionId = conversation ? text(event?.sessionId) : '';
285
+ const costSource = text(event?.costSource);
286
+ const priced = event?.costUsd != null && Number.isFinite(Number(event.costUsd))
287
+ && !['', 'none', 'unpriced'].includes(costSource);
280
288
  const usage = {
281
289
  turns: 1,
282
290
  // Same normalization the rollup applies: a provider that reports the whole
@@ -286,19 +294,15 @@ function foldEvent(state, key, event, conversationOnly) {
286
294
  cacheRead,
287
295
  cacheWrite,
288
296
  costUsd: num(event?.costUsd),
289
- costKnownTurns: event?.costSource && event.costSource !== 'none' ? 1 : 0,
297
+ costKnownTurns: priced ? 1 : 0,
298
+ costBilled: priced && costSource === 'provider' ? num(event.costUsd) : 0,
299
+ costEstimated: priced && costSource !== 'provider' ? num(event.costUsd) : 0,
290
300
  sessions: sessionId ? { [sessionId]: 1 } : {},
291
301
  sessionsComplete: !conversation || Boolean(sessionId),
292
302
  };
293
303
  foldRoute(state, providerId, modelId, text(event?.providerKind), usage);
294
304
  addDaily(state, key, usage, providerId);
295
305
 
296
- const costSource = text(event?.costSource);
297
- if (costSource && costSource !== 'none') {
298
- state.costKnownTurns += 1;
299
- if (costSource === 'provider') state.costBilled += usage.costUsd;
300
- else state.costEstimated += usage.costUsd;
301
- }
302
306
  const durationMs = num(event?.durationMs);
303
307
  if (durationMs > 0) {
304
308
  state.durationMs += durationMs;
@@ -417,15 +421,17 @@ function median(values) {
417
421
  : sorted[middle];
418
422
  }
419
423
 
420
- // Cache reads outweigh everything else by two orders of magnitude on a long
421
- // session, so "tokens" counts what a turn actually read fresh or produced.
422
- // Cache is reported beside that figure instead of swallowing it.
424
+ // "tokens" is everything a turn moved: fresh input, output, and the cached
425
+ // prompt it read or wrote. Cached providers re-read the whole context every
426
+ // turn, so counting only the fresh part made a cached route look idle beside
427
+ // an uncached one doing the same work. The split stays in input/output/cache.
423
428
  function tokensOf(bucket) {
424
- return bucket.input + bucket.output;
429
+ return bucket.input + bucket.output + bucket.cacheRead + bucket.cacheWrite;
425
430
  }
426
431
 
427
432
  function exportRoute(bucket, totalTokens) {
428
433
  const tokens = tokensOf(bucket);
434
+ const fresh = bucket.input + bucket.output;
429
435
  const prompt = bucket.input + bucket.cacheRead + bucket.cacheWrite;
430
436
  const unmeasuredTurns = num(bucket.unmeasuredTurns);
431
437
  const unknown = unmeasuredTurns > 0 && unmeasuredTurns === bucket.turns;
@@ -443,17 +449,20 @@ function exportRoute(bucket, totalTokens) {
443
449
  tokens,
444
450
  unmeasuredTurns,
445
451
  costUsd: round(bucket.costUsd, 6),
452
+ costBilled: round(num(bucket.costBilled), 6),
453
+ costEstimated: round(num(bucket.costEstimated), 6),
454
+ costKnownTurns: num(bucket.costKnownTurns),
455
+ costUnpricedTurns: Math.max(0, bucket.turns - num(bucket.costKnownTurns)),
446
456
  costCoverage: bucket.turns > 0 ? num(bucket.costKnownTurns) / bucket.turns : 0,
447
457
  share: unmeasuredTurns > 0 ? null : totalTokens > 0 ? round(tokens / totalTokens, 6) : 0,
448
458
  // How much of this route's prompt arrived from cache instead of being read
449
459
  // again. Cache writes are misses, so belong in the denominator, not the
450
460
  // numerator.
451
461
  cacheHitRate: unmeasuredTurns > 0 ? null : prompt > 0 ? round(bucket.cacheRead / prompt, 4) : 0,
452
- // What a million tokens actually cost on this route. Cache is excluded from
453
- // the divisor for the same reason it is excluded from the token figure —
454
- // including it would divide real spend by a number two orders of magnitude
455
- // larger and rank every route as free.
456
- costPerMTokens: tokens > 0 ? round(bucket.costUsd / (tokens / 1_000_000), 4) : 0,
462
+ // What a million fresh tokens actually cost on this route. Cache is excluded
463
+ // from the divisor: including it would divide real spend by a number two
464
+ // orders of magnitude larger and rank every route as free.
465
+ costPerMTokens: fresh > 0 ? round(bucket.costUsd / (fresh / 1_000_000), 4) : 0,
457
466
  // Answer length, which is what separates a terse route from a verbose one
458
467
  // at the same price.
459
468
  outputPerTurn: bucket.turns > 0 ? Math.round(bucket.output / bucket.turns) : 0,
@@ -492,8 +501,8 @@ export function usageStatsSnapshot({
492
501
  const eventList = Array.isArray(events) ? events : [];
493
502
  const state = collect({ events: eventList, rollupDays, historyDays, window, conversationOnly });
494
503
 
495
- const totalTokens = state.input + state.output;
496
504
  const cacheTokens = state.cacheRead + state.cacheWrite;
505
+ const totalTokens = state.input + state.output + cacheTokens;
497
506
  const sessionTotals = [...state.sessions.values(), ...state.anonymousSessions];
498
507
  const dayKeys = [...state.dayKeys].sort();
499
508
  const firstDay = dayKeys[0] || '';
@@ -531,22 +540,24 @@ export function usageStatsSnapshot({
531
540
  const bucket = state.daily.get(day);
532
541
  const future = period ? { future: day > endDay } : {};
533
542
  if (!bucket) {
534
- return { day, turns: 0, tokens: 0, cacheTokens: 0, costUsd: 0, providers: [], ...future };
543
+ return { day, turns: 0, tokens: 0, cacheTokens: 0, costUsd: 0, costKnownTurns: 0, providers: [], ...future };
535
544
  }
536
545
  return {
537
546
  day: bucket.day,
538
547
  turns: bucket.turns,
539
- tokens: bucket.input + bucket.output,
548
+ tokens: tokensOf(bucket),
540
549
  cacheTokens: bucket.cacheRead + bucket.cacheWrite,
541
550
  costUsd: round(bucket.costUsd, 6),
551
+ costKnownTurns: num(bucket.costKnownTurns),
542
552
  unmeasuredTurns: num(bucket.unmeasuredTurns),
543
553
  ...future,
544
554
  providers: [...bucket.providers.values()]
545
555
  .map((slice) => ({
546
556
  provider: slice.provider,
547
557
  turns: slice.turns,
548
- tokens: slice.input + slice.output,
558
+ tokens: tokensOf(slice),
549
559
  costUsd: round(slice.costUsd, 6),
560
+ costKnownTurns: num(slice.costKnownTurns),
550
561
  unmeasuredTurns: num(slice.unmeasuredTurns),
551
562
  }))
552
563
  .sort((a, b) => b.tokens - a.tokens),
@@ -584,21 +595,21 @@ export function usageStatsSnapshot({
584
595
  cacheRead: state.cacheRead,
585
596
  cacheWrite: state.cacheWrite,
586
597
  cacheTokens,
587
- // Everything that moved, cache included — the figure to quote when the
588
- // question is how much data flowed rather than what it cost.
589
- totalTokens: totalTokens + cacheTokens,
598
+ totalTokens,
590
599
  // How much of the prompt arrived from cache instead of being read again.
591
600
  cacheHitRate: state.unmeasuredTurns > 0 ? null : state.input + state.cacheRead + state.cacheWrite > 0
592
601
  ? round(state.cacheRead / (state.input + state.cacheRead + state.cacheWrite), 4)
593
602
  : 0,
594
603
  costUsd: round(state.costUsd, 6),
604
+ costKnownTurns: state.costKnownTurns,
605
+ costUnpricedTurns: Math.max(0, state.turns - state.costKnownTurns),
595
606
  // Priced by the provider vs derived from the catalog. A subscription turn
596
607
  // lands in the second: real spend, but never an invoice line.
597
608
  costBilled: round(state.costBilled, 6),
598
609
  costEstimated: round(state.costEstimated, 6),
599
610
  costPerDay: effectiveDays > 0 ? round(state.costUsd / effectiveDays, 6) : 0,
600
611
  tokensPerSession: sessionTotals.length
601
- ? Math.round((totalTokens + cacheTokens) / sessionTotals.length)
612
+ ? Math.round(totalTokens / sessionTotals.length)
602
613
  : 0,
603
614
  medianTokensPerSession: Math.round(median(sessionTotals)),
604
615
  avgDurationMs: state.durationTurns > 0
@@ -610,7 +621,7 @@ export function usageStatsSnapshot({
610
621
  previous: prior
611
622
  ? {
612
623
  turns: prior.turns,
613
- tokens: prior.input + prior.output,
624
+ tokens: tokensOf(prior),
614
625
  costUsd: round(prior.costUsd, 6),
615
626
  }
616
627
  : null,