mixdog 0.9.166 → 0.9.167

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/package.json +1 -1
  2. package/scripts/audit-usage-ledger.mjs +86 -0
  3. package/scripts/generate-voice-runtime-manifest.mjs +22 -17
  4. package/scripts/memory-chunk-quality.mjs +128 -0
  5. package/scripts/memory-layer2-check.mjs +133 -0
  6. package/scripts/model-catalog-audit.mjs +6 -0
  7. package/scripts/native-tool-download.mjs +14 -0
  8. package/scripts/prepare-native-assets.mjs +2 -7
  9. package/scripts/tool-failures.mjs +7 -2
  10. package/scripts/verify-release-assets.mjs +48 -90
  11. package/src/defaults/skills/browser-use/SKILL.md +33 -18
  12. package/src/defaults/skills/computer-use/SKILL.md +16 -8
  13. package/src/lib/keychain-cjs.cjs +0 -1
  14. package/src/lib/rules-builder.cjs +4 -1
  15. package/src/rules/agent/40-cycle1-agent.md +21 -13
  16. package/src/rules/lead/01-general.md +8 -3
  17. package/src/rules/shared/10-tool-workflow.md +20 -11
  18. package/src/rules/shared/30-exploration.md +11 -4
  19. package/src/rules/shared/40-editing.md +2 -3
  20. package/src/rules/shared/50-execution.md +6 -3
  21. package/src/rules/shared/60-verification.md +3 -2
  22. package/src/rules/shared/70-delivery.md +0 -4
  23. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +51 -45
  24. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +3 -4
  25. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -7
  26. package/src/runtime/agent/orchestrator/agent-trace.mjs +9 -0
  27. package/src/runtime/agent/orchestrator/config.mjs +9 -4
  28. package/src/runtime/agent/orchestrator/context/collect.mjs +25 -1055
  29. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +272 -0
  30. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +296 -0
  31. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +523 -0
  32. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +7 -2
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +13 -14
  34. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +214 -0
  35. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +220 -116
  36. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +74 -44
  37. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +191 -29
  38. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +13 -12
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +1 -9
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +5 -2
  41. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +48 -25
  42. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +28 -2
  43. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -1
  44. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +1 -8
  45. package/src/runtime/agent/orchestrator/providers/lib/action-input-contract.mjs +32 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +21 -6
  47. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +61 -13
  48. package/src/runtime/agent/orchestrator/providers/model-catalog-projection.mjs +6 -1
  49. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +123 -118
  50. package/src/runtime/agent/orchestrator/providers/model-pricing-rates.mjs +54 -0
  51. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +5 -2
  52. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +34 -3
  53. package/src/runtime/agent/orchestrator/providers/openai-codex-endpoints.mjs +21 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +154 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -1
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +322 -0
  57. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +243 -702
  58. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +2 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-startup-prewarm.mjs +221 -0
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +19 -0
  63. package/src/runtime/agent/orchestrator/providers/provider-model-identities.mjs +40 -0
  64. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +120 -710
  65. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +36 -15
  66. package/src/runtime/agent/orchestrator/session/cache/util.mjs +9 -0
  67. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +17 -4
  68. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +36 -31
  69. package/src/runtime/agent/orchestrator/session/compact.mjs +4 -5
  70. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +6 -5
  71. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +8 -6
  72. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +121 -0
  73. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +2 -3
  74. package/src/runtime/agent/orchestrator/session/loop/diagnostics.mjs +74 -0
  75. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +44 -17
  76. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +266 -0
  77. package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +111 -0
  78. package/src/runtime/agent/orchestrator/session/loop/request-projection.mjs +99 -0
  79. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +91 -0
  80. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +36 -0
  81. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +3 -3
  82. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  83. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +20 -11
  84. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +3 -5
  85. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -3
  86. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +39 -0
  87. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +3 -2
  88. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +21 -3
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +62 -10
  93. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +17 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +61 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +13 -9
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +12 -2
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +3 -2
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +8 -3
  99. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +69 -6
  100. package/src/runtime/agent/orchestrator/tools/builtin.mjs +14 -17
  101. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  102. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  104. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  105. package/src/runtime/bridge-discovery.mjs +8 -0
  106. package/src/runtime/browser-bridge/action-schema.mjs +37 -43
  107. package/src/runtime/browser-bridge/client.mjs +111 -99
  108. package/src/runtime/browser-bridge/input-fields.mjs +187 -0
  109. package/src/runtime/browser-bridge/tool-defs.mjs +12 -227
  110. package/src/runtime/computer-bridge/action-schema.mjs +2 -46
  111. package/src/runtime/computer-bridge/client.mjs +69 -77
  112. package/src/runtime/computer-bridge/core-actions.mjs +7 -5
  113. package/src/runtime/computer-bridge/error-recovery.mjs +41 -37
  114. package/src/runtime/computer-bridge/tool-defs.mjs +1 -1
  115. package/src/runtime/github/commands.mjs +4 -1
  116. package/src/runtime/github/contract.mjs +1 -1
  117. package/src/runtime/github/tool.mjs +2 -2
  118. package/src/runtime/media/adapters/antigravity-image.mjs +72 -0
  119. package/src/runtime/media/adapters/gemini-image.mjs +20 -11
  120. package/src/runtime/media/auth.mjs +12 -0
  121. package/src/runtime/media/catalog.mjs +30 -10
  122. package/src/runtime/media/jobs.mjs +4 -0
  123. package/src/runtime/media/lanes.mjs +40 -31
  124. package/src/runtime/memory/index.mjs +2 -9
  125. package/src/runtime/memory/lib/compact-handoff.mjs +26 -18
  126. package/src/runtime/memory/lib/memory-chunk-audit.mjs +41 -0
  127. package/src/runtime/memory/lib/memory-chunk-quality.mjs +388 -0
  128. package/src/runtime/memory/lib/memory-cycle1.mjs +95 -394
  129. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +29 -4
  130. package/src/runtime/memory/lib/memory-recall-store.mjs +8 -12
  131. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -11
  132. package/src/runtime/memory/lib/memory.mjs +2 -0
  133. package/src/runtime/memory/lib/query-handlers.mjs +98 -144
  134. package/src/runtime/memory/lib/query-ranking.mjs +23 -9
  135. package/src/runtime/memory/lib/recall-format.mjs +37 -27
  136. package/src/runtime/memory/lib/session-ingest-runtime.mjs +14 -8
  137. package/src/runtime/memory/lib/session-ingest.mjs +45 -84
  138. package/src/runtime/memory/lib/transcript-ingest.mjs +19 -21
  139. package/src/runtime/memory/tool-defs.mjs +2 -2
  140. package/src/runtime/office/authoring/pptx-author-session.mjs +3 -7
  141. package/src/runtime/office/core/office-actions-batch.mjs +2 -8
  142. package/src/runtime/office/core/office-actions-open.mjs +124 -0
  143. package/src/runtime/office/core/office-actions-read.mjs +127 -0
  144. package/src/runtime/office/core/office-core.mjs +80 -1
  145. package/src/runtime/office/core/office-sessions.mjs +116 -138
  146. package/src/runtime/office/index.mjs +18 -271
  147. package/src/runtime/office/pdf/pdf-analysis.mjs +2 -2
  148. package/src/runtime/office/pdf/pdf-limits.mjs +2 -0
  149. package/src/runtime/office/tool-defs.mjs +7 -6
  150. package/src/runtime/shared/agent-route-config.mjs +8 -8
  151. package/src/runtime/shared/child-spawn-gate.mjs +10 -13
  152. package/src/runtime/shared/config.mjs +2 -7
  153. package/src/runtime/shared/env.mjs +3 -3
  154. package/src/runtime/shared/err-text.mjs +1 -1
  155. package/src/runtime/shared/extension-scopes.mjs +3 -2
  156. package/src/runtime/shared/heap-cap.mjs +10 -6
  157. package/src/runtime/shared/llm/cost.mjs +35 -10
  158. package/src/runtime/shared/llm/usage-accounting.mjs +11 -1
  159. package/src/runtime/shared/llm/usage-context.mjs +6 -0
  160. package/src/runtime/shared/llm/usage-ledger-import.mjs +5 -0
  161. package/src/runtime/shared/llm/usage-ledger-repair.mjs +109 -0
  162. package/src/runtime/shared/llm/usage-ledger.mjs +59 -20
  163. package/src/runtime/shared/llm/usage-pricing-refresh.mjs +26 -0
  164. package/src/runtime/shared/llm/usage-rollup.mjs +17 -15
  165. package/src/runtime/shared/native-asset.mjs +3 -4
  166. package/src/runtime/shared/object.mjs +4 -0
  167. package/src/runtime/shared/pg-schema-db.mjs +21 -0
  168. package/src/runtime/shared/plugin-metadata.mjs +3 -1
  169. package/src/runtime/shared/resource-admission.mjs +1 -6
  170. package/src/runtime/shared/schedules-db.mjs +8 -24
  171. package/src/runtime/shared/schema-value-error.d.mts +2 -0
  172. package/src/runtime/shared/schema-value-error.mjs +68 -0
  173. package/src/runtime/shared/singleton-owner.mjs +11 -24
  174. package/src/runtime/shared/staged-update.mjs +1 -10
  175. package/src/runtime/shared/tool-card-model.mjs +8 -5
  176. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  177. package/src/runtime/shared/tool-surface.mjs +1 -5
  178. package/src/runtime/shared/user-data-guard.mjs +1 -4
  179. package/src/runtime/shared/webhooks-db.mjs +8 -24
  180. package/src/runtime/web-search/index.mjs +32 -8
  181. package/src/runtime/web-search/tool-defs.mjs +4 -4
  182. package/src/session-runtime/config-lifecycle.mjs +34 -0
  183. package/src/session-runtime/context-status.mjs +19 -0
  184. package/src/session-runtime/cwd-plugins.mjs +12 -29
  185. package/src/session-runtime/goal-tasks.mjs +2 -2
  186. package/src/session-runtime/goal-tool-defs.mjs +3 -2
  187. package/src/session-runtime/internal-tool-executor.mjs +7 -0
  188. package/src/session-runtime/lifecycle-api.mjs +5 -11
  189. package/src/session-runtime/notification-bus.mjs +41 -0
  190. package/src/session-runtime/prewarm.mjs +22 -0
  191. package/src/session-runtime/runtime-bootstrap.mjs +90 -0
  192. package/src/session-runtime/runtime-core.mjs +88 -202
  193. package/src/session-runtime/runtime-feature-gates.mjs +37 -0
  194. package/src/session-runtime/session-lifecycle.mjs +5 -5
  195. package/src/session-runtime/session-turn-api.mjs +1 -12
  196. package/src/session-runtime/setup-tool/executor.mjs +13 -5
  197. package/src/session-runtime/setup-tool/tool-defs.mjs +52 -46
  198. package/src/session-runtime/tool-defs.mjs +73 -4
  199. package/src/session-runtime/usage-stats-api.mjs +4 -1
  200. package/src/session-runtime/warmup-schedulers.mjs +8 -2
  201. package/src/session-runtime/workflow.mjs +14 -0
  202. package/src/standalone/channel-session-router.mjs +3 -1
  203. package/src/standalone/channel-worker.mjs +18 -9
  204. package/src/standalone/daemon-crash-capture.mjs +534 -0
  205. package/src/standalone/daemon-telemetry.mjs +175 -0
  206. package/src/standalone/daemon.mjs +31 -21
  207. package/src/standalone/session-client.mjs +31 -23
  208. package/src/standalone/usage-dashboard-model.mjs +2 -0
  209. package/src/standalone/usage-stats-hours.mjs +18 -11
  210. package/src/standalone/usage-stats-model.mjs +39 -28
  211. package/src/standalone/usage-stats-period.mjs +43 -45
  212. package/src/tui/app/use-transcript-window.mjs +1 -1
  213. package/src/tui/components/StatusLine.jsx +13 -42
  214. package/src/tui/dist/index.mjs +256 -239
  215. package/src/tui/session/session-api-ext.mjs +44 -41
  216. package/src/tui/session/session-api.mjs +4 -0
  217. package/src/tui/session/tool-card-results.mjs +25 -78
  218. package/src/tui/session/tool-result-status.mjs +94 -1
  219. package/src/tui/session/turn-aggregate-cards.mjs +17 -54
  220. package/src/ui/statusline-format.mjs +6 -26
  221. package/src/workflows/default/WORKFLOW.md +5 -0
