limn-review 0.1.136 → 0.1.138

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 (265) hide show
  1. package/README.md +23 -2
  2. package/dist/ai/active-runtime.js +11 -0
  3. package/dist/ai/budget.js +108 -238
  4. package/dist/ai/codex/app-server.js +449 -0
  5. package/dist/ai/codex/config.js +292 -0
  6. package/dist/ai/codex/detect.js +88 -0
  7. package/dist/ai/codex/entry.js +27 -0
  8. package/dist/ai/codex/events.js +290 -0
  9. package/dist/ai/codex/home.js +165 -0
  10. package/dist/ai/codex/hook-rule.js +165 -0
  11. package/dist/ai/codex/hook.js +199 -0
  12. package/dist/ai/codex/kinds/docs.js +10 -0
  13. package/dist/ai/codex/kinds/fix.js +13 -0
  14. package/dist/ai/codex/kinds/review.js +2 -0
  15. package/dist/ai/codex/kinds/tickets.js +2 -0
  16. package/dist/ai/codex/linear-regex.js +539 -0
  17. package/dist/ai/codex/login.js +155 -0
  18. package/dist/ai/codex/path-guard-hook-main.js +59 -0
  19. package/dist/ai/codex/readings.js +148 -0
  20. package/dist/ai/codex/rollouts.js +146 -0
  21. package/dist/ai/codex/run.js +210 -0
  22. package/dist/ai/codex/specialists.js +140 -0
  23. package/dist/ai/codex/threads.js +359 -0
  24. package/dist/ai/codex/types.js +5 -0
  25. package/dist/ai/codex/workspace-files.js +470 -0
  26. package/dist/ai/codex/workspace-mcp-main.js +13 -0
  27. package/dist/ai/codex/workspace-mcp.js +150 -0
  28. package/dist/ai/credentials.js +108 -2
  29. package/dist/ai/effort.js +105 -0
  30. package/dist/ai/prompt-tools.js +57 -0
  31. package/dist/ai/provider-setting.js +146 -0
  32. package/dist/ai/provider-store.js +66 -0
  33. package/dist/ai/provider.js +70 -0
  34. package/dist/ai/providers/claude-activity.js +179 -0
  35. package/dist/ai/providers/claude-usage.js +160 -0
  36. package/dist/ai/providers/claude.js +358 -0
  37. package/dist/ai/providers/codex.js +694 -0
  38. package/dist/ai/result-schemas.js +143 -0
  39. package/dist/ai/run-choice.js +82 -0
  40. package/dist/ai/runtime-errors.js +16 -0
  41. package/dist/ai/runtime.js +377 -56
  42. package/dist/api/plugins/rate-limit.js +38 -39
  43. package/dist/api/routes/ai-budget.js +13 -4
  44. package/dist/api/routes/ai-codex.js +48 -0
  45. package/dist/api/routes/ai-provider.js +57 -0
  46. package/dist/api/routes/ai-runtime.js +13 -4
  47. package/dist/api/routes/bot-triage.js +1 -1
  48. package/dist/api/routes/conflicts.js +2 -1
  49. package/dist/api/routes/insights.js +3 -4
  50. package/dist/api/routes/me.js +89 -6
  51. package/dist/api/routes/sprint-stories.js +1 -1
  52. package/dist/api/routes/workspaces.js +51 -1
  53. package/dist/app.js +9 -2
  54. package/dist/cli.js +73 -14
  55. package/dist/coding/agent.js +138 -110
  56. package/dist/coding/ai-fix/auto-fix.js +7 -2
  57. package/dist/coding/ai-fix/manager.js +66 -17
  58. package/dist/coding/ai-fix/persist.js +31 -3
  59. package/dist/coding/ai-fix/plan.js +12 -1
  60. package/dist/coding/ai-fix/prompts.js +39 -17
  61. package/dist/coding/ai-fix/routes.js +31 -30
  62. package/dist/coding/ai-resolve/prompt.js +11 -4
  63. package/dist/coding/ai-resolve/routes.js +3 -2
  64. package/dist/coding/ai-resolve/run.js +75 -31
  65. package/dist/coding/git-ops.js +4 -121
  66. package/dist/coding/git.js +3 -5
  67. package/dist/config.js +7 -18
  68. package/dist/db/auto-retry-at-head.js +39 -0
  69. package/dist/db/bot-dormancy.js +25 -33
  70. package/dist/db/changepoint.js +2 -2
  71. package/dist/db/erase-account.js +20 -1
  72. package/dist/db/export-account.js +27 -0
  73. package/dist/db/migrations/0127_ai_usage_provider.sql +11 -0
  74. package/dist/db/migrations/0128_ai_provider_labels.sql +44 -0
  75. package/dist/db/migrations/0129_cheap_run_provider.sql +10 -0
  76. package/dist/db/migrations/0130_ai_effort.sql +13 -0
  77. package/dist/db/migrations/0131_related_repos.sql +23 -0
  78. package/dist/db/migrations/0132_sprint_code_judgements.sql +24 -0
  79. package/dist/db/migrations/0133_review_fresh.sql +4 -0
  80. package/dist/db/migrations/meta/_journal.json +49 -0
  81. package/dist/db/period-metrics.js +0 -30
  82. package/dist/db/pr-intervals.js +1 -1
  83. package/dist/db/queries.js +86 -738
  84. package/dist/db/related-repos.js +128 -0
  85. package/dist/db/schema.sqlite.js +151 -3
  86. package/dist/db/sprint-briefs.js +2 -1
  87. package/dist/db/sprint-code-judgements.js +100 -0
  88. package/dist/db/sprint-graph-fold.js +8 -3
  89. package/dist/db/sprint-graph.js +29 -0
  90. package/dist/db/sprint-paths.js +11 -3
  91. package/dist/db/sprint-themes.js +2 -1
  92. package/dist/db/usage.js +8 -2
  93. package/dist/db/work-plan.js +1 -1
  94. package/dist/github/client.js +3 -49
  95. package/dist/github/mutations.js +16 -7
  96. package/dist/index.js +6 -0
  97. package/dist/platform/advisor/schema.sqlite.js +4 -4
  98. package/dist/platform/annotations/runner.js +6 -2
  99. package/dist/platform/annotations/schema.sqlite.js +3 -0
  100. package/dist/platform/bot-themes/prompt.js +2 -0
  101. package/dist/platform/bot-themes/routes.js +11 -5
  102. package/dist/platform/bot-themes/schema.sqlite.js +3 -0
  103. package/dist/platform/bots/behaviour-routes.js +1 -1
  104. package/dist/platform/bots/benchmark/collect.js +1 -1
  105. package/dist/platform/context.js +8 -24
  106. package/dist/platform/event-hooks.js +2 -1
  107. package/dist/platform/flow-pointers/routes.js +10 -4
  108. package/dist/platform/flow-pointers/schema.sqlite.js +3 -0
  109. package/dist/platform/index.js +3 -5
  110. package/dist/platform/insights/grounded-figures.js +1 -1
  111. package/dist/platform/insights/period-report.js +28 -92
  112. package/dist/platform/insights/period-routes.js +4 -17
  113. package/dist/platform/insights/period-schema.sqlite.js +5 -0
  114. package/dist/platform/insights/routes.js +2 -3
  115. package/dist/platform/insights/schema.sqlite.js +3 -0
  116. package/dist/platform/insights/sprint-report.js +10 -4
  117. package/dist/platform/llm/seam.js +21 -8
  118. package/dist/platform/migrations/0042_ai_provider_labels.sql +17 -0
  119. package/dist/platform/migrations/0043_period_report_ran_on.sql +13 -0
  120. package/dist/platform/retention.js +3 -2
  121. package/dist/platform/settings/schema.sqlite.js +1 -1
  122. package/dist/platform/slack/pending-blocks.js +1 -1
  123. package/dist/platform/slack/signals.js +24 -7
  124. package/dist/platform/synthesis/routes.js +11 -5
  125. package/dist/platform/synthesis/schema.sqlite.js +3 -0
  126. package/dist/platform/work-plan/routes.js +10 -4
  127. package/dist/platform/work-plan/schema.sqlite.js +3 -0
  128. package/dist/reference-docs/agent.js +201 -136
  129. package/dist/reference-docs/chat.js +13 -7
  130. package/dist/reference-docs/crawl-rank.js +6 -4
  131. package/dist/reference-docs/manager.js +29 -10
  132. package/dist/reference-docs/prompts.js +42 -6
  133. package/dist/reference-docs/review-refs.js +4 -1
  134. package/dist/reference-docs/routes.js +3 -2
  135. package/dist/reference-docs/schema.js +9 -1
  136. package/dist/reference-docs/store.js +8 -1
  137. package/dist/review/agent-context.js +9 -4
  138. package/dist/review/agent.js +262 -208
  139. package/dist/review/agentic.js +9 -0
  140. package/dist/review/auth.js +48 -1
  141. package/dist/review/chat-agent.js +99 -88
  142. package/dist/review/cheap-usage.js +21 -0
  143. package/dist/review/ci-review/agent.js +63 -73
  144. package/dist/review/ci-review/auto-post.js +4 -4
  145. package/dist/review/ci-review/manager.js +20 -4
  146. package/dist/review/ci-review/persist.js +15 -2
  147. package/dist/review/ci-review/prompts.js +11 -5
  148. package/dist/review/ci-review/routes.js +1 -1
  149. package/dist/review/ci-review/sweep.js +1 -1
  150. package/dist/review/claude-review/ai-ready.js +8 -4
  151. package/dist/review/claude-review/auto-post.js +21 -14
  152. package/dist/review/claude-review/auto-pushback.js +4 -4
  153. package/dist/review/claude-review/auto-resolve.js +4 -4
  154. package/dist/review/claude-review/auto-retry.js +142 -0
  155. package/dist/review/claude-review/auto-settings.js +58 -0
  156. package/dist/review/claude-review/auto.js +165 -20
  157. package/dist/review/claude-review/chat.js +32 -10
  158. package/dist/review/claude-review/follow-up.js +50 -15
  159. package/dist/review/claude-review/manager.js +168 -31
  160. package/dist/review/claude-review/persist.js +196 -22
  161. package/dist/review/claude-review/posted-state.js +3 -3
  162. package/dist/review/claude-review/prompts.js +80 -1
  163. package/dist/review/claude-review/related-repos.js +185 -0
  164. package/dist/review/claude-review/routes.js +40 -13
  165. package/dist/review/claude-review/specialists.js +8 -3
  166. package/dist/review/claude-review/thread-check-agent.js +96 -82
  167. package/dist/review/claude-review/thread-check-persist.js +17 -1
  168. package/dist/review/claude-review/thread-check-routes.js +1 -1
  169. package/dist/review/claude-review/thread-check-schema.js +10 -0
  170. package/dist/review/claude-review/thread-check.js +23 -9
  171. package/dist/review/claude-review/thread-judgements.js +48 -2
  172. package/dist/review/clone-manager.js +0 -26
  173. package/dist/review/codex-review.js +106 -0
  174. package/dist/review/codex-wording.js +192 -0
  175. package/dist/review/diagrams/agent.js +88 -82
  176. package/dist/review/diagrams/manager.js +11 -5
  177. package/dist/review/diagrams/persist.js +10 -1
  178. package/dist/review/diagrams/prompts.js +30 -9
  179. package/dist/review/diagrams/routes.js +3 -2
  180. package/dist/review/llm.js +11 -120
  181. package/dist/review/model-options.js +7 -32
  182. package/dist/review/path-guard.js +13 -3
  183. package/dist/review/run-provider.js +23 -0
  184. package/dist/review/sprint-ask/routes.js +17 -10
  185. package/dist/review/sprint-briefs/index.js +4 -0
  186. package/dist/review/sprint-briefs/manager.js +9 -5
  187. package/dist/review/sprint-code/manager.js +94 -0
  188. package/dist/review/sprint-code/prompt.js +33 -0
  189. package/dist/review/sprint-code/validate.js +66 -0
  190. package/dist/review/sprint-paths/agent.js +72 -81
  191. package/dist/review/sprint-paths/manager.js +10 -4
  192. package/dist/review/sprint-paths/prompt.js +15 -2
  193. package/dist/review/sprint-paths/schema.js +17 -0
  194. package/dist/review/sprint-themes/manager.js +9 -5
  195. package/dist/review/story-docs/agent.js +74 -71
  196. package/dist/review/story-docs/manager.js +24 -15
  197. package/dist/review/story-docs/persist.js +6 -3
  198. package/dist/review/story-docs/prompts.js +11 -5
  199. package/dist/review/story-docs/routes.js +3 -2
  200. package/dist/review/story-docs/shortlist.js +6 -4
  201. package/dist/review/story-labels/manager.js +18 -10
  202. package/dist/review/ticket-review/agent.js +64 -73
  203. package/dist/review/ticket-review/auto-post.js +3 -2
  204. package/dist/review/ticket-review/cards.js +2 -1
  205. package/dist/review/ticket-review/manager.js +26 -5
  206. package/dist/review/ticket-review/persist.js +15 -2
  207. package/dist/review/ticket-review/prepass.js +68 -65
  208. package/dist/review/ticket-review/prompts.js +30 -7
  209. package/dist/review/ticket-review/routes.js +1 -1
  210. package/dist/review/ticket-review/sprint-checks.js +1 -1
  211. package/dist/review/ticket-review/sweep.js +1 -1
  212. package/dist/shared/ai-budget.js +148 -1
  213. package/dist/shared/ai-effort.js +43 -0
  214. package/dist/shared/ai-provider-words.js +58 -0
  215. package/dist/shared/ai-run-label.js +183 -0
  216. package/dist/shared/claude-review.js +23 -0
  217. package/dist/shared/index.js +3 -0
  218. package/dist/shared/reference-docs.js +0 -16
  219. package/dist/shared/sequence-diagrams.js +4 -17
  220. package/dist/shared/sprint-graph.js +25 -6
  221. package/dist/shared/ticket-prs.js +2 -0
  222. package/dist/shared/types.js +40 -7
  223. package/dist/sonar/comment.js +6 -4
  224. package/dist/sonar/post.js +6 -1
  225. package/dist/sync/bot-detection.js +1 -1
  226. package/dist/sync/upsert.js +1 -1
  227. package/dist/tracker/jira/fetch.js +39 -4
  228. package/dist/tracker/routes.js +4 -0
  229. package/dist/tracker/status-change.js +268 -0
  230. package/package.json +4 -4
  231. package/public/assets/AiFixTab-DwAzsycp.js +3 -0
  232. package/public/assets/ClaudeReviewTab-CGFvyUOI.js +20 -0
  233. package/public/assets/{PrBotBehaviourTab-BkY9gzPl.js → PrBotBehaviourTab-CtfRiaES.js} +1 -1
  234. package/public/assets/index-B23U_yh7.css +1 -0
  235. package/public/assets/index-BHxZquEA.js +81 -0
  236. package/public/index.html +2 -2
  237. package/dist/coding/activity.js +0 -80
  238. package/dist/coding/safe-path.js +0 -75
  239. package/dist/platform/advisor/discovery/tier2.js +0 -88
  240. package/dist/platform/advisor/discovery/tier3.js +0 -57
  241. package/dist/platform/advisor/emitters/adapter.js +0 -5
  242. package/dist/platform/advisor/emitters/coderabbit.js +0 -175
  243. package/dist/platform/advisor/emitters/copilot-prose.js +0 -118
  244. package/dist/platform/advisor/emitters/greptile.js +0 -176
  245. package/dist/platform/advisor/emitters/lint.js +0 -37
  246. package/dist/platform/advisor/emitters/manifest.js +0 -239
  247. package/dist/platform/advisor/emitters/prose.js +0 -183
  248. package/dist/platform/advisor/emitters/registry.js +0 -20
  249. package/dist/platform/advisor/emitters/yaml-doc.js +0 -95
  250. package/dist/platform/advisor/evidence.js +0 -79
  251. package/dist/platform/advisor/findings.js +0 -15
  252. package/dist/platform/advisor/intents.js +0 -269
  253. package/dist/platform/advisor/measure/effect.js +0 -18
  254. package/dist/platform/advisor/outputs/brief.js +0 -68
  255. package/dist/platform/advisor/outputs/config-pr.js +0 -156
  256. package/dist/platform/advisor/refine/diff-guard.js +0 -38
  257. package/dist/platform/advisor/refine/refine.js +0 -35
  258. package/dist/platform/advisor/retro-check.js +0 -98
  259. package/dist/platform/advisor/routes.js +0 -648
  260. package/dist/platform/advisor/store.js +0 -321
  261. package/dist/platform/advisor/taxonomy.js +0 -67
  262. package/public/assets/AiFixTab-FwTFw_Ub.js +0 -3
  263. package/public/assets/ClaudeReviewTab-CJ6BRXTq.js +0 -20
  264. package/public/assets/index-BB6qXSAO.css +0 -1
  265. package/public/assets/index-Cconixya.js +0 -80
