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.
Files changed (210) hide show
  1. package/LICENSES/oh-my-pi-MIT.txt +23 -0
  2. package/NOTICE.md +15 -0
  3. package/README.md +11 -0
  4. package/package.json +3 -2
  5. package/scripts/embed-google-oauth-client.mjs +49 -0
  6. package/scripts/lib/test-spawn-ledger.mjs +46 -1
  7. package/scripts/test.mjs +5 -0
  8. package/src/defaults/shipped-history.json +8 -0
  9. package/src/defaults/skills/browser-use/SKILL.md +21 -9
  10. package/src/defaults/skills/setup/SKILL.md +7 -2
  11. package/src/defaults/skills/setup/references/actions.md +82 -27
  12. package/src/defaults/skills/setup/references/surfaces.md +67 -34
  13. package/src/defaults/skills/setup-guide/SKILL.md +3 -3
  14. package/src/lib/keychain-cjs.cjs +359 -29
  15. package/src/rules/agent-mode/AGENT_MODE.md +10 -2
  16. package/src/rules/agent-mode/MIXY.md +12 -0
  17. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
  18. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  19. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  21. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
  22. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
  23. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
  24. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
  25. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
  26. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
  27. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
  28. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
  32. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
  33. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
  35. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
  36. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
  37. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
  41. package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
  42. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
  43. package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
  45. package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
  46. package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
  47. package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
  48. package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
  49. package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
  50. package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
  51. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
  52. package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
  53. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
  54. package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
  55. package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
  56. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
  57. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
  58. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
  60. package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
  61. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
  63. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
  64. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
  65. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
  69. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
  70. package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
  71. package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
  72. package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
  73. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
  78. package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  83. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
  84. package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
  85. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
  86. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
  87. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
  88. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
  90. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
  91. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
  92. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
  93. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
  97. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
  99. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
  102. package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
  103. package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
  104. package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
  105. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
  106. package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
  107. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
  109. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
  110. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
  111. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
  112. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
  113. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
  114. package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
  115. package/src/runtime/channels/lib/scheduler.mjs +109 -14
  116. package/src/runtime/channels/lib/schedules-db.mjs +49 -0
  117. package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
  118. package/src/runtime/channels/lib/worker-main.mjs +9 -0
  119. package/src/runtime/office/com/com-adapter.mjs +17 -1
  120. package/src/runtime/office/com/office-com-host.ps1 +51 -1
  121. package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
  122. package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
  123. package/src/runtime/office/pdf/document-preview.mjs +326 -8
  124. package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
  125. package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
  126. package/src/runtime/office/portable/portable-soffice.mjs +45 -5
  127. package/src/runtime/shared/agent-room-host.mjs +1 -1
  128. package/src/runtime/shared/bot-hops.mjs +62 -2
  129. package/src/runtime/shared/config.mjs +49 -22
  130. package/src/runtime/shared/connector-catalog.d.mts +4 -2
  131. package/src/runtime/shared/connector-catalog.mjs +16 -8
  132. package/src/runtime/shared/developer-options.mjs +32 -1
  133. package/src/runtime/shared/google-oauth-client.generated.json +4 -0
  134. package/src/runtime/shared/google-oauth-client.mjs +34 -9
  135. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
  136. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
  137. package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
  138. package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
  139. package/src/runtime/shared/llm/well-formed.mjs +66 -0
  140. package/src/runtime/shared/provider-accounts.mjs +224 -58
  141. package/src/runtime/shared/provider-api-key.mjs +19 -8
  142. package/src/runtime/shared/routine-trigger.mjs +42 -0
  143. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  144. package/src/runtime/shared/transcript-metadata.mjs +38 -0
  145. package/src/runtime/shared/user-data-guard.mjs +30 -2
  146. package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
  147. package/src/session-runtime/agent-home-delivery.mjs +57 -13
  148. package/src/session-runtime/agent-room-delivery.mjs +28 -1
  149. package/src/session-runtime/agent-task-sessions.mjs +292 -79
  150. package/src/session-runtime/agent-tool-routing.mjs +49 -4
  151. package/src/session-runtime/boot/begin.mjs +3 -2
  152. package/src/session-runtime/boot/tools.mjs +4 -0
  153. package/src/session-runtime/bot-chain.mjs +32 -0
  154. package/src/session-runtime/bot-status.mjs +7 -4
  155. package/src/session-runtime/channel-config-api.mjs +63 -17
  156. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
  157. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
  158. package/src/session-runtime/internal-tool-executor.mjs +4 -0
  159. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
  160. package/src/session-runtime/mixy-heartbeat.mjs +807 -0
  161. package/src/session-runtime/model-route/route-tuning.mjs +10 -6
  162. package/src/session-runtime/owned-automation-run.mjs +33 -12
  163. package/src/session-runtime/provider-auth/login.mjs +7 -0
  164. package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
  165. package/src/session-runtime/routine-home-turn.mjs +33 -6
  166. package/src/session-runtime/schedule-session-run.mjs +7 -3
  167. package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
  168. package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
  169. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +11 -2
  170. package/src/session-runtime/services/agent-tool/render.mjs +3 -1
  171. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
  172. package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
  173. package/src/session-runtime/services/channel-admin.mjs +66 -18
  174. package/src/session-runtime/services/channel-worker.mjs +1 -1
  175. package/src/session-runtime/services/provider-admin.mjs +128 -25
  176. package/src/session-runtime/services/schedule-access.mjs +14 -0
  177. package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
  178. package/src/session-runtime/session-create/session-options.mjs +7 -4
  179. package/src/session-runtime/settings-system-api.mjs +16 -1
  180. package/src/session-runtime/setup-tool/executor.mjs +49 -14
  181. package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
  182. package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
  183. package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
  184. package/src/session-runtime/task-session-route.mjs +99 -0
  185. package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
  186. package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
  187. package/src/session-runtime/tool-surface.mjs +4 -0
  188. package/src/session-runtime/usage-stats-api.mjs +1 -1
  189. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
  190. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
  191. package/src/session-runtime/workspace/transitions.mjs +10 -2
  192. package/src/standalone/daemon-session-control.mjs +88 -20
  193. package/src/standalone/daemon.mjs +8 -0
  194. package/src/standalone/session-protocol.mjs +5 -0
  195. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
  196. package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
  197. package/src/standalone/session-service.mjs +13 -0
  198. package/src/standalone/session-tool-output.mjs +8 -5
  199. package/src/tui/app/model-options.mjs +2 -0
  200. package/src/tui/dist/index.mjs +335 -172
  201. package/src/tui/session/notice-surface.mjs +17 -1
  202. package/src/tui/session/session-api/integrations.mjs +1 -0
  203. package/src/tui/session/session-api/lifecycle.mjs +8 -0
  204. package/src/tui/session/session-api/settings.mjs +14 -2
  205. package/src/tui/session/turn-stream.mjs +17 -1
  206. package/src/tui/session/turn.mjs +22 -2
  207. package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
  208. package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
  209. package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
  210. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
@@ -1,78 +1,15 @@
1
1
  /**
2
- * Durable per-day usage rollup (provider × model).
3
- *
4
- * The gateway usage store keeps one raw event per turn, but it is bounded to a
5
- * recent window (TTL + row cap) because it exists to answer "what is happening
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
+ }