mixdog 1.3.3 → 1.3.5
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/LICENSES/oh-my-pi-MIT.txt +23 -0
- package/NOTICE.md +15 -0
- package/README.md +11 -0
- package/package.json +3 -2
- package/scripts/embed-google-oauth-client.mjs +49 -0
- package/scripts/lib/test-spawn-ledger.mjs +46 -1
- package/scripts/test.mjs +5 -0
- package/src/defaults/shipped-history.json +8 -0
- package/src/defaults/skills/browser-use/SKILL.md +21 -9
- package/src/defaults/skills/setup/SKILL.md +7 -2
- package/src/defaults/skills/setup/references/actions.md +82 -27
- package/src/defaults/skills/setup/references/surfaces.md +67 -34
- package/src/defaults/skills/setup-guide/SKILL.md +3 -3
- package/src/lib/keychain-cjs.cjs +359 -29
- package/src/rules/agent-mode/AGENT_MODE.md +10 -2
- package/src/rules/agent-mode/MIXY.md +12 -0
- package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
- package/src/runtime/agent/orchestrator/config.mjs +18 -0
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
- package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
- package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
- package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
- package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
- package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
- package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
- package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
- package/src/runtime/channels/lib/scheduler.mjs +109 -14
- package/src/runtime/channels/lib/schedules-db.mjs +49 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
- package/src/runtime/channels/lib/worker-main.mjs +9 -0
- package/src/runtime/office/com/com-adapter.mjs +17 -1
- package/src/runtime/office/com/office-com-host.ps1 +51 -1
- package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
- package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
- package/src/runtime/office/pdf/document-preview.mjs +326 -8
- package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
- package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
- package/src/runtime/office/portable/portable-soffice.mjs +45 -5
- package/src/runtime/shared/agent-room-host.mjs +1 -1
- package/src/runtime/shared/bot-hops.mjs +62 -2
- package/src/runtime/shared/config.mjs +49 -22
- package/src/runtime/shared/connector-catalog.d.mts +4 -2
- package/src/runtime/shared/connector-catalog.mjs +16 -8
- package/src/runtime/shared/developer-options.mjs +32 -1
- package/src/runtime/shared/google-oauth-client.generated.json +4 -0
- package/src/runtime/shared/google-oauth-client.mjs +34 -9
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
- package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
- package/src/runtime/shared/llm/well-formed.mjs +66 -0
- package/src/runtime/shared/provider-accounts.mjs +224 -58
- package/src/runtime/shared/provider-api-key.mjs +19 -8
- package/src/runtime/shared/routine-trigger.mjs +42 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -0
- package/src/runtime/shared/transcript-metadata.mjs +38 -0
- package/src/runtime/shared/user-data-guard.mjs +30 -2
- package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
- package/src/session-runtime/agent-home-delivery.mjs +57 -13
- package/src/session-runtime/agent-room-delivery.mjs +28 -1
- package/src/session-runtime/agent-task-sessions.mjs +292 -79
- package/src/session-runtime/agent-tool-routing.mjs +49 -4
- package/src/session-runtime/boot/begin.mjs +3 -2
- package/src/session-runtime/boot/tools.mjs +4 -0
- package/src/session-runtime/bot-chain.mjs +32 -0
- package/src/session-runtime/bot-status.mjs +7 -4
- package/src/session-runtime/channel-config-api.mjs +63 -17
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
- package/src/session-runtime/internal-tool-executor.mjs +4 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
- package/src/session-runtime/mixy-heartbeat.mjs +807 -0
- package/src/session-runtime/model-route/route-tuning.mjs +10 -6
- package/src/session-runtime/owned-automation-run.mjs +33 -12
- package/src/session-runtime/provider-auth/login.mjs +7 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
- package/src/session-runtime/routine-home-turn.mjs +33 -6
- package/src/session-runtime/schedule-session-run.mjs +7 -3
- package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
- package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +11 -2
- package/src/session-runtime/services/agent-tool/render.mjs +3 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
- package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
- package/src/session-runtime/services/channel-admin.mjs +66 -18
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +128 -25
- package/src/session-runtime/services/schedule-access.mjs +14 -0
- package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
- package/src/session-runtime/session-create/session-options.mjs +7 -4
- package/src/session-runtime/settings-system-api.mjs +16 -1
- package/src/session-runtime/setup-tool/executor.mjs +49 -14
- package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
- package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
- package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
- package/src/session-runtime/task-session-route.mjs +99 -0
- package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
- package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
- package/src/session-runtime/tool-surface.mjs +4 -0
- package/src/session-runtime/usage-stats-api.mjs +1 -1
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
- package/src/session-runtime/workspace/transitions.mjs +10 -2
- package/src/standalone/daemon-session-control.mjs +88 -20
- package/src/standalone/daemon.mjs +8 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
- package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
- package/src/standalone/session-service.mjs +13 -0
- package/src/standalone/session-tool-output.mjs +8 -5
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/dist/index.mjs +335 -172
- package/src/tui/session/notice-surface.mjs +17 -1
- package/src/tui/session/session-api/integrations.mjs +1 -0
- package/src/tui/session/session-api/lifecycle.mjs +8 -0
- package/src/tui/session/session-api/settings.mjs +14 -2
- package/src/tui/session/turn-stream.mjs +17 -1
- package/src/tui/session/turn.mjs +22 -2
- package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
- package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
- package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
|
@@ -1,78 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* right now". Totals for a month ago cannot be recovered from it once the cap
|
|
7
|
-
* evicts those rows, so every turn is ALSO folded into a per-day bucket here.
|
|
8
|
-
* A bucket is a few numbers, which keeps years of history in one small file.
|
|
9
|
-
*
|
|
10
|
-
* Most turns on this machine are NOT the user typing: background runners
|
|
11
|
-
* (memory cycles, schedules, webhooks) spend tokens on their own and outnumber
|
|
12
|
-
* conversation turns several times over. Every bucket therefore carries a
|
|
13
|
-
* `conversation` sub-total beside the full one, so "what did I spend" can be
|
|
14
|
-
* answered without the background drowning it.
|
|
15
|
-
*
|
|
16
|
-
* Session token totals are kept id-keyed only while a day is recent; older days
|
|
17
|
-
* keep the anonymous per-session totals alone. Only conversation sessions are
|
|
18
|
-
* tracked: a background runner mints a fresh id per cycle, which would turn
|
|
19
|
-
* "how many sessions" into a count of cycles.
|
|
20
|
-
*
|
|
21
|
-
* `firstTs`/`lastTs` bound the turns already folded into a day. A reader that
|
|
22
|
-
* also holds the raw event list adds ONLY the events outside that span, which
|
|
23
|
-
* is what lets both stores be read together without counting a turn twice.
|
|
24
|
-
*
|
|
25
|
-
* Shape:
|
|
26
|
-
* {
|
|
27
|
-
* version, updatedAt,
|
|
28
|
-
* days: {
|
|
29
|
-
* "2026-08-12": {
|
|
30
|
-
* turns, input, output, cacheRead, cacheWrite,
|
|
31
|
-
* costUsd, costBilled, costEstimated, costKnownTurns,
|
|
32
|
-
* durationMs, durationTurns, firstTs, lastTs,
|
|
33
|
-
* conversation: { …the same turn totals, user-driven turns only },
|
|
34
|
-
* conversationPartial,
|
|
35
|
-
* models: { "<provider>/<model>": {
|
|
36
|
-
* provider, model, kind, turns, input, output, cacheRead, cacheWrite,
|
|
37
|
-
* costUsd, conversation: { …route totals }
|
|
38
|
-
* } },
|
|
39
|
-
* sessions: { "<sessionId>": <tokens> },
|
|
40
|
-
* sessionTokens: [<tokens>, …],
|
|
41
|
-
* sessionsDropped
|
|
42
|
-
* }
|
|
43
|
-
* }
|
|
44
|
-
* }
|
|
2
|
+
* Day keys, conversation-source classification and numeric helpers shared by the
|
|
3
|
+
* usage ledger and the stats model. The daily JSON rollup these helpers were
|
|
4
|
+
* written for is no longer produced; its past day records are still imported
|
|
5
|
+
* by usage-ledger-import.mjs.
|
|
45
6
|
*/
|
|
46
|
-
import { billableInputTokensForProvider } from './cost.mjs';
|
|
47
|
-
|
|
48
|
-
const USAGE_ROLLUP_VERSION = 2;
|
|
49
|
-
|
|
50
|
-
// A day costs well under a kilobyte, so a long retention stays cheap while
|
|
51
|
-
// making "all time" mean something closer to the install date.
|
|
52
|
-
const RETENTION_DAYS = 400;
|
|
53
|
-
// Session ids are the only identifying values in the file; drop them once the
|
|
54
|
-
// day is old enough that per-session drill-down no longer matters.
|
|
55
|
-
const SESSION_DETAIL_DAYS = 45;
|
|
56
|
-
const MAX_MODELS_PER_DAY = 80;
|
|
57
|
-
const MAX_SESSION_IDS_PER_DAY = 400;
|
|
58
|
-
const MAX_SESSION_TOKENS_PER_DAY = 400;
|
|
59
|
-
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
60
|
-
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
61
7
|
|
|
62
8
|
export function num(value) {
|
|
63
9
|
const n = Number(value);
|
|
64
10
|
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
65
11
|
}
|
|
66
12
|
|
|
67
|
-
export function cleanId(value) {
|
|
68
|
-
const text = typeof value === 'string' ? value.trim() : '';
|
|
69
|
-
return text && !UNSAFE_KEYS.has(text) ? text.slice(0, 200) : '';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function round6(value) {
|
|
73
|
-
return Math.round(value * 1e6) / 1e6;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
13
|
/** Local calendar day, so "today" in the UI means the user's today. */
|
|
77
14
|
export function usageRollupDayKey(ts) {
|
|
78
15
|
const date = new Date(Number.isFinite(Number(ts)) ? Number(ts) : Date.now());
|
|
@@ -92,324 +29,8 @@ export function isConversationUsageSource(sourceType) {
|
|
|
92
29
|
return !BACKGROUND_SOURCES.has(id);
|
|
93
30
|
}
|
|
94
31
|
|
|
95
|
-
function emptyRouteTotals() {
|
|
96
|
-
return {
|
|
97
|
-
turns: 0,
|
|
98
|
-
input: 0,
|
|
99
|
-
output: 0,
|
|
100
|
-
cacheRead: 0,
|
|
101
|
-
cacheWrite: 0,
|
|
102
|
-
costUsd: 0,
|
|
103
|
-
costBilled: 0,
|
|
104
|
-
costEstimated: 0,
|
|
105
|
-
costKnownTurns: 0,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function emptyTurnTotals() {
|
|
110
|
-
return {
|
|
111
|
-
...emptyRouteTotals(),
|
|
112
|
-
durationMs: 0,
|
|
113
|
-
durationTurns: 0,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function addRouteTotals(target, turn, { priced, billed } = {}) {
|
|
118
|
-
target.turns += turn.turns;
|
|
119
|
-
target.input += turn.input;
|
|
120
|
-
target.output += turn.output;
|
|
121
|
-
target.cacheRead += turn.cacheRead;
|
|
122
|
-
target.cacheWrite += turn.cacheWrite;
|
|
123
|
-
target.costUsd = round6(target.costUsd + turn.costUsd);
|
|
124
|
-
if (priced) {
|
|
125
|
-
target.costKnownTurns += 1;
|
|
126
|
-
if (billed) target.costBilled = round6(target.costBilled + turn.costUsd);
|
|
127
|
-
else target.costEstimated = round6(target.costEstimated + turn.costUsd);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function addTurnTotals(target, turn, pricing) {
|
|
132
|
-
addRouteTotals(target, turn, pricing);
|
|
133
|
-
const { durationMs } = pricing;
|
|
134
|
-
if (durationMs > 0) {
|
|
135
|
-
target.durationMs += durationMs;
|
|
136
|
-
target.durationTurns += 1;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function emptyModel(provider, model) {
|
|
141
|
-
return {
|
|
142
|
-
provider,
|
|
143
|
-
model,
|
|
144
|
-
kind: '',
|
|
145
|
-
...emptyRouteTotals(),
|
|
146
|
-
conversation: emptyRouteTotals(),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function emptyDay() {
|
|
151
|
-
return {
|
|
152
|
-
...emptyTurnTotals(),
|
|
153
|
-
firstTs: 0,
|
|
154
|
-
lastTs: 0,
|
|
155
|
-
conversation: emptyTurnTotals(),
|
|
156
|
-
conversationPartial: false,
|
|
157
|
-
// Rebuilt from session transcripts rather than recorded turn by turn. The
|
|
158
|
-
// flag travels with the bucket so a surface can keep saying so long after
|
|
159
|
-
// the transcripts it came from are gone.
|
|
160
|
-
restored: false,
|
|
161
|
-
// Set once a day's pre-classification session ids have been cleared, which
|
|
162
|
-
// is what tells a reader the remaining ids can be trusted.
|
|
163
|
-
sessionsPurged: true,
|
|
164
|
-
models: {},
|
|
165
|
-
sessions: {},
|
|
166
|
-
sessionTokens: [],
|
|
167
|
-
sessionsDropped: 0,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function readRouteTotals(raw, target) {
|
|
172
|
-
target.turns = num(raw?.turns);
|
|
173
|
-
target.input = num(raw?.input);
|
|
174
|
-
target.output = num(raw?.output);
|
|
175
|
-
target.cacheRead = num(raw?.cacheRead);
|
|
176
|
-
target.cacheWrite = num(raw?.cacheWrite);
|
|
177
|
-
target.costUsd = round6(num(raw?.costUsd));
|
|
178
|
-
target.costBilled = round6(num(raw?.costBilled));
|
|
179
|
-
target.costEstimated = round6(
|
|
180
|
-
raw?.costEstimated == null ? Math.max(0, target.costUsd - target.costBilled) : num(raw.costEstimated)
|
|
181
|
-
);
|
|
182
|
-
if (raw?.costKnownTurns != null) target.costKnownTurns = num(raw.costKnownTurns);
|
|
183
|
-
else target.costKnownTurns = target.costUsd > 0 ? target.turns : 0;
|
|
184
|
-
return target;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
32
|
/** Turns with a known cost: the recorded count, else every turn when the usage carries any cost at all. */
|
|
188
33
|
export function knownCostTurns(usage, amount = num) {
|
|
189
34
|
if (usage.costKnownTurns != null) return amount(usage.costKnownTurns);
|
|
190
35
|
return amount(usage.costUsd) > 0 ? amount(usage.turns) : 0;
|
|
191
36
|
}
|
|
192
|
-
|
|
193
|
-
function readTurnTotals(raw, target) {
|
|
194
|
-
readRouteTotals(raw, target);
|
|
195
|
-
target.costEstimated = round6(num(raw?.costEstimated));
|
|
196
|
-
target.costKnownTurns = num(raw?.costKnownTurns);
|
|
197
|
-
target.durationMs = num(raw?.durationMs);
|
|
198
|
-
target.durationTurns = num(raw?.durationTurns);
|
|
199
|
-
return target;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function normalizeModel(raw, key) {
|
|
203
|
-
const slash = typeof key === 'string' ? key.indexOf('/') : -1;
|
|
204
|
-
const bucket = emptyModel(
|
|
205
|
-
cleanId(raw?.provider) || (slash > 0 ? key.slice(0, slash) : ''),
|
|
206
|
-
cleanId(raw?.model) || (slash > 0 ? key.slice(slash + 1) : '')
|
|
207
|
-
);
|
|
208
|
-
bucket.kind = cleanId(raw?.kind);
|
|
209
|
-
readRouteTotals(raw, bucket);
|
|
210
|
-
// A bucket written before turns carried a source has no split to read.
|
|
211
|
-
// `null` means unknown, which a reader must not quietly treat as zero.
|
|
212
|
-
bucket.conversation =
|
|
213
|
-
raw?.conversation && typeof raw.conversation === 'object'
|
|
214
|
-
? readRouteTotals(raw.conversation, emptyRouteTotals())
|
|
215
|
-
: null;
|
|
216
|
-
return bucket;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function normalizeDay(raw) {
|
|
220
|
-
const day = emptyDay();
|
|
221
|
-
readTurnTotals(raw, day);
|
|
222
|
-
day.firstTs = num(raw?.firstTs);
|
|
223
|
-
day.lastTs = num(raw?.lastTs);
|
|
224
|
-
day.conversation =
|
|
225
|
-
raw?.conversation && typeof raw.conversation === 'object'
|
|
226
|
-
? readTurnTotals(raw.conversation, emptyTurnTotals())
|
|
227
|
-
: null;
|
|
228
|
-
day.conversationPartial = raw?.conversationPartial === true;
|
|
229
|
-
day.restored = raw?.restored === true;
|
|
230
|
-
// A day that never held unclassified turns needs no purge; one that did is
|
|
231
|
-
// trusted only once it says the old ids were cleared.
|
|
232
|
-
day.sessionsPurged = !day.conversationPartial || raw?.sessionsPurged === true;
|
|
233
|
-
day.sessionsDropped = num(raw?.sessionsDropped);
|
|
234
|
-
const models = raw?.models && typeof raw.models === 'object' ? raw.models : {};
|
|
235
|
-
for (const [key, value] of Object.entries(models)) {
|
|
236
|
-
const id = cleanId(key);
|
|
237
|
-
if (!id) continue;
|
|
238
|
-
day.models[id] = normalizeModel(value, id);
|
|
239
|
-
}
|
|
240
|
-
const sessions = raw?.sessions && typeof raw.sessions === 'object' ? raw.sessions : {};
|
|
241
|
-
for (const [key, value] of Object.entries(sessions)) {
|
|
242
|
-
const id = cleanId(key);
|
|
243
|
-
if (!id) continue;
|
|
244
|
-
day.sessions[id] = num(value);
|
|
245
|
-
}
|
|
246
|
-
if (Array.isArray(raw?.sessionTokens)) {
|
|
247
|
-
day.sessionTokens = raw.sessionTokens.map(num).slice(-MAX_SESSION_TOKENS_PER_DAY);
|
|
248
|
-
}
|
|
249
|
-
return day;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** Defensive read: a hand-edited or partially written file must not throw. */
|
|
253
|
-
export function normalizeUsageRollup(raw) {
|
|
254
|
-
const days = {};
|
|
255
|
-
const source = raw?.days && typeof raw.days === 'object' ? raw.days : {};
|
|
256
|
-
for (const [key, value] of Object.entries(source)) {
|
|
257
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(key)) continue;
|
|
258
|
-
days[key] = normalizeDay(value);
|
|
259
|
-
}
|
|
260
|
-
return {
|
|
261
|
-
version: USAGE_ROLLUP_VERSION,
|
|
262
|
-
updatedAt: num(raw?.updatedAt),
|
|
263
|
-
days,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** Retention + identifier aging. Both bounds are evaluated against `now`. */
|
|
268
|
-
function pruneUsageRollup(rollup, now = Date.now()) {
|
|
269
|
-
const dropBefore = usageRollupDayKey(now - RETENTION_DAYS * DAY_MS);
|
|
270
|
-
const anonymizeBefore = usageRollupDayKey(now - SESSION_DETAIL_DAYS * DAY_MS);
|
|
271
|
-
for (const [key, day] of Object.entries(rollup.days)) {
|
|
272
|
-
if (key < dropBefore) {
|
|
273
|
-
delete rollup.days[key];
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
if (key >= anonymizeBefore) continue;
|
|
277
|
-
const ids = Object.keys(day.sessions);
|
|
278
|
-
if (!ids.length) continue;
|
|
279
|
-
const archived = day.sessionTokens.concat(ids.map((id) => day.sessions[id]));
|
|
280
|
-
day.sessionTokens = archived.slice(-MAX_SESSION_TOKENS_PER_DAY);
|
|
281
|
-
day.sessions = {};
|
|
282
|
-
}
|
|
283
|
-
return rollup;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Fold one gateway usage summary into the rollup. Pure: the caller owns the
|
|
288
|
-
* document, which is what makes this testable without touching disk.
|
|
289
|
-
*/
|
|
290
|
-
export function foldUsageRollup(current, summary, now = Date.now()) {
|
|
291
|
-
const rollup = normalizeUsageRollup(current);
|
|
292
|
-
const provider = cleanId(summary?.provider);
|
|
293
|
-
const model = cleanId(summary?.model);
|
|
294
|
-
// A turn that cannot be attributed to a route would silently inflate every
|
|
295
|
-
// total it lands in, so it is dropped rather than bucketed as "unknown".
|
|
296
|
-
if (!provider || !model) return pruneUsageRollup(rollup, now);
|
|
297
|
-
|
|
298
|
-
const at = Number(summary?.ts ?? summary?.at);
|
|
299
|
-
const ts = Number.isFinite(at) && at > 0 ? at : now;
|
|
300
|
-
const key = usageRollupDayKey(ts);
|
|
301
|
-
const day = rollup.days[key] || emptyDay();
|
|
302
|
-
|
|
303
|
-
const cacheRead = num(summary?.cacheReadTokens);
|
|
304
|
-
const cacheWrite = num(summary?.cacheWriteTokens);
|
|
305
|
-
const turn = {
|
|
306
|
-
turns: 1,
|
|
307
|
-
// Providers disagree about what `inputTokens` counts: several report the
|
|
308
|
-
// whole prompt with the cached part inside it. Stripping it where it is
|
|
309
|
-
// already included is what keeps one cache hit from being counted twice.
|
|
310
|
-
input: billableInputTokensForProvider(provider, num(summary?.inputTokens), cacheRead, cacheWrite),
|
|
311
|
-
output: num(summary?.outputTokens),
|
|
312
|
-
cacheRead,
|
|
313
|
-
cacheWrite,
|
|
314
|
-
costUsd: num(summary?.costUsd),
|
|
315
|
-
};
|
|
316
|
-
const tokens = turn.input + turn.output + turn.cacheRead + turn.cacheWrite;
|
|
317
|
-
const costSource = cleanId(summary?.costSource);
|
|
318
|
-
// costSource 'none' means neither the provider nor the catalog priced this
|
|
319
|
-
// turn; counting it keeps the surface honest about partial cost coverage.
|
|
320
|
-
const pricing = {
|
|
321
|
-
priced:
|
|
322
|
-
summary?.costUsd != null &&
|
|
323
|
-
Number.isFinite(Number(summary.costUsd)) &&
|
|
324
|
-
!['', 'none', 'unpriced'].includes(costSource),
|
|
325
|
-
billed: costSource === 'provider',
|
|
326
|
-
durationMs: num(summary?.durationMs),
|
|
327
|
-
};
|
|
328
|
-
const conversation = isConversationUsageSource(summary?.sourceType);
|
|
329
|
-
|
|
330
|
-
addTurnTotals(day, turn, pricing);
|
|
331
|
-
day.firstTs = day.firstTs > 0 ? Math.min(day.firstTs, ts) : ts;
|
|
332
|
-
day.lastTs = Math.max(day.lastTs, ts);
|
|
333
|
-
if (conversation) {
|
|
334
|
-
if (!day.conversation) {
|
|
335
|
-
// A day carried over from before turns were classified: what is already
|
|
336
|
-
// in it stays unattributed, and the flag lets a reader say so.
|
|
337
|
-
day.conversation = emptyTurnTotals();
|
|
338
|
-
day.conversationPartial = true;
|
|
339
|
-
// Those ids were collected while background cycles still counted, and
|
|
340
|
-
// most of them are cycles. Keeping them would report cycles as sessions.
|
|
341
|
-
day.sessions = {};
|
|
342
|
-
day.sessionTokens = [];
|
|
343
|
-
day.sessionsPurged = true;
|
|
344
|
-
}
|
|
345
|
-
addTurnTotals(day.conversation, turn, pricing);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const modelKey = `${provider}/${model}`;
|
|
349
|
-
const known = Object.hasOwn(day.models, modelKey);
|
|
350
|
-
if (known || Object.keys(day.models).length < MAX_MODELS_PER_DAY) {
|
|
351
|
-
const bucket = known ? day.models[modelKey] : emptyModel(provider, model);
|
|
352
|
-
bucket.kind = cleanId(summary?.providerKind) || bucket.kind;
|
|
353
|
-
addRouteTotals(bucket, turn, pricing);
|
|
354
|
-
if (conversation) {
|
|
355
|
-
if (!bucket.conversation) bucket.conversation = emptyRouteTotals();
|
|
356
|
-
addRouteTotals(bucket.conversation, turn, pricing);
|
|
357
|
-
}
|
|
358
|
-
day.models[modelKey] = bucket;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Only conversation sessions are kept: a background runner mints a new id per
|
|
362
|
-
// cycle, so counting those would report cycles as sessions.
|
|
363
|
-
const sessionId = conversation ? cleanId(summary?.sessionId) : '';
|
|
364
|
-
if (sessionId) {
|
|
365
|
-
if (Object.hasOwn(day.sessions, sessionId)) {
|
|
366
|
-
day.sessions[sessionId] += tokens;
|
|
367
|
-
} else if (Object.keys(day.sessions).length < MAX_SESSION_IDS_PER_DAY) {
|
|
368
|
-
day.sessions[sessionId] = tokens;
|
|
369
|
-
} else {
|
|
370
|
-
day.sessionsDropped += 1;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
rollup.days[key] = day;
|
|
375
|
-
rollup.updatedAt = now;
|
|
376
|
-
return pruneUsageRollup(rollup, now);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Write transcript-rebuilt days into the rollup, so they outlive the sessions
|
|
381
|
-
* they were derived from.
|
|
382
|
-
*
|
|
383
|
-
* The session store prunes: ephemeral sessions past 14 days or beyond the
|
|
384
|
-
* newest 300 are deleted, which silently takes their tokens out of any figure
|
|
385
|
-
* derived from them. The rollup is a few numbers per day kept for 400 days, so
|
|
386
|
-
* once a rebuilt day lands here it stops depending on the transcripts at all.
|
|
387
|
-
*
|
|
388
|
-
* Recording always wins. A day the rollup already holds is left exactly as it
|
|
389
|
-
* is — including a day already frozen this way, because re-deriving it from a
|
|
390
|
-
* half-pruned store would quietly shrink it.
|
|
391
|
-
*
|
|
392
|
-
* Pure: the caller owns the document.
|
|
393
|
-
*/
|
|
394
|
-
export function freezeRestoredDays(current, historyDays, now = Date.now()) {
|
|
395
|
-
const rollup = normalizeUsageRollup(current);
|
|
396
|
-
const source = historyDays && typeof historyDays === 'object' ? historyDays : {};
|
|
397
|
-
const dropBefore = usageRollupDayKey(now - RETENTION_DAYS * DAY_MS);
|
|
398
|
-
const today = usageRollupDayKey(now);
|
|
399
|
-
let frozen = 0;
|
|
400
|
-
for (const [key, day] of Object.entries(source)) {
|
|
401
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(key)) continue;
|
|
402
|
-
if (Object.hasOwn(rollup.days, key)) continue;
|
|
403
|
-
if (key < dropBefore) continue;
|
|
404
|
-
// Today is still being recorded turn by turn; freezing it now would pin a
|
|
405
|
-
// partial rebuild and lock out the rest of the day's real events.
|
|
406
|
-
if (key >= today) continue;
|
|
407
|
-
const bucket = normalizeDay(day);
|
|
408
|
-
if (bucket.turns <= 0) continue;
|
|
409
|
-
bucket.restored = true;
|
|
410
|
-
rollup.days[key] = bucket;
|
|
411
|
-
frozen += 1;
|
|
412
|
-
}
|
|
413
|
-
if (frozen > 0) rollup.updatedAt = now;
|
|
414
|
-
return { rollup, frozen };
|
|
415
|
-
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outgoing-request surrogate sanitizer.
|
|
3
|
+
*
|
|
4
|
+
* A lone UTF-16 surrogate (half of an emoji left by a char-count cut, or
|
|
5
|
+
* carried by tool output / file content) is serialized by JSON.stringify as a
|
|
6
|
+
* `\ud83d` escape that provider APIs reject ("not valid JSON"). This rewrites
|
|
7
|
+
* every string value and every object key to well-formed text (lone surrogate
|
|
8
|
+
* -> U+FFFD) in the request that is about to be sent.
|
|
9
|
+
*
|
|
10
|
+
* Copy-on-write: a request that is already well-formed is returned as the very
|
|
11
|
+
* same object (no allocation, identity kept for caches keyed on messages or
|
|
12
|
+
* tool lists). Only the containers on the path to a bad string are copied; the
|
|
13
|
+
* stored transcript is never mutated. Only arrays and plain objects are
|
|
14
|
+
* walked: Buffer / Uint8Array / ArrayBuffer and any other class instance are
|
|
15
|
+
* binary or opaque payloads and are returned untouched. Own symbol properties
|
|
16
|
+
* (e.g. the provider-native tool prefix count on a tool list) survive a copy.
|
|
17
|
+
*/
|
|
18
|
+
function copySymbols(target, source) {
|
|
19
|
+
for (const symbol of Object.getOwnPropertySymbols(source)) {
|
|
20
|
+
Object.defineProperty(target, symbol, Object.getOwnPropertyDescriptor(source, symbol));
|
|
21
|
+
}
|
|
22
|
+
return target;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setOwn(target, key, value) {
|
|
26
|
+
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function walk(value, active) {
|
|
30
|
+
if (typeof value === 'string') return value.isWellFormed() ? value : value.toWellFormed();
|
|
31
|
+
if (value === null || typeof value !== 'object' || active.has(value)) return value;
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
active.add(value);
|
|
34
|
+
let out = null;
|
|
35
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
36
|
+
const next = walk(value[i], active);
|
|
37
|
+
if (!out && next !== value[i]) out = value.slice();
|
|
38
|
+
if (out) out[i] = next;
|
|
39
|
+
}
|
|
40
|
+
active.delete(value);
|
|
41
|
+
return out ? copySymbols(out, value) : value;
|
|
42
|
+
}
|
|
43
|
+
const proto = Object.getPrototypeOf(value);
|
|
44
|
+
if (proto !== Object.prototype && proto !== null) return value;
|
|
45
|
+
active.add(value);
|
|
46
|
+
const keys = Object.keys(value);
|
|
47
|
+
let out = null;
|
|
48
|
+
for (let i = 0; i < keys.length; i += 1) {
|
|
49
|
+
const key = keys[i];
|
|
50
|
+
const entry = value[key];
|
|
51
|
+
const nextKey = key.isWellFormed() ? key : key.toWellFormed();
|
|
52
|
+
const nextEntry = walk(entry, active);
|
|
53
|
+
if (!out && (nextKey !== key || nextEntry !== entry)) {
|
|
54
|
+
out = Object.create(proto);
|
|
55
|
+
for (let j = 0; j < i; j += 1) setOwn(out, keys[j], value[keys[j]]);
|
|
56
|
+
}
|
|
57
|
+
if (out) setOwn(out, nextKey, nextEntry);
|
|
58
|
+
}
|
|
59
|
+
active.delete(value);
|
|
60
|
+
return out ? copySymbols(out, value) : value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `value` with every string and object key made well-formed; `value` itself when it already is. */
|
|
64
|
+
export function sanitizeSurrogates(value) {
|
|
65
|
+
return walk(value, new WeakSet());
|
|
66
|
+
}
|