package/README.md CHANGED
@@ -41,8 +41,8 @@ Claude Review (a code review of any PR, follow-ups, a check against a user
41
41
  story you paste, and a chat about the review) and AI Fix are free. By default
42
42
  they use your local Claude Code login: if you are signed in to Claude Code,
43
43
  there is nothing to set up. An Anthropic API key is the alternative: set
44
- `ANTHROPIC_API_KEY`. Limn stores no key and charges nothing for it. Codex
45
- support is coming soon.
44
+ `ANTHROPIC_API_KEY`. Limn stores no key and charges nothing for it. They can
45
+ run on Codex instead (below).
46
46
 
47
47
  - **First use downloads the AI runtime, once (about 110 MB).** Press
48
48
  **Set up AI** in the app, or run `limn ai install`. It goes to
@@ -57,6 +57,27 @@ support is coming soon.
57
57
  in to Claude Code.
58
58
  - `LIMN_AI_DISABLED=true` turns every AI feature off.
59
59
 
60
+ ### Codex instead of Claude
61
+
62
+ Every AI feature can run on Codex, on your own ChatGPT plan. One provider runs
63
+ everything at a time.
64
+
65
+ ```bash
66
+ limn ai install codex # adds the Codex CLI to the AI download
67
+ limn ai login codex # sign in with ChatGPT (add --device-auth on a headless machine)
68
+ ```
69
+
70
+ Or press **Set up AI** and sign in from **Settings → AI**. Limn keeps its own
71
+ Codex sign-in in `~/.limn/codex`; your `~/.codex` is not read or changed. Codex
72
+ with an OpenAI API key is not available yet.
73
+
74
+ - Pick the provider in **Settings → AI**, or set `LIMN_AI_PROVIDER` to
75
+ `claude`, `codex` or `auto` (the default: Claude when both are signed in,
76
+ Codex when only Codex is).
77
+ - Each run is labelled with the provider and model that ran it.
78
+ - `limn ai status` shows whether Codex is set up and signed in.
79
+ - `limn ai logout codex` signs Limn's Codex out.
80
+
60
81
  ## Prerequisites