@@ -4,14 +4,15 @@
4
4
  * Pricing is pulled from the LiteLLM catalog (already warmed by providers/
5
5
  * agent bootstrap). All four token slots — input / output / cacheRead /
6
6
  * cacheWrite — are multiplied by their matching $/M rate from the catalog
7
- * and summed. Missing rates are treated as 0 (no extrapolation).
7
+ * and summed. A missing rate for a used token slot leaves the cost unknown.
8
8
  *
9
9
  * The catalog is looked up synchronously: if it has not been warmed yet
10
- * (fresh process, first call), this returns 0 without blocking. The next
10
+ * (fresh process, first call), this returns an unknown price. The next
11
11
  * call will pick up the cache.
12
12
  */
13
13
 
14
- import { getModelMetadataSync } from '../../agent/orchestrator/providers/model-catalog.mjs';
14
+ import { getModelMetadataSync, resolveModelPricingIdentity } from '../../agent/orchestrator/providers/model-catalog.mjs';
15
+ import { PRICING_RATE_KEYS, ratesForPrompt } from '../../agent/orchestrator/providers/model-pricing-rates.mjs';
15
16
 
16
17
  // OpenAI OAuth / OpenAI API / Gemini report `input_tokens` as the total prompt token
17
18
  // count *including* the cached portion (inclusive). Anthropic reports the
