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.
- package/package.json +1 -1
- package/scripts/audit-usage-ledger.mjs +86 -0
- package/scripts/generate-voice-runtime-manifest.mjs +22 -17
- package/scripts/memory-chunk-quality.mjs +128 -0
- package/scripts/memory-layer2-check.mjs +133 -0
- package/scripts/model-catalog-audit.mjs +6 -0
- package/scripts/native-tool-download.mjs +14 -0
- package/scripts/prepare-native-assets.mjs +2 -7
- package/scripts/tool-failures.mjs +7 -2
- package/scripts/verify-release-assets.mjs +48 -90
- package/src/defaults/skills/browser-use/SKILL.md +33 -18
- package/src/defaults/skills/computer-use/SKILL.md +16 -8
- package/src/lib/keychain-cjs.cjs +0 -1
- package/src/lib/rules-builder.cjs +4 -1
- package/src/rules/agent/40-cycle1-agent.md +21 -13
- package/src/rules/lead/01-general.md +8 -3
- package/src/rules/shared/10-tool-workflow.md +20 -11
- package/src/rules/shared/30-exploration.md +11 -4
- package/src/rules/shared/40-editing.md +2 -3
- package/src/rules/shared/50-execution.md +6 -3
- package/src/rules/shared/60-verification.md +3 -2
- package/src/rules/shared/70-delivery.md +0 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +51 -45
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +3 -4
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -7
- package/src/runtime/agent/orchestrator/agent-trace.mjs +9 -0
- package/src/runtime/agent/orchestrator/config.mjs +9 -4
- package/src/runtime/agent/orchestrator/context/collect.mjs +25 -1055
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +272 -0
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +296 -0
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +523 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +13 -14
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +214 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +220 -116
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +74 -44
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +191 -29
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +13 -12
- package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +1 -9
- package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +48 -25
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +28 -2
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +1 -8
- package/src/runtime/agent/orchestrator/providers/lib/action-input-contract.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +21 -6
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +61 -13
- package/src/runtime/agent/orchestrator/providers/model-catalog-projection.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +123 -118
- package/src/runtime/agent/orchestrator/providers/model-pricing-rates.mjs +54 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +34 -3
- package/src/runtime/agent/orchestrator/providers/openai-codex-endpoints.mjs +21 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +154 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +322 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +243 -702
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-startup-prewarm.mjs +221 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/provider-model-identities.mjs +40 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +120 -710
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +36 -15
- package/src/runtime/agent/orchestrator/session/cache/util.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +17 -4
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +36 -31
- package/src/runtime/agent/orchestrator/session/compact.mjs +4 -5
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +6 -5
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +121 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/loop/diagnostics.mjs +74 -0
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +44 -17
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +266 -0
- package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +111 -0
- package/src/runtime/agent/orchestrator/session/loop/request-projection.mjs +99 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +20 -11
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +3 -5
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +39 -0
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +21 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -25
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +62 -10
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +17 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +61 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +13 -9
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +8 -3
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +69 -6
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +14 -17
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/bridge-discovery.mjs +8 -0
- package/src/runtime/browser-bridge/action-schema.mjs +37 -43
- package/src/runtime/browser-bridge/client.mjs +111 -99
- package/src/runtime/browser-bridge/input-fields.mjs +187 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +12 -227
- package/src/runtime/computer-bridge/action-schema.mjs +2 -46
- package/src/runtime/computer-bridge/client.mjs +69 -77
- package/src/runtime/computer-bridge/core-actions.mjs +7 -5
- package/src/runtime/computer-bridge/error-recovery.mjs +41 -37
- package/src/runtime/computer-bridge/tool-defs.mjs +1 -1
- package/src/runtime/github/commands.mjs +4 -1
- package/src/runtime/github/contract.mjs +1 -1
- package/src/runtime/github/tool.mjs +2 -2
- package/src/runtime/media/adapters/antigravity-image.mjs +72 -0
- package/src/runtime/media/adapters/gemini-image.mjs +20 -11
- package/src/runtime/media/auth.mjs +12 -0
- package/src/runtime/media/catalog.mjs +30 -10
- package/src/runtime/media/jobs.mjs +4 -0
- package/src/runtime/media/lanes.mjs +40 -31
- package/src/runtime/memory/index.mjs +2 -9
- package/src/runtime/memory/lib/compact-handoff.mjs +26 -18
- package/src/runtime/memory/lib/memory-chunk-audit.mjs +41 -0
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +388 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +95 -394
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +29 -4
- package/src/runtime/memory/lib/memory-recall-store.mjs +8 -12
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -11
- package/src/runtime/memory/lib/memory.mjs +2 -0
- package/src/runtime/memory/lib/query-handlers.mjs +98 -144
- package/src/runtime/memory/lib/query-ranking.mjs +23 -9
- package/src/runtime/memory/lib/recall-format.mjs +37 -27
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +14 -8
- package/src/runtime/memory/lib/session-ingest.mjs +45 -84
- package/src/runtime/memory/lib/transcript-ingest.mjs +19 -21
- package/src/runtime/memory/tool-defs.mjs +2 -2
- package/src/runtime/office/authoring/pptx-author-session.mjs +3 -7
- package/src/runtime/office/core/office-actions-batch.mjs +2 -8
- package/src/runtime/office/core/office-actions-open.mjs +124 -0
- package/src/runtime/office/core/office-actions-read.mjs +127 -0
- package/src/runtime/office/core/office-core.mjs +80 -1
- package/src/runtime/office/core/office-sessions.mjs +116 -138
- package/src/runtime/office/index.mjs +18 -271
- package/src/runtime/office/pdf/pdf-analysis.mjs +2 -2
- package/src/runtime/office/pdf/pdf-limits.mjs +2 -0
- package/src/runtime/office/tool-defs.mjs +7 -6
- package/src/runtime/shared/agent-route-config.mjs +8 -8
- package/src/runtime/shared/child-spawn-gate.mjs +10 -13
- package/src/runtime/shared/config.mjs +2 -7
- package/src/runtime/shared/env.mjs +3 -3
- package/src/runtime/shared/err-text.mjs +1 -1
- package/src/runtime/shared/extension-scopes.mjs +3 -2
- package/src/runtime/shared/heap-cap.mjs +10 -6
- package/src/runtime/shared/llm/cost.mjs +35 -10
- package/src/runtime/shared/llm/usage-accounting.mjs +11 -1
- package/src/runtime/shared/llm/usage-context.mjs +6 -0
- package/src/runtime/shared/llm/usage-ledger-import.mjs +5 -0
- package/src/runtime/shared/llm/usage-ledger-repair.mjs +109 -0
- package/src/runtime/shared/llm/usage-ledger.mjs +59 -20
- package/src/runtime/shared/llm/usage-pricing-refresh.mjs +26 -0
- package/src/runtime/shared/llm/usage-rollup.mjs +17 -15
- package/src/runtime/shared/native-asset.mjs +3 -4
- package/src/runtime/shared/object.mjs +4 -0
- package/src/runtime/shared/pg-schema-db.mjs +21 -0
- package/src/runtime/shared/plugin-metadata.mjs +3 -1
- package/src/runtime/shared/resource-admission.mjs +1 -6
- package/src/runtime/shared/schedules-db.mjs +8 -24
- package/src/runtime/shared/schema-value-error.d.mts +2 -0
- package/src/runtime/shared/schema-value-error.mjs +68 -0
- package/src/runtime/shared/singleton-owner.mjs +11 -24
- package/src/runtime/shared/staged-update.mjs +1 -10
- package/src/runtime/shared/tool-card-model.mjs +8 -5
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +1 -5
- package/src/runtime/shared/user-data-guard.mjs +1 -4
- package/src/runtime/shared/webhooks-db.mjs +8 -24
- package/src/runtime/web-search/index.mjs +32 -8
- package/src/runtime/web-search/tool-defs.mjs +4 -4
- package/src/session-runtime/config-lifecycle.mjs +34 -0
- package/src/session-runtime/context-status.mjs +19 -0
- package/src/session-runtime/cwd-plugins.mjs +12 -29
- package/src/session-runtime/goal-tasks.mjs +2 -2
- package/src/session-runtime/goal-tool-defs.mjs +3 -2
- package/src/session-runtime/internal-tool-executor.mjs +7 -0
- package/src/session-runtime/lifecycle-api.mjs +5 -11
- package/src/session-runtime/notification-bus.mjs +41 -0
- package/src/session-runtime/prewarm.mjs +22 -0
- package/src/session-runtime/runtime-bootstrap.mjs +90 -0
- package/src/session-runtime/runtime-core.mjs +88 -202
- package/src/session-runtime/runtime-feature-gates.mjs +37 -0
- package/src/session-runtime/session-lifecycle.mjs +5 -5
- package/src/session-runtime/session-turn-api.mjs +1 -12
- package/src/session-runtime/setup-tool/executor.mjs +13 -5
- package/src/session-runtime/setup-tool/tool-defs.mjs +52 -46
- package/src/session-runtime/tool-defs.mjs +73 -4
- package/src/session-runtime/usage-stats-api.mjs +4 -1
- package/src/session-runtime/warmup-schedulers.mjs +8 -2
- package/src/session-runtime/workflow.mjs +14 -0
- package/src/standalone/channel-session-router.mjs +3 -1
- package/src/standalone/channel-worker.mjs +18 -9
- package/src/standalone/daemon-crash-capture.mjs +534 -0
- package/src/standalone/daemon-telemetry.mjs +175 -0
- package/src/standalone/daemon.mjs +31 -21
- package/src/standalone/session-client.mjs +31 -23
- package/src/standalone/usage-dashboard-model.mjs +2 -0
- package/src/standalone/usage-stats-hours.mjs +18 -11
- package/src/standalone/usage-stats-model.mjs +39 -28
- package/src/standalone/usage-stats-period.mjs +43 -45
- package/src/tui/app/use-transcript-window.mjs +1 -1
- package/src/tui/components/StatusLine.jsx +13 -42
- package/src/tui/dist/index.mjs +256 -239
- package/src/tui/session/session-api-ext.mjs +44 -41
- package/src/tui/session/session-api.mjs +4 -0
- package/src/tui/session/tool-card-results.mjs +25 -78
- package/src/tui/session/tool-result-status.mjs +94 -1
- package/src/tui/session/turn-aggregate-cards.mjs +17 -54
- package/src/ui/statusline-format.mjs +6 -26
- 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
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
267
|
+
child = fork(entry, [], {
|
|
257
268
|
cwd,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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) => {
|
|
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
|
-
//
|
|
13
|
-
for (let ts = period.fromMs; ts
|
|
14
|
-
const
|
|
15
|
-
|
|
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,
|
|
34
|
-
amount(row.
|
|
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)
|
|
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:
|
|
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
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
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
|
|
453
|
-
// the divisor
|
|
454
|
-
//
|
|
455
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
624
|
+
tokens: tokensOf(prior),
|
|
614
625
|
costUsd: round(prior.costUsd, 6),
|
|
615
626
|
}
|
|
616
627
|
: null,
|