61
82
 
62
83
  - **Node.js ≥ 20** (with npm).
@@ -0,0 +1,11 @@
1
+ // THE ACTIVE PROVIDER'S DOWNLOAD — is the program the next run needs installed? Claude's runs need the
2
+ // Claude SDK download (`getAiRuntimeStatus`); Codex's need the Codex program (`getCodexRuntimeStatus`,
3
+ // installed WITH the shared download, so its zod is there too). `/api/me`'s top-level `ai.runtime`
4
+ // and the background sweepers' gate (`agenticRunReady`) both ask this, so a Codex machine is never
5
+ // held back for a missing Claude program, nor a Claude one for a missing Codex.
6
+ import { getAiRuntimeStatus, getCodexRuntimeStatus } from './runtime.js';
7
+ import { activeProviderId } from './provider-setting.js';
8
+ export function activeRuntimeStatus() {
9
+ return activeProviderId() === 'codex' ? getCodexRuntimeStatus() : getAiRuntimeStatus();
10
+ }
11
+ //# sourceMappingURL=active-runtime.js.map
package/dist/ai/budget.js CHANGED
@@ -37,160 +37,37 @@
37
37
  // There is no shared "≤ 1 means a fraction" guess: each path knows its own scale. Any throw or shape
38
38
  // mismatch is "no reading" — the windows are then rolling and the subscription is the reader's pick.