@@ -47,10 +48,29 @@ export function priceUsage(args) {
47
48
  const cached = n(args.cacheReadTokens);
48
49
  const written = n(args.cacheWriteTokens);
49
50
  const inclusive = args.inputTokensInclusive ?? isInclusiveProvider(args.provider);
50
- const input = args.uncachedInputTokens != null ? n(args.uncachedInputTokens)
51
+ const input = args.inputTokensKnown === false ? 0 : args.uncachedInputTokens != null ? n(args.uncachedInputTokens)
51
52
  : inclusive ? Math.max(0, n(args.inputTokens) - cached - written) : n(args.inputTokens);
52
- const meta = getModelMetadataSync(args.model, args.provider);
53
- if (!meta) return { input, costUsd: null, rates: null };
53
+ const identity = resolveModelPricingIdentity(args.model, args.provider, args);
54
+ const meta = getModelMetadataSync(identity.pricingModel, args.provider);
55
+ const provenance = {
56
+ ...identity,
57
+ pricingProvider: meta?.pricingProvider || identity.pricingProvider,
58
+ pricingSource: meta?.pricingSource || null,
59
+ ...(args.inputTokensKnown === false ? { inputTokensKnown: false } : {}),
60
+ ...(args.fast ? { fast: true } : {}),
61
+ ...(args.serviceTier ? { serviceTier: args.serviceTier } : {}),
62
+ };
63
+ if (args.inputTokensKnown === false || !meta) return {
64
+ input, costUsd: null,
65
+ rates: { ...provenance, unpricedReason: args.inputTokensKnown === false ? 'unmeasured-input' : 'model-not-found' },
66
+ };
67
+ const promptTokens = input + cached + written;
68
+ if (args.historicalAggregate && (meta.pricingTiers?.some((tier) => promptTokens > tier.aboveInputTokens)
69
+ || (meta.longContextThreshold && promptTokens >= meta.longContextThreshold))) {
70
+ // A daily sum cannot establish which individual requests crossed a
71
+ // context boundary. Do not price the whole day as one huge prompt.
72
+ return { input, costUsd: null, rates: { ...provenance, unpricedReason: 'request-boundaries-unavailable' } };
73
+ }
54
74
  let multiplier = 1;
55
75
  if (meta.longContextThreshold && input + cached + written >= meta.longContextThreshold) {
56
76
  multiplier *= meta.longContextMultiplier || 1;
@@ -64,11 +84,16 @@ export function priceUsage(args) {
64
84
  && ((h >= 1 && h < 4) || (h >= 6 && h < 10));
65
85
  if (!peak) multiplier *= meta.offPeakMultiplier;
66
86
  }
67
- if (args.model === 'claude-opus-4-8' && (args.fast || args.serviceTier === 'fast')) multiplier *= 2;
68
- const keys = ['inputCostPerM', 'outputCostPerM', 'cacheReadCostPerM', 'cacheWriteCostPerM'];
87
+ if (identity.pricingModel === 'claude-opus-4-8' && (args.fast || args.serviceTier === 'fast')) multiplier *= 2;
88
+ const keys = PRICING_RATE_KEYS;
69
89
  const tokens = [input, n(args.outputTokens), cached, written];
70
- const rates = Object.fromEntries(keys.map((key) => [key, meta[key] == null ? null : meta[key] * multiplier]));
71
- if (tokens.some((amount, i) => amount > 0 && rates[keys[i]] === null)) {
90
+ const tierRates = ratesForPrompt(meta, promptTokens);
91
+ const rates = { ...provenance, ...Object.fromEntries(keys.map((key) =>
92
+ [key, tierRates[key] == null ? null : tierRates[key] * multiplier])) };
93
+ const missingRates = keys.filter((key, i) => tokens[i] > 0 && rates[key] === null);
94
+ if (missingRates.length) {
95
+ rates.unpricedReason = 'missing-rate';
96
+ rates.missingRates = missingRates;
72
97
  return { input, costUsd: null, rates };
73
98
  }
74
99
  const costUsd = tokens.reduce((sum, amount, i) => sum + amount * (rates[keys[i]] ?? 0), 0) / 1_000_000;
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { getUsageLedger, makeUsageRecord } from './usage-ledger.mjs';
3
+ import { withUsageContext } from './usage-context.mjs';
3
4
 
4
5
  /**
5
6
  * Runs at the common provider boundary, not inside optional diagnostic IO.
@@ -23,6 +24,8 @@ export async function accountProviderSend(provider, instance, send, model, opts
23
24
  const row = makeUsageRecord({
24
25
  id: result.responseId ? undefined : requestId,
25
26
  ts: Date.now(), provider, model: result.model || model,
27
+ requestedModel: model,
28
+ pricingModel: result.pricingModel,
26
29
  sessionId: opts.sessionId || opts.session?.id,
27
30
  sourceType: opts.session?.sourceType || opts.sourceType || opts.requestKind || '',
28
31
  inputTokens: usage.inputTokens,
@@ -47,7 +50,14 @@ export async function accountProviderSend(provider, instance, send, model, opts
47
50
  }
48
51
  };
49
52
  let result;
50
- try { result = await send(); }
53
+ try {
54
+ result = await withUsageContext({
55
+ provider, requestedModel: model,
56
+ sessionId: opts.sessionId || opts.session?.id,
57
+ sourceType: opts.session?.sourceType || opts.sourceType || opts.requestKind || '',
58
+ inputTokensInclusive: instance.constructor?.inputExcludesCache !== true,
59
+ }, send);
60
+ }
51
61
  catch (error) {
52
62
  // Only provider-reported partial usage is recordable; never invent
53
63
  // tokens for a failed request or reinterpret an error as a success.
@@ -0,0 +1,6 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+
3
+ // The selected route owns accounting identity, not an inner transport adapter.
4
+ const context = new AsyncLocalStorage();
5
+ export const withUsageContext = (identity, send) => context.run(identity, send);
6
+ export const currentUsageContext = () => context.getStore();
@@ -29,6 +29,8 @@ export function importTraceRow(row) {
29
29
  : /^\d+$/.test(String(row.ts)) ? Number(row.ts) : Date.parse(row.ts);
30
30
  return makeUsageRecord({
31
31
  ts, provider, model: row.model,
32
+ requestedModel: row.requested_model || payload.requested_model,
33
+ pricingModel: row.pricing_model || payload.pricing_model,
32
34
  inputTokens: input, outputTokens: output,
33
35
  uncachedInputTokens: raw ? row.uncached_input_tokens ?? payload.uncached_input_tokens : undefined,
34
36
  cacheReadTokens: raw ? row.cached_tokens : row.cacheReadTokens,
@@ -61,6 +63,7 @@ export function repriceRestoredDays(original) {
61
63
  uncachedInputTokens: bucket.input, outputTokens: bucket.output,
62
64
  cacheReadTokens: bucket.cacheRead, cacheWriteTokens: bucket.cacheWrite,
63
65
  historical: true,
66
+ historicalAggregate: true,
64
67
  });
65
68
  bucket.costUsd = price.costUsd ?? 0;
66
69
  bucket.costBilled = 0;
@@ -145,6 +148,8 @@ export async function readOriginalTrace(dataDir, until) {
145
148
  SELECT ts,kind,model,session_id,input_tokens,output_tokens,cached_tokens,cache_write_tokens,
146
149
  duration_ms,jsonb_build_object(
147
150
  'provider',payload->'provider',
151
+ 'requested_model',payload->'requested_model',
152
+ 'pricing_model',payload->'pricing_model',
148
153
  'uncached_input_tokens',payload->'uncached_input_tokens',
149
154
  'response_id',payload->'response_id',
150
155
  'service_tier',payload->'service_tier',
@@ -0,0 +1,109 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdtempSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ import { priceUsage } from './cost.mjs';
6
+ import { usageRouteKind } from './usage-ledger.mjs';
7
+ import { normalizeUsageMeasurement } from './usage-measurement.mjs';
8
+
9
+ // Excludes only the two fields a repair may change: route attribution and price.
10
+ export function usageLedgerIntegrity(db) {
11
+ const hash = createHash('sha256');
12
+ let records = 0;
13
+ for (const row of db.prepare(`SELECT id,ts,day,session,input,output,cache_read,cache_write,duration_ms
14
+ FROM usage_events ORDER BY id`).iterate()) {
15
+ hash.update(JSON.stringify({ ...row, id: Buffer.from(row.id).toString('hex') }));
16
+ records++;
17
+ }
18
+ return { records, sha256: hash.digest('hex') };
19
+ }
20
+
21
+ /**
22
+ * Bounded correction. Automatic use can only revisit unpriced records and
23
+ * takes a coherent backup under the writer reservation before its first write.
24
+ * Provider overrides require operator evidence; unknown model aliases are NOT
25
+ * inferred. Existing known prices remain fixed unless the billing route changes.
26
+ */
27
+ export function repairUsageLedger(ledger, {
28
+ throughTs, providerOverrides = {}, price = priceUsage, onlyUnpriced = false, backup = false,
29
+ } = {}) {
30
+ if (!Number.isSafeInteger(throughTs) || throughTs <= 0) throw new Error('Repair requires an explicit timestamp bound');
31
+ for (const [from, to] of Object.entries(providerOverrides)) {
32
+ if (!from || typeof to !== 'string' || !to) throw new Error('Invalid provider correction');
33
+ }
34
+ if (onlyUnpriced && Object.keys(providerOverrides).length) throw new Error('Automatic repricing cannot change providers');
35
+ const db = ledger.db;
36
+ db.exec('BEGIN IMMEDIATE');
37
+ try {
38
+ const before = usageLedgerIntegrity(db);
39
+ const insertRoute = db.prepare('INSERT OR IGNORE INTO usage_routes(signature) VALUES (?)');
40
+ const findRoute = db.prepare('SELECT id FROM usage_routes WHERE signature=?');
41
+ const update = db.prepare('UPDATE usage_events SET route=?,cost_usd=? WHERE id=?');
42
+ const report = { reattributed: 0, repriced: 0, stillUnpriced: 0, changes: {} };
43
+ const changes = [];
44
+ const rows = db.prepare(`SELECT e.*,r.signature FROM usage_events e
45
+ JOIN usage_routes r ON r.id=e.route WHERE e.ts<=?
46
+ ${onlyUnpriced ? "AND e.cost_usd IS NULL AND json_extract(r.signature,'$[4]')='unpriced'" : ''}`).all(throughTs);
47
+ for (const row of rows) {
48
+ const signature = JSON.parse(row.signature);
49
+ const originalProvider = signature[0];
50
+ const provider = Object.hasOwn(providerOverrides, originalProvider)
51
+ ? providerOverrides[originalProvider] : originalProvider;
52
+ const moved = provider !== originalProvider;
53
+ const kind = moved ? usageRouteKind(provider) : signature[2];
54
+ let cost = row.cost_usd;
55
+ const oldRates = signature[5] ? JSON.parse(signature[5]) : null;
56
+ if (moved || signature[4] === 'unpriced') {
57
+ const priced = kind === 'local' ? { costUsd: 0, rates: null } : price({
58
+ provider, model: signature[1], pricingModel: oldRates?.pricingModel,
59
+ requestedModel: oldRates?.requestedModel,
60
+ inputTokensKnown: oldRates?.inputTokensKnown === false
61
+ || normalizeUsageMeasurement(provider, { turns: 1 }).unmeasuredTurns ? false : undefined,
62
+ uncachedInputTokens: row.input, outputTokens: row.output,
63
+ cacheReadTokens: row.cache_read, cacheWriteTokens: row.cache_write,
64
+ fast: oldRates?.fast, serviceTier: oldRates?.serviceTier,
65
+ ts: row.ts, historical: true,
66
+ });
67
+ cost = priced.costUsd;
68
+ if (cost !== null && (!Number.isFinite(cost) || cost < 0)) throw new Error('Invalid repair price');
69
+ signature[4] = kind === 'local' ? 'local' : cost === null ? 'unpriced'
70
+ : kind === 'oauth' || kind === 'quota-api' ? 'subscription' : 'catalog';
71
+ signature[5] = priced.rates ? JSON.stringify(priced.rates) : null;
72
+ if (row.cost_usd === null && cost !== null) report.repriced++;
73
+ }
74
+ signature[0] = provider;
75
+ signature[2] = kind;
76
+ if (moved) report.reattributed++;
77
+ if (cost === null) report.stillUnpriced++;
78
+ const encoded = JSON.stringify(signature);
79
+ if (encoded === row.signature && cost === row.cost_usd) continue;
80
+ changes.push({ row, encoded, cost });
81
+ const key = `${originalProvider} → ${provider} / ${signature[1]}`;
82
+ report.changes[key] = (report.changes[key] || 0) + 1;
83
+ }
84
+ if (changes.length && backup && ledger.path !== ':memory:') {
85
+ const directory = mkdtempSync(join(dirname(ledger.path), 'pricing-backup-'));
86
+ report.backupPath = join(directory, 'ledger.sqlite');
87
+ const reader = new DatabaseSync(ledger.path, { readOnly: true });
88
+ try { reader.prepare('VACUUM INTO ?').run(report.backupPath); }
89
+ finally { reader.close(); }
90
+ }
91
+ for (const { row, encoded, cost } of changes) {
92
+ insertRoute.run(encoded);
93
+ update.run(findRoute.get(encoded).id, cost, row.id);
94
+ }
95
+ if (changes.length) ledger.rebuildIndexes();
96
+ const after = usageLedgerIntegrity(db);
97
+ if (before.records !== after.records || before.sha256 !== after.sha256) {
98
+ throw new Error('Repair changed immutable request records');
99
+ }
100
+ if (changes.length) ledger.set('lastUsageRepair', JSON.stringify({
101
+ at: Date.now(), throughTs, providerOverrides, ...report, integrity: after,
102
+ }));
103
+ db.exec('COMMIT');
104
+ return { ...report, integrity: after };
105
+ } catch (error) {
106
+ db.exec('ROLLBACK');
107
+ throw error;
108
+ }
109
+ }
@@ -162,9 +162,7 @@ export function makeUsageRecord(args) {
162
162
  const ts = Number(args.ts ?? Date.now());
163
163
  if (!Number.isSafeInteger(ts) || ts <= 0) throw new Error('Usage requires a valid timestamp');
164
164
  const kind = args.kind || usageRouteKind(provider);
165
- const priced = args.inputTokensKnown === false
166
- ? { input: 0, costUsd: null, rates: null }
167
- : priceUsage({ ...args, provider, model, ts });
165
+ const priced = priceUsage({ ...args, provider, model, ts });
168
166
  const supplied = args.costUsd !== null && args.costUsd !== undefined && args.costUsd !== ''
169
167
  && Number.isFinite(Number(args.costUsd)) && Number(args.costUsd) >= 0;
170
168
  // OAuth/quota plans have value, not per-request invoices. A backend's
@@ -181,7 +179,10 @@ export function makeUsageRecord(args) {
181
179
  costUsd,
182
180
  costSource: kind === 'local' ? 'local' : costUsd === null ? 'unpriced'
183
181
  : subscription ? 'subscription' : reported ? 'provider' : 'catalog',
184
- rates: reported ? null : priced.rates,
182
+ rates: reported || kind === 'local' ? {
183
+ requestedModel: priced.rates.requestedModel, pricingModel: priced.rates.pricingModel,
184
+ pricingProvider: provider, pricingSource: kind === 'local' ? 'local' : 'provider',
185
+ } : priced.rates,
185
186
  responseId: text(args.responseId),
186
187
  origin: args.origin || 'live',
187
188
  durationMs: number(args.durationMs),
@@ -192,6 +193,7 @@ export function makeUsageRecord(args) {
192
193
 
193
194
  export class UsageLedger {
194
195
  constructor(path) {
196
+ this.path = path;
195
197
  if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
196
198
  this.db = new DatabaseSync(path);
197
199
  this.db.exec('PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON;');
@@ -253,6 +255,27 @@ export class UsageLedger {
253
255
  this.db.prepare('INSERT OR IGNORE INTO metadata VALUES (?,?)').run('liveSince', String(ts));
254
256
  }
255
257
 
258
+ indexRecord(row, rank) {
259
+ const conversation = isConversationUsageSource(row.sourceType);
260
+ this.daily.run(row.day, rank, row.provider, row.model, row.kind, row.costSource,
261
+ Number(conversation), 1, row.input, row.output, row.cacheRead, row.cacheWrite,
262
+ row.costUsd ?? 0, row.durationMs, Number(row.origin !== 'live'));
263
+ if (conversation && row.sessionId) this.session.run(row.day, rank, row.sessionId,
264
+ row.input + row.output + row.cacheRead + row.cacheWrite);
265
+ }
266
+
267
+ // Caller owns the transaction; only derived indexes are rebuilt.
268
+ rebuildIndexes() {
269
+ this.db.exec('DELETE FROM daily; DELETE FROM day_sessions;');
270
+ for (const row of this.db.prepare('SELECT * FROM events').iterate()) {
271
+ this.indexRecord({
272
+ ...row, sourceType: row.source_type, sessionId: row.session_id,
273
+ costSource: row.cost_source, costUsd: row.cost_usd,
274
+ cacheRead: row.cache_read, cacheWrite: row.cache_write, durationMs: row.duration_ms,
275
+ }, row.rank);
276
+ }
277
+ }
278
+
256
279
  record(rows) {
257
280
  let inserted = 0;
258
281
  this.db.exec('BEGIN IMMEDIATE');
@@ -266,12 +289,7 @@ export class UsageLedger {
266
289
  rates: row.rates ? JSON.stringify(row.rates) : null,
267
290
  duration_ms: row.durationMs, rank,
268
291
  })) continue;
269
- const conversation = isConversationUsageSource(row.sourceType);
270
- this.daily.run(row.day, rank, row.provider, row.model, row.kind, row.costSource,
271
- Number(conversation), 1, row.input, row.output, row.cacheRead, row.cacheWrite,
272
- row.costUsd ?? 0, row.durationMs, Number(row.origin !== 'live'));
273
- if (conversation && row.sessionId) this.session.run(row.day, rank, row.sessionId,
274
- row.input + row.output + row.cacheRead + row.cacheWrite);
292
+ this.indexRecord(row, rank);
275
293
  if (row.origin === 'live' && !this.get('liveSince')) this.set('liveSince', row.ts);
276
294
  inserted += 1;
277
295
  }
@@ -298,13 +316,14 @@ export class UsageLedger {
298
316
  }
299
317
 
300
318
  /** Read cached amounts; group retained attribution separately for distinct sessions. */
301
- rollup({ hourlyDay = null, fromDay = '0000-01-01', toDay = '9999-12-31' } = {}) {
319
+ rollup({ hourlyDay = null, fromDay = '0000-01-01', toDay = '9999-12-31',
320
+ fromMs = null, toMs = null } = {}) {
302
321
  const days = {};
303
322
  const hourly = hourlyDay ? { rows: [], unallocated: [] } : null;
304
- const fromTs = fromDay === '0000-01-01' ? 0 : new Date(`${fromDay}T00:00:00`).getTime();
323
+ const fromTs = fromMs ?? (fromDay === '0000-01-01' ? 0 : new Date(`${fromDay}T00:00:00`).getTime());
305
324
  const end = toDay === '9999-12-31' ? null : new Date(`${toDay}T00:00:00`);
306
325
  if (end) end.setDate(end.getDate() + 1);
307
- const toTs = end ? end.getTime() : Number.MAX_SAFE_INTEGER;
326
+ const toTs = toMs == null ? (end ? end.getTime() : Number.MAX_SAFE_INTEGER) : toMs + 1;
308
327
  const empty = () => ({
309
328
  turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, costUsd: 0,
310
329
  costBilled: 0, costEstimated: 0, costKnownTurns: 0, durationMs: 0, durationTurns: 0,
@@ -335,17 +354,34 @@ export class UsageLedger {
335
354
  const end = new Date(start);
336
355
  end.setDate(end.getDate() + 1);
337
356
  hourly.rows = this.db.prepare(`
338
- SELECT e.ts,e.provider,e.source_type,e.input,e.output,e.cost_usd
357
+ SELECT e.ts,e.provider,e.source_type,e.input,e.output,e.cache_read,e.cache_write,e.cost_usd
339
358
  FROM events e JOIN (${best}) b USING(day,provider,model,rank)
340
359
  WHERE e.ts>=? AND e.ts<? ORDER BY e.ts
341
- `).all(fromDay, toDay, start.getTime(), end.getTime()).map((row) => {
342
- const usage = normalizeUsageMeasurement(row.provider,
343
- { turns: 1, input: row.input, costUsd: row.cost_usd });
344
- return { ...row, input: usage.input, cost_usd: usage.costUsd,
345
- unmeasuredTurns: usage.unmeasuredTurns || 0 };
360
+ `).all(fromDay, toDay, fromMs == null ? start.getTime() : fromTs,
361
+ toMs == null ? end.getTime() : toTs).map((row) => {
362
+ const usage = normalizeUsageMeasurement(row.provider, { turns: 1, input: row.input,
363
+ cacheRead: row.cache_read, cacheWrite: row.cache_write, costUsd: row.cost_usd });
364
+ return { ...row, input: usage.input, cache_read: usage.cacheRead, cache_write: usage.cacheWrite,
365
+ cost_usd: usage.costUsd, unmeasuredTurns: usage.unmeasuredTurns || 0 };
346
366
  });
347
367
  }
348
- for (const row of this.db.prepare(`SELECT d.* FROM daily d JOIN (${best}) b USING(day,provider,model,rank)`).all(fromDay, toDay)) {
368
+ // Partial calendar days must be rebuilt from retained timestamps;
369
+ // cached whole-day totals would include usage outside a rolling window.
370
+ const amounts = fromMs == null
371
+ ? this.db.prepare(`SELECT d.* FROM daily d JOIN (${best}) b USING(day,provider,model,rank)`).all(fromDay, toDay)
372
+ : this.db.prepare(`
373
+ SELECT e.day,e.rank,e.provider,e.model,e.kind,e.cost_source,e.source_type,
374
+ COUNT(*) AS turns,SUM(e.input) AS input,SUM(e.output) AS output,
375
+ SUM(e.cache_read) AS cache_read,SUM(e.cache_write) AS cache_write,
376
+ SUM(e.cost_usd) AS cost_usd,SUM(e.duration_ms) AS duration_ms,
377
+ MAX(e.origin!='live') AS imported
378
+ FROM events e JOIN (${best}) b USING(day,provider,model,rank)
379
+ WHERE e.ts>=? AND e.ts<?
380
+ GROUP BY e.day,e.rank,e.provider,e.model,e.kind,e.cost_source,e.source_type
381
+ `).all(fromDay, toDay, fromTs, toTs).map((row) => ({
382
+ ...row, conversation: isConversationUsageSource(row.source_type),
383
+ }));
384
+ for (const row of amounts) {
349
385
  const day = days[row.day] ||= { ...empty(), models: {}, sessions: {}, conversation: empty() };
350
386
  const key = `${row.provider}/${row.model}`;
351
387
  const route = day.models[key] ||= {
@@ -395,6 +431,9 @@ export class UsageLedger {
395
431
  }
396
432
  }
397
433
  }
434
+ // Timeless legacy totals cannot establish membership in a 24-hour window.
435
+ // They remain available in the calendar/history views.
436
+ if (fromMs != null) return hourly ? { days, hourly } : { days };
398
437
  for (const row of this.db.prepare('SELECT day,document FROM legacy_days WHERE day BETWEEN ? AND ?').all(fromDay, toDay)) {
399
438
  const legacy = normalizeLegacyUsageDay(JSON.parse(row.document));
400
439
  const day = days[row.day];
@@ -0,0 +1,26 @@
1
+ import { pricingCatalogRevisionSync } from '../../agent/orchestrator/providers/model-catalog.mjs';
2
+ import { repairUsageLedger } from './usage-ledger-repair.mjs';
3
+
4
+ const checked = new WeakMap();
5
+
6
+ function pendingFingerprint(ledger, revision) {
7
+ const pending = ledger.db.prepare(`SELECT COUNT(*) AS count,MAX(e.ts) AS latest
8
+ FROM usage_events e JOIN usage_routes r ON r.id=e.route
9
+ WHERE e.cost_usd IS NULL AND json_extract(r.signature,'$[4]')='unpriced'
10
+ AND e.ts<=?`).get(Date.now());
11
+ return { count: pending.count, key: `${revision}:${pending.count}:${pending.latest}` };
12
+ }
13
+
14
+ /** Price-table changes revisit unknown costs, never overwrite a known bill or
15
+ * estimate. New unknown records also get checked without polling old rows on
16
+ * every dashboard render. Failed repairs are not marked successful. */
17
+ export function refreshUnpricedUsage(ledger) {
18
+ const revision = pricingCatalogRevisionSync();
19
+ const pending = pendingFingerprint(ledger, revision);
20
+ if (!pending.count || checked.get(ledger) === pending.key) return { skipped: true };
21
+ const result = repairUsageLedger(ledger, {
22
+ throughTs: Date.now(), onlyUnpriced: true, backup: true,
23
+ });
24
+ checked.set(ledger, pendingFingerprint(ledger, revision).key);
25
+ return result;
26
+ }
@@ -104,39 +104,35 @@ export function isConversationUsageSource(sourceType) {
104
104
  }
105
105
 
106
106
  function emptyRouteTotals() {
107
- return { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, costUsd: 0 };
107
+ return { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, costUsd: 0,
108
+ costBilled: 0, costEstimated: 0, costKnownTurns: 0 };
108
109
  }
109
110
 
110
111
  function emptyTurnTotals() {
111
112
  return {
112
113
  ...emptyRouteTotals(),
113
- // Priced by the provider itself vs derived from the model catalog. A
114
- // subscription turn is never invoiced per token, so merging the two would
115
- // present a guess as a bill.
116
- costBilled: 0,
117
- costEstimated: 0,
118
- costKnownTurns: 0,
119
114
  durationMs: 0,
120
115
  durationTurns: 0,
121
116
  };
122
117
  }
123
118
 
124
- function addRouteTotals(target, turn) {
119
+ function addRouteTotals(target, turn, { priced, billed } = {}) {
125
120
  target.turns += turn.turns;
126
121
  target.input += turn.input;
127
122
  target.output += turn.output;
128
123
  target.cacheRead += turn.cacheRead;
129
124
  target.cacheWrite += turn.cacheWrite;
130
125
  target.costUsd = round6(target.costUsd + turn.costUsd);
131
- }
132
-
133
- function addTurnTotals(target, turn, { priced, billed, durationMs }) {
134
- addRouteTotals(target, turn);
135
126
  if (priced) {
136
127
  target.costKnownTurns += 1;
137
128
  if (billed) target.costBilled = round6(target.costBilled + turn.costUsd);
138
129
  else target.costEstimated = round6(target.costEstimated + turn.costUsd);
139
130
  }
131
+ }
132
+
133
+ function addTurnTotals(target, turn, pricing) {
134
+ addRouteTotals(target, turn, pricing);
135
+ const { durationMs } = pricing;
140
136
  if (durationMs > 0) {
141
137
  target.durationMs += durationMs;
142
138
  target.durationTurns += 1;
@@ -181,6 +177,11 @@ function readRouteTotals(raw, target) {
181
177
  target.cacheRead = num(raw?.cacheRead);
182
178
  target.cacheWrite = num(raw?.cacheWrite);
183
179
  target.costUsd = round6(num(raw?.costUsd));
180
+ target.costBilled = round6(num(raw?.costBilled));
181
+ target.costEstimated = round6(raw?.costEstimated == null
182
+ ? Math.max(0, target.costUsd - target.costBilled) : num(raw.costEstimated));
183
+ target.costKnownTurns = raw?.costKnownTurns == null
184
+ ? (target.costUsd > 0 ? target.turns : 0) : num(raw.costKnownTurns);
184
185
  return target;
185
186
  }
186
187
 
@@ -311,7 +312,8 @@ export function foldUsageRollup(current, summary, now = Date.now()) {
311
312
  // costSource 'none' means neither the provider nor the catalog priced this
312
313
  // turn; counting it keeps the surface honest about partial cost coverage.
313
314
  const pricing = {
314
- priced: Boolean(costSource) && costSource !== 'none',
315
+ priced: summary?.costUsd != null && Number.isFinite(Number(summary.costUsd))
316
+ && !['', 'none', 'unpriced'].includes(costSource),
315
317
  billed: costSource === 'provider',
316
318
  durationMs: num(summary?.durationMs),
317
319
  };
@@ -340,10 +342,10 @@ export function foldUsageRollup(current, summary, now = Date.now()) {
340
342
  if (known || Object.keys(day.models).length < MAX_MODELS_PER_DAY) {
341
343
  const bucket = known ? day.models[modelKey] : emptyModel(provider, model);
342
344
  bucket.kind = cleanId(summary?.providerKind) || bucket.kind;
343
- addRouteTotals(bucket, turn);
345
+ addRouteTotals(bucket, turn, pricing);
344
346
  if (conversation) {
345
347
  if (!bucket.conversation) bucket.conversation = emptyRouteTotals();
346
- addRouteTotals(bucket.conversation, turn);
348
+ addRouteTotals(bucket.conversation, turn, pricing);
347
349
  }
348
350
  day.models[modelKey] = bucket;
349
351
  }
@@ -10,6 +10,7 @@ import {
10
10
  readFileSync, readdirSync, renameSync, rmSync,
11
11
  } from 'node:fs';
12
12
  import { join } from 'node:path';
13
+ import { readJsonSafe } from './json-file.mjs';
13
14
  import {
14
15
  downloadToFileWithRetry,
15
16
  MAX_NATIVE_BINARY_DOWNLOAD_BYTES,
@@ -26,9 +27,7 @@ export function binSuffix() {
26
27
  return process.platform === 'win32' ? '.exe' : '';
27
28
  }
28
29
 
29
- export function readJsonOrNull(path) {
30
- try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
31
- }
30
+ export { readJsonSafe as readJsonOrNull };
32
31
 
33
32
  export function validSha256(value) {
34
33
  return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value);
@@ -73,7 +72,7 @@ export function validReleaseAsset(manifest, pkey, { name, tagPrefix }) {
73
72
 
74
73
  export function readBundledManifest(bundledPath, options = {}) {
75
74
  if (options.bundledManifest) return options.bundledManifest;
76
- return existsSync(bundledPath) ? readJsonOrNull(bundledPath) : null;
75
+ return existsSync(bundledPath) ? readJsonSafe(bundledPath) : null;
77
76
  }
78
77
 
79
78
  export async function fetchRemoteManifest(url, { fetch: fetchFn = fetch, label = '[native-asset]' } = {}) {
@@ -5,3 +5,7 @@
5
5
  export function hasOwn(value, key) {
6
6
  return value != null && Object.hasOwn(value, key);
7
7
  }
8
+
9
+ export function isPlainObject(value) {
10
+ return !!value && typeof value === 'object' && !Array.isArray(value);
11
+ }
@@ -0,0 +1,21 @@
1
+ /** Lazy per-dataDir PG handle: one in-flight init, bootstrap lock, retry after failure. */
2
+ export function createPgSchemaDb({ schema, ddl, defaultDataDir, ensurePg, withLock }) {
3
+ const ready = new Map();
4
+ return async function getDb(dataDir = defaultDataDir()) {
5
+ if (ready.has(dataDir)) return ready.get(dataDir);
6
+ const p = (async () => {
7
+ const { db, pool } = await ensurePg(dataDir, { schema });
8
+ // Serialize CREATE TABLE across concurrent first-boot processes on the
9
+ // same cluster-global advisory lock the adapter uses for schema bootstrap.
10
+ await withLock(pool, () => db.exec(ddl));
11
+ return db;
12
+ })();
13
+ ready.set(dataDir, p);
14
+ try {
15
+ return await p;
16
+ } catch (err) {
17
+ ready.delete(dataDir); // let the next call retry after a transient failure
18
+ throw err;
19
+ }
20
+ };
21
+ }
@@ -1,3 +1,5 @@
1
+ import { isPlainObject } from './object.mjs';
2
+
1
3
  // Display-only manifest metadata, shared by registry and session status.
2
4
  // Keep malformed values empty instead of leaking "[object Object]" into UI.
3
5
  function text(value) {
@@ -5,7 +7,7 @@ function text(value) {
5
7
  }
6
8
 
7
9
  function object(value) {
8
- return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
10
+ return isPlainObject(value) ? value : {};
9
11
  }
10
12
 
11
13
  function authorText(value) {
@@ -1,7 +1,7 @@
1
1
  import { freemem, totalmem } from 'node:os';
2
2
  import { AsyncLocalStorage } from 'node:async_hooks';
3
3
  import { requestMemoryPressureSnapshot } from './memory-snapshot.mjs';
4
- import { positiveInt } from './numbers.mjs';
4
+ import { nonNegativeInt, positiveInt } from './numbers.mjs';
5
5
 
6
6
  const MB = 1024 * 1024;
7
7
 
@@ -29,11 +29,6 @@ export function normalizePriority(value) {
29
29
  }
30
30
 
31
31
 
32
- function nonNegativeInt(value, fallback) {
33
- const parsed = Math.floor(Number(value));
34
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
35
- }
36
-
37
32
  function envLimits(env = process.env) {
38
33
  return {
39
34
  maxAgents: positiveInt(env.MIXDOG_MAX_CONCURRENT_AGENTS, RESOURCE_ADMISSION_DEFAULTS.maxAgents),
@@ -12,16 +12,11 @@
12
12
  */
13
13
 
14
14
  import { ensurePgInstance, withSchemaBootstrapLock } from '../memory/lib/pg/adapter.mjs';
15
+ import { createPgSchemaDb } from './pg-schema-db.mjs';
15
16
  import { resolvePluginData } from './plugin-paths.mjs';
16
17
 
17
18
  const SCHEMA = 'scheduler';
18
19
 
19
- // ---------------------------------------------------------------------------
20
- // Lazy connection + one-shot idempotent DDL (keyed per resolved dataDir).
21
- // ---------------------------------------------------------------------------
22
-
23
- const _ready = new Map(); // dataDir → Promise<db>
24
-
25
20
  const DDL = `
26
21
  CREATE TABLE IF NOT EXISTS scheduler.schedules (
27
22
  name text PRIMARY KEY,
@@ -61,24 +56,13 @@ ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS last_success_at timesta
61
56
  ALTER TABLE scheduler.schedules ADD COLUMN IF NOT EXISTS last_failed_at timestamptz;
62
57
  `;
63
58
 
64
- async function getDb(dataDir = resolvePluginData()) {
65
- if (_ready.has(dataDir)) return _ready.get(dataDir);
66
- const p = (async () => {
67
- const { db, pool } = await ensurePgInstance(dataDir, { schema: SCHEMA });
68
- // Serialize the CREATE TABLE across concurrent first-boot processes on the
69
- // same cluster-global advisory lock the adapter uses for schema bootstrap,
70
- // so racing first calls can't run the DDL simultaneously.
71
- await withSchemaBootstrapLock(pool, () => db.exec(DDL));
72
- return db;
73
- })();
74
- _ready.set(dataDir, p);
75
- try {
76
- return await p;
77
- } catch (err) {
78
- _ready.delete(dataDir); // let the next call retry DDL after a transient failure
79
- throw err;
80
- }
81
- }
59
+ const getDb = createPgSchemaDb({
60
+ schema: SCHEMA,
61
+ ddl: DDL,
62
+ defaultDataDir: resolvePluginData,
63
+ ensurePg: ensurePgInstance,
64
+ withLock: withSchemaBootstrapLock,
65
+ });
82
66
 
83
67
  // ---------------------------------------------------------------------------
84
68
  // Row <-> def mapping
@@ -0,0 +1,2 @@
1
+ export function schemaStringLength(value: string): number;
2
+ export function schemaValueError(value: unknown, schema: Record<string, unknown>, path: string): string | null;