39
39
  //
40
+ // CODEX (docs/CODEX-PLAN.md § 8.1): the budget judges the ACTIVE provider. On a ChatGPT sign-in the
41
+ // reading is the app-server's `account/rateLimits/read` (ai/codex/app-server.ts — in memory, probed in
42
+ // the background at most every 5 minutes), and Limn's share of each window is TOKEN-WEIGHTED: its own
43
+ // codex ai_usage tokens ÷ the account's tokens in that window (`account/usage/read`) × the window's
44
+ // whole-percent usedPercent. Same gate, same settings, percentages only. An API key is weekly US$.
45
+ //
40
46
  // One budget per ACCOUNT (= per machine), never per workspace.
41
47
  import { and, eq, gte, inArray } from 'drizzle-orm';
42
- import { AI_BUDGET_DAY_MS, AI_BUDGET_WEEK_MS, aiUsageView, decideAiBudget, liveFiveHourReset, liveWeekReset, resolveAiBudgetSettings, rollingSpend, } from '../shared/index.js';
48
+ import { AI_BUDGET_DAY_MS, AI_BUDGET_WEEK_MS, aiUsageView, budgetWindowStart, decideAiBudget, liveFiveHourReset, liveWeekReset, resolveAiBudgetSettings, rollingSpend, } from '../shared/index.js';
43
49
  import { db, schema } from '../db/client.js';
44
50
  import { LOCAL_ACCOUNT_ID } from '../auth/account.js';
45
- import { resolveAiCredential, agentAuthOptions } from './credentials.js';
46
- import { ISOLATED_MCP } from './agent-isolation.js';
47
- import { claudeExecutableOptions, getAiRuntimeStatus, loadAgentSdk, setAgentMessageObserver, } from './runtime.js';
51
+ import { getActiveProvider, getProvider } from './provider.js';
52
+ import { foldRateLimitEvent, mergeReadings, normaliseStoredReading, parseUsageProbe, } from './providers/claude-usage.js';
53
+ import { setAgentMessageObserver } from './runtime.js';
54
+ import { usageResetFromReading } from '../review/claude-review/auto-retry.js';
55
+ import { codexAccountTokens, codexRateLimitsCached, kickCodexProbe } from './codex/app-server.js';
48
56
  export const DAY_MS = AI_BUDGET_DAY_MS;
49
57
  export const WEEK_MS = AI_BUDGET_WEEK_MS;
50
58
  /** The window "typical cycle cost" is averaged over. */
51
59
  export const CYCLE_COST_WINDOW_MS = 28 * DAY_MS;
52
60
  /** The usage probe runs at most this often (the header polls every ~2 minutes while visible). */
53
61
  export const PROBE_MIN_INTERVAL_MS = 5 * 60 * 1000;
54
- const PROBE_TIMEOUT_MS = 20_000;
55
- /** At most this many per-model windows are kept from one reading. */
56
- const MAX_MODEL_WINDOWS = 8;
57
62
  export { decideAiBudget, rollingSpend, liveWeekReset, liveFiveHourReset, aiUsageView };
58
- // ---- folding the SDK's usage reports (pure) ---------------------------------------------------
59
- function num(v) {
60
- return typeof v === 'number' && Number.isFinite(v) ? v : null;
61
- }
62
- /** Epoch seconds or ms → ms. */
63
- function epochMs(v) {
64
- const n = num(v);
65
- if (n == null || n <= 0)
66
- return null;
67
- return n < 1e12 ? n * 1000 : n;
68
- }
69
- /** A `rate_limit_event` utilisation is a 0–1 FRACTION (the CLI parses it from the rate-limit header):
70
- * → 0–100, explicitly. Out of range or not a number → null. */
71
- function eventFractionToPct(v) {
72
- const n = num(v);
73
- if (n == null || n < 0 || n > 1.5)
74
- return null;
75
- return n * 100;
76
- }
77
- /** Claude's window keys this module keeps: the 5-hour, the week, and per-model weeks. */
78
- const MODEL_WINDOW_KEY = /^seven_day_[a-z0-9_]{1,40}$/;
79
- /** Put one window into a reading under Claude's key. Unknown keys are ignored. Pure. */
80
- function withWindow(reading, key, w) {
81
- if (key === 'five_hour')
82
- return { ...reading, fiveHour: w };
83
- if (key === 'seven_day')
84
- return { ...reading, week: w };
85
- if (MODEL_WINDOW_KEY.test(key)) {
86
- const models = { ...(reading.models ?? {}) };
87
- if (!(key in models) && Object.keys(models).length >= MAX_MODEL_WINDOWS)
88
- return reading;
89
- models[key] = w;
90
- return { ...reading, models };
91
- }
92
- return reading;
93
- }
94
- const isWindowKey = (k) => k === 'five_hour' || k === 'seven_day' || MODEL_WINDOW_KEY.test(k);
95
- /**
96
- * Fold one Agent SDK message into the reading. Returns the NEW reading (the windows this message names
97
- * merged over the rest), or null when the message names none. Pure.
98
- *
99
- * Reads `rate_limit_event.rate_limit_info`: every window in `unifiedWindows` (when the CLI sends
100
- * them) and otherwise the one named by `rateLimitType` ('five_hour', 'seven_day', 'seven_day_opus',
101
- * …). Reset times arrive in epoch seconds. Utilisation is a fraction on this path.
102
- */
103
- export function foldRateLimitEvent(reading, message, nowMs) {
104
- if (!message || typeof message !== 'object')
105
- return null;
106
- const m = message;
107
- if (m.type !== 'rate_limit_event' || !m.rate_limit_info || typeof m.rate_limit_info !== 'object')
108
- return null;
109
- const info = m.rate_limit_info;
110
- const unified = info.unifiedWindows && typeof info.unifiedWindows === 'object'
111
- ? info.unifiedWindows
112
- : null;
113
- const sources = [];
114
- if (unified) {
115
- for (const [k, v] of Object.entries(unified)) {
116
- if (isWindowKey(k) && v && typeof v === 'object')
117
- sources.push([k, v]);
118
- }
119
- }
120
- else if (typeof info.rateLimitType === 'string' && isWindowKey(info.rateLimitType)) {
121
- sources.push([info.rateLimitType, info]);
122
- }
123
- let out = {};
124
- let any = false;
125
- for (const [key, src] of sources) {
126
- let utilization = eventFractionToPct(src.utilization);
127
- if (utilization == null && src.status === 'rejected')
128
- utilization = 100;
129
- const resetsAtMs = epochMs(src.resetsAt);
130
- if (utilization == null && resetsAtMs == null)
131
- continue;
132
- out = withWindow(out, key, { utilization, resetsAtMs, observedAtMs: nowMs });
133
- any = true;
134
- }
135
- return any ? mergeReadings(reading, out) : null;
136
- }
137
- /** Parse the experimental usage method's answer. Its utilisation is ALREADY 0–100 (measured on a
138
- * real login: `seven_day.utilization: 31`) and is kept as-is. Null on any shape mismatch. Pure. */
139
- export function parseUsageProbe(resp, nowMs) {
140
- if (!resp || typeof resp !== 'object')
141
- return null;
142
- const r = resp;
143
- const subscriptionType = typeof r.subscription_type === 'string' && r.subscription_type !== '' ? r.subscription_type : null;
144
- let out = {};
145
- let any = false;
146
- if (r.rate_limits_available === true && r.rate_limits && typeof r.rate_limits === 'object') {
147
- for (const [key, v] of Object.entries(r.rate_limits)) {
148
- if (!isWindowKey(key) || !v || typeof v !== 'object')
149
- continue;
150
- const o = v;
151
- const u = num(o.utilization);
152
- const t = typeof o.resets_at === 'string' ? Date.parse(o.resets_at) : NaN;
153
- const utilization = u != null && u >= 0 && u <= 150 ? u : null;
154
- const resetsAtMs = Number.isFinite(t) ? t : null;
155
- if (utilization == null && resetsAtMs == null)
156
- continue;
157
- out = withWindow(out, key, { utilization, resetsAtMs, observedAtMs: nowMs });
158
- any = true;
159
- }
160
- }
161
- if (!any && subscriptionType == null)
162
- return null;
163
- return { ...out, subscriptionType };
164
- }
165
- function storedWindow(v) {
166
- if (!v || typeof v !== 'object')
167
- return null;
168
- const w = v;
169
- const observedAtMs = num(w.observedAtMs);
170
- return observedAtMs == null ? null : { utilization: num(w.utilization), resetsAtMs: num(w.resetsAtMs), observedAtMs };
171
- }
172
- /** A stored reading of any age → today's shape. Never throws. */
173
- export function normaliseStoredReading(raw) {
174
- if (!raw || typeof raw !== 'object')
175
- return null;
176
- const r = raw;
177
- const week = storedWindow(r.week);
178
- const fiveHour = storedWindow(r.fiveHour);
179
- let models = null;
180
- if (r.models && typeof r.models === 'object') {
181
- for (const [k, v] of Object.entries(r.models).slice(0, MAX_MODEL_WINDOWS)) {
182
- const w = MODEL_WINDOW_KEY.test(k) ? storedWindow(v) : null;
183
- if (w)
184
- (models ??= {})[k] = w;
185
- }
186
- }
187
- const subscriptionType = typeof r.subscriptionType === 'string' ? r.subscriptionType : null;
188
- if (week == null && fiveHour == null && models == null && subscriptionType == null)
189
- return null;
190
- return { week, fiveHour, models, subscriptionType };
191
- }
192
- // ---- the reading: memory + persistence --------------------------------------------------------
193
- let memReading; // undefined = not loaded yet
63
+ // Claude's reading folds live in ai/providers/claude-usage.ts since the provider seam; re-exported
64
+ // here under the names this module always had.
65
+ export { foldRateLimitEvent, mergeReadings, normaliseStoredReading, parseUsageProbe };
66
+ // ---- the reading: ONE normalised AiUsageReading per provider (docs/CODEX-PLAN.md § 8.1) ----------
67
+ //
68
+ // Claude's is kept in memory and persisted on `accounts.ai_budget_reading`, so a restart keeps it.
69
+ // No other provider has a reading yet (Phase 2 gives Codex its own): `readingFor` answers null for it.
70
+ let memReading; // Claude's; undefined = not loaded yet
194
71
  async function loadReading() {
195
72
  if (memReading !== undefined)
196
73
  return memReading;
@@ -208,29 +85,21 @@ async function loadReading() {
208
85
  }
209
86
  return memReading;
210
87
  }
211
- /** One window merged: the new one wins, but keeps the known reset when it read none. */
212
- function mergeWindow(prev, next) {
213
- if (next == null)
214
- return prev ?? null;
215
- if (next.resetsAtMs == null && prev?.resetsAtMs != null)
216
- return { ...next, resetsAtMs: prev.resetsAtMs };
217
- return next;
88
+ /**
89
+ * The provider's latest normalised reading. Codex's is the app-server probe's CACHE
90
+ * (ai/codex/app-server.ts) — in memory only: the probe refreshes it in the background within seconds
91
+ * of a restart, and until then the reading is unknown, which never blocks.
92
+ */
93
+ async function readingFor(provider) {
94
+ if (provider.id === 'codex')
95
+ return codexRateLimitsCached();
96
+ return loadReading();
218
97
  }
219
- /** Merge two readings (pure): each window independently; what `next` does not mention is kept (an
220
- * event never names the subscription, and names one or two windows). */
221
- export function mergeReadings(prev, next) {
222
- const models = { ...(prev?.models ?? {}) };
223
- for (const [k, w] of Object.entries(next.models ?? {})) {
224
- const merged = mergeWindow(models[k], w);
225
- if (merged && (k in models || Object.keys(models).length < MAX_MODEL_WINDOWS))
226
- models[k] = merged;
227
- }
228
- return {
229
- week: mergeWindow(prev?.week, next.week),
230
- fiveHour: mergeWindow(prev?.fiveHour, next.fiveHour),
231
- models: Object.keys(models).length === 0 ? null : models,
232
- subscriptionType: next.subscriptionType ?? prev?.subscriptionType ?? null,
233
- };
98
+ /** Codex on a ChatGPT sign-in: the account's tokens in each budget window (from the probe's cache). */
99
+ async function codexAccountTokensFor(reading, nowMs) {
100
+ const span = async (which) => codexAccountTokens({ fromMs: budgetWindowStart(reading, nowMs, which).fromMs, toMs: nowMs });
101
+ const [week, fiveHour] = await Promise.all([span('week'), span('five_hour')]);
102
+ return { week, fiveHour };
234
103
  }
235
104
  function storeReading(next) {
236
105
  memReading = mergeReadings(memReading ?? null, next);
@@ -244,9 +113,9 @@ function storeReading(next) {
244
113
  /* in memory still; the next reading tries again */
245
114
  });
246
115
  }
247
- /** The observer ai/runtime.ts calls with every agent message. */
116
+ /** The observer ai/runtime.ts calls with every agent message (the Agent SDK's, so Claude's). */
248
117
  export function noteAgentMessage(message, nowMs = Date.now()) {
249
- const next = foldRateLimitEvent(null, message, nowMs);
118
+ const next = getProvider('claude').usage.readingFromMessage(message, nowMs);
250
119
  if (next == null)
251
120
  return;
252
121
  // ⚠ Merge over the PERSISTED reading, never over "not loaded yet": a run's event arriving before
@@ -264,64 +133,15 @@ export function installAiBudgetObserver() {
264
133
  setAgentMessageObserver((m) => noteAgentMessage(m));
265
134
  }
266
135
  // ---- the usage probe ----------------------------------------------------------------------------
136
+ //
137
+ // The probe itself is the provider's (`provider.usage.probe`; Claude's is the Agent SDK's
138
+ // experimental usage method, in ai/providers/claude.ts). Only Claude's reading is stored.
267
139
  let lastProbeAtMs = 0;
268
140
  let probeInFlight = null;
269
- /** Ask the Agent SDK's experimental usage method. Spawns the CLI with an empty input stream (no model
270
- * call), asks, closes. Throws on anything unexpected; the caller swallows. */
271
- const sdkUsageProbe = async () => {
272
- const sdk = await loadAgentSdk();
273
- let release = () => { };
274
- const held = new Promise((r) => {
275
- release = r;
276
- });
277
- // An input stream that never yields a turn: the CLI starts, answers control requests, and spends
278
- // nothing.
279
- async function* noTurns() {
280
- await held;
281
- }
282
- const q = sdk.query({
283
- prompt: noTurns(),
284
- options: {
285
- allowedTools: [],
286
- settingSources: [],
287
- ...ISOLATED_MCP,
288
- ...claudeExecutableOptions(),
289
- ...agentAuthOptions(),
290
- },
291
- });
292
- try {
293
- const method = q
294
- .usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET;
295
- if (typeof method !== 'function')
296
- throw new Error('usage method not available');
297
- let timer;
298
- try {
299
- return await Promise.race([
300
- method.call(q, { skipBehaviors: true }),
301
- new Promise((_, reject) => {
302
- timer = setTimeout(() => reject(new Error('usage probe timed out')), PROBE_TIMEOUT_MS);
303
- }),
304
- ]);
305
- }
306
- finally {
307
- if (timer)
308
- clearTimeout(timer);
309
- }
310
- }
311
- finally {
312
- release();
313
- try {
314
- q.close?.();
315
- }
316
- catch {
317
- /* already gone */
318
- }
319
- }
320
- };
321
- let probeImpl = sdkUsageProbe;
141
+ let probeOverride = null;
322
142
  /** Test hook. */
323
143
  export function _setUsageProbeForTest(p) {
324
- probeImpl = p ?? sdkUsageProbe;
144
+ probeOverride = p;
325
145
  lastProbeAtMs = 0;
326
146
  probeInFlight = null;
327
147
  }
@@ -335,18 +155,25 @@ export function _resetAiBudgetForTest() {
335
155
  /** Start the probe IN THE BACKGROUND when it can help: login route (the caller checks), runtime
336
156
  * ready, not run in the last PROBE_MIN_INTERVAL_MS. Single-flight. Never throws, never awaited by a
337
157
  * request: the caller serves the cached reading. Returns the in-flight promise for tests. */
338
- export function kickUsageProbe(nowMs = Date.now()) {
158
+ export function kickUsageProbe(nowMs = Date.now(), provider = getActiveProvider()) {
159
+ // Codex's probe is the app-server's own (account, windows and token counts in one process), with
160
+ // the same 5-minute floor and single flight.
161
+ if (provider.id === 'codex')
162
+ return provider.usage.probeReady() ? kickCodexProbe({ nowMs }) : null;
163
+ if (provider.id !== 'claude')
164
+ return null;
339
165
  if (probeInFlight)
340
166
  return probeInFlight;
341
167
  if (nowMs - lastProbeAtMs < PROBE_MIN_INTERVAL_MS)
342
168
  return null;
343
- if (probeImpl === sdkUsageProbe && getAiRuntimeStatus().runtime !== 'ready')
169
+ if (probeOverride == null && !provider.usage.probeReady())
344
170
  return null;
345
171
  lastProbeAtMs = nowMs;
172
+ const probe = probeOverride ?? (() => provider.usage.probe());
346
173
  probeInFlight = (async () => {
347
174
  try {
348
175
  await loadReading();
349
- const parsed = parseUsageProbe(await probeImpl(), Date.now());
176
+ const parsed = provider.usage.readingFromProbe(await probe(), Date.now());
350
177
  if (parsed)
351
178
  storeReading(parsed);
352
179
  }
@@ -382,12 +209,25 @@ export async function writeAiBudgetSettings(accountId, body) {
382
209
  return next;
383
210
  }
384
211
  // ---- the status and the gate ----------------------------------------------------------------------
385
- async function readLedger(accountId, sinceMs) {
212
+ /**
213
+ * Where the ledger read starts: the week, today, and every account-token span's start (a Codex day
214
+ * bucket can start before the week window; Limn's tokens must be counted over the account's span
215
+ * EXACTLY, so the read must reach back to it). Exported for the test.
216
+ */
217
+ export function ledgerSinceMs(nowMs, accountTokens) {
218
+ const starts = [nowMs - WEEK_MS, localMidnightMs(nowMs)];
219
+ for (const s of [accountTokens?.week, accountTokens?.fiveHour])
220
+ if (s && Number.isFinite(s.fromMs))
221
+ starts.push(s.fromMs);
222
+ return Math.min(...starts);
223
+ }
224
+ /** Limn's recorded spend since `sinceMs` — ONE provider's rows only (a budget is per provider). */
225
+ async function readLedger(accountId, sinceMs, providerId) {
386
226
  const { aiUsage } = schema;
387
227
  const rows = await db
388
228
  .select({ at: aiUsage.occurredAt, usd: aiUsage.costUsd, inTok: aiUsage.inputTokens, outTok: aiUsage.outputTokens })
389
229
  .from(aiUsage)
390
- .where(and(eq(aiUsage.accountId, accountId), gte(aiUsage.occurredAt, new Date(sinceMs))))
230
+ .where(and(eq(aiUsage.accountId, accountId), eq(aiUsage.provider, providerId), gte(aiUsage.occurredAt, new Date(sinceMs))))
391
231
  .execute();
392
232
  return rows.map((r) => ({
393
233
  atMs: r.at.getTime(),
@@ -398,13 +238,13 @@ async function readLedger(accountId, sinceMs) {
398
238
  }
399
239
  /** The account's typical automatic-review cycle: the mean recorded cost of a Claude review and of a
400
240
  * diagram run over the last 28 days, and whether any of its workspaces draws diagrams by itself. */
401
- export async function readCycleCosts(accountId, nowMs) {
241
+ export async function readCycleCosts(accountId, nowMs, providerId = 'claude') {
402
242
  const { aiUsage, workspaces } = schema;
403
243
  const [rows, ws] = await Promise.all([
404
244
  db
405
245
  .select({ feature: aiUsage.feature, usd: aiUsage.costUsd })
406
246
  .from(aiUsage)
407
- .where(and(eq(aiUsage.accountId, accountId), inArray(aiUsage.feature, ['claude_review', 'sequence_diagram']), gte(aiUsage.occurredAt, new Date(nowMs - CYCLE_COST_WINDOW_MS))))
247
+ .where(and(eq(aiUsage.accountId, accountId), eq(aiUsage.provider, providerId), inArray(aiUsage.feature, ['claude_review', 'sequence_diagram']), gte(aiUsage.occurredAt, new Date(nowMs - CYCLE_COST_WINDOW_MS))))
408
248
  .execute(),
409
249
  db
410
250
  .select({ id: workspaces.id })
@@ -444,21 +284,41 @@ function localMidnightMs(nowMs) {
444
284
  */
445
285
  export async function aiBudgetSnapshot(accountId, opts = {}) {
446
286
  const nowMs = opts.nowMs ?? Date.now();
447
- const route = resolveAiCredential().route;
287
+ // THE PROVIDER's budget (docs/CODEX-PLAN.md § 8.1): its credential route, its reading, its rows of
288
+ // the ledger — the named one (a manual run's per-run choice), else the default (active) one.
289
+ const provider = opts.provider ? getProvider(opts.provider) : getActiveProvider();
290
+ const route = provider.credentialRoute();
448
291
  const { settings, isDefault } = await readAiBudgetSettings(accountId);
449
292
  const probe = opts.probe ?? 'when_on';
450
293
  if (route === 'login' && (probe === 'always' || (probe === 'when_on' && settings.enabled)))
451
- kickUsageProbe(nowMs);
452
- const [reading, ledger, costs] = await Promise.all([
453
- route === 'login' ? loadReading() : Promise.resolve(null),
454
- readLedger(accountId, Math.min(nowMs - WEEK_MS, localMidnightMs(nowMs))),
455
- settings.enabled && route === 'login' ? readCycleCosts(accountId, nowMs) : Promise.resolve(NO_CYCLE_COSTS),
294
+ kickUsageProbe(nowMs, provider);
295
+ const isCodex = provider.id === 'codex';
296
+ const reading = route === 'login' ? await readingFor(provider) : null;
297
+ // Codex's share is TOKEN-WEIGHTED (packages/shared ai-budget.ts): the account's tokens over a span,
298
+ // and Limn's counted over EXACTLY that span — so the ledger is read from the EARLIEST span start (a
299
+ // day bucket may start before the week window).
300
+ const accountTokens = isCodex && route === 'login' ? await codexAccountTokensFor(reading, nowMs) : null;
301
+ const [ledger, costs] = await Promise.all([
302
+ readLedger(accountId, ledgerSinceMs(nowMs, accountTokens), provider.id),
303
+ settings.enabled && route === 'login' && !isCodex
304
+ ? readCycleCosts(accountId, nowMs, provider.id)
305
+ : Promise.resolve(NO_CYCLE_COSTS),
456
306
  ]);
307
+ const providerId = provider.id;
457
308
  return {
458
309
  settings,
459
310
  isDefault,
460
- status: decideAiBudget({ settings, route, reading, ledger, costs, nowMs }),
461
- usage: aiUsageView({ settings, route, reading, ledger, nowMs, todayFromMs: localMidnightMs(nowMs) }),
311
+ status: decideAiBudget({ settings, route, reading, ledger, costs, nowMs, provider: providerId, accountTokens }),
312
+ usage: aiUsageView({
313
+ settings,
314
+ route,
315
+ reading,
316
+ ledger,
317
+ nowMs,
318
+ todayFromMs: localMidnightMs(nowMs),
319
+ provider: providerId,
320
+ accountTokens,
321
+ }),
462
322
  };
463
323
  }
464
324
  /** The budget's status for an account. Never throws: a failed read is "off" (never blocks). */
@@ -467,7 +327,7 @@ export async function aiBudgetStatus(accountId, opts = {}) {
467
327
  return (await aiBudgetSnapshot(accountId, opts)).status;
468
328
  }
469
329
  catch {
470
- return OFF_STATUS(resolveAiCredential().route);
330
+ return OFF_STATUS(getActiveProvider().credentialRoute());
471
331
  }
472
332
  }
473
333
  // account → when its current pause was first logged (log once per pause).
@@ -490,6 +350,16 @@ export async function allowAutomaticRun(accountId, what, log, statusOf = (a) =>
490
350
  return false;
491
351
  }
492
352
  export function buildAiBudgetGate(log) {
493
- return { allowAutomaticRun: (accountId, what) => allowAutomaticRun(accountId, what, log) };
353
+ return {
354
+ allowAutomaticRun: (accountId, what) => allowAutomaticRun(accountId, what, log),
355
+ usageResetAfter: async (provider, failedAtMs) => {
356
+ try {
357
+ return usageResetFromReading(await readingFor(getProvider(provider)), failedAtMs);
358
+ }
359
+ catch {
360
+ return null;
361
+ }
362
+ },
363
+ };
494
364
  }
495
365
  //# sourceMappingURL=budget.js.map