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.
- package/README.md +23 -2
- package/dist/ai/active-runtime.js +11 -0
- package/dist/ai/budget.js +108 -238
- package/dist/ai/codex/app-server.js +449 -0
- package/dist/ai/codex/config.js +292 -0
- package/dist/ai/codex/detect.js +88 -0
- package/dist/ai/codex/entry.js +27 -0
- package/dist/ai/codex/events.js +290 -0
- package/dist/ai/codex/home.js +165 -0
- package/dist/ai/codex/hook-rule.js +165 -0
- package/dist/ai/codex/hook.js +199 -0
- package/dist/ai/codex/kinds/docs.js +10 -0
- package/dist/ai/codex/kinds/fix.js +13 -0
- package/dist/ai/codex/kinds/review.js +2 -0
- package/dist/ai/codex/kinds/tickets.js +2 -0
- package/dist/ai/codex/linear-regex.js +539 -0
- package/dist/ai/codex/login.js +155 -0
- package/dist/ai/codex/path-guard-hook-main.js +59 -0
- package/dist/ai/codex/readings.js +148 -0
- package/dist/ai/codex/rollouts.js +146 -0
- package/dist/ai/codex/run.js +210 -0
- package/dist/ai/codex/specialists.js +140 -0
- package/dist/ai/codex/threads.js +359 -0
- package/dist/ai/codex/types.js +5 -0
- package/dist/ai/codex/workspace-files.js +470 -0
- package/dist/ai/codex/workspace-mcp-main.js +13 -0
- package/dist/ai/codex/workspace-mcp.js +150 -0
- package/dist/ai/credentials.js +108 -2
- package/dist/ai/effort.js +105 -0
- package/dist/ai/prompt-tools.js +57 -0
- package/dist/ai/provider-setting.js +146 -0
- package/dist/ai/provider-store.js +66 -0
- package/dist/ai/provider.js +70 -0
- package/dist/ai/providers/claude-activity.js +179 -0
- package/dist/ai/providers/claude-usage.js +160 -0
- package/dist/ai/providers/claude.js +358 -0
- package/dist/ai/providers/codex.js +694 -0
- package/dist/ai/result-schemas.js +143 -0
- package/dist/ai/run-choice.js +82 -0
- package/dist/ai/runtime-errors.js +16 -0
- package/dist/ai/runtime.js +377 -56
- package/dist/api/plugins/rate-limit.js +38 -39
- package/dist/api/routes/ai-budget.js +13 -4
- package/dist/api/routes/ai-codex.js +48 -0
- package/dist/api/routes/ai-provider.js +57 -0
- package/dist/api/routes/ai-runtime.js +13 -4
- package/dist/api/routes/bot-triage.js +1 -1
- package/dist/api/routes/conflicts.js +2 -1
- package/dist/api/routes/insights.js +3 -4
- package/dist/api/routes/me.js +89 -6
- package/dist/api/routes/sprint-stories.js +1 -1
- package/dist/api/routes/workspaces.js +51 -1
- package/dist/app.js +9 -2
- package/dist/cli.js +73 -14
- package/dist/coding/agent.js +138 -110
- package/dist/coding/ai-fix/auto-fix.js +7 -2
- package/dist/coding/ai-fix/manager.js +66 -17
- package/dist/coding/ai-fix/persist.js +31 -3
- package/dist/coding/ai-fix/plan.js +12 -1
- package/dist/coding/ai-fix/prompts.js +39 -17
- package/dist/coding/ai-fix/routes.js +31 -30
- package/dist/coding/ai-resolve/prompt.js +11 -4
- package/dist/coding/ai-resolve/routes.js +3 -2
- package/dist/coding/ai-resolve/run.js +75 -31
- package/dist/coding/git-ops.js +4 -121
- package/dist/coding/git.js +3 -5
- package/dist/config.js +7 -18
- package/dist/db/auto-retry-at-head.js +39 -0
- package/dist/db/bot-dormancy.js +25 -33
- package/dist/db/changepoint.js +2 -2
- package/dist/db/erase-account.js +20 -1
- package/dist/db/export-account.js +27 -0
- package/dist/db/migrations/0127_ai_usage_provider.sql +11 -0
- package/dist/db/migrations/0128_ai_provider_labels.sql +44 -0
- package/dist/db/migrations/0129_cheap_run_provider.sql +10 -0
- package/dist/db/migrations/0130_ai_effort.sql +13 -0
- package/dist/db/migrations/0131_related_repos.sql +23 -0
- package/dist/db/migrations/0132_sprint_code_judgements.sql +24 -0
- package/dist/db/migrations/0133_review_fresh.sql +4 -0
- package/dist/db/migrations/meta/_journal.json +49 -0
- package/dist/db/period-metrics.js +0 -30
- package/dist/db/pr-intervals.js +1 -1
- package/dist/db/queries.js +86 -738
- package/dist/db/related-repos.js +128 -0
- package/dist/db/schema.sqlite.js +151 -3
- package/dist/db/sprint-briefs.js +2 -1
- package/dist/db/sprint-code-judgements.js +100 -0
- package/dist/db/sprint-graph-fold.js +8 -3
- package/dist/db/sprint-graph.js +29 -0
- package/dist/db/sprint-paths.js +11 -3
- package/dist/db/sprint-themes.js +2 -1
- package/dist/db/usage.js +8 -2
- package/dist/db/work-plan.js +1 -1
- package/dist/github/client.js +3 -49
- package/dist/github/mutations.js +16 -7
- package/dist/index.js +6 -0
- package/dist/platform/advisor/schema.sqlite.js +4 -4
- package/dist/platform/annotations/runner.js +6 -2
- package/dist/platform/annotations/schema.sqlite.js +3 -0
- package/dist/platform/bot-themes/prompt.js +2 -0
- package/dist/platform/bot-themes/routes.js +11 -5
- package/dist/platform/bot-themes/schema.sqlite.js +3 -0
- package/dist/platform/bots/behaviour-routes.js +1 -1
- package/dist/platform/bots/benchmark/collect.js +1 -1
- package/dist/platform/context.js +8 -24
- package/dist/platform/event-hooks.js +2 -1
- package/dist/platform/flow-pointers/routes.js +10 -4
- package/dist/platform/flow-pointers/schema.sqlite.js +3 -0
- package/dist/platform/index.js +3 -5
- package/dist/platform/insights/grounded-figures.js +1 -1
- package/dist/platform/insights/period-report.js +28 -92
- package/dist/platform/insights/period-routes.js +4 -17
- package/dist/platform/insights/period-schema.sqlite.js +5 -0
- package/dist/platform/insights/routes.js +2 -3
- package/dist/platform/insights/schema.sqlite.js +3 -0
- package/dist/platform/insights/sprint-report.js +10 -4
- package/dist/platform/llm/seam.js +21 -8
- package/dist/platform/migrations/0042_ai_provider_labels.sql +17 -0
- package/dist/platform/migrations/0043_period_report_ran_on.sql +13 -0
- package/dist/platform/retention.js +3 -2
- package/dist/platform/settings/schema.sqlite.js +1 -1
- package/dist/platform/slack/pending-blocks.js +1 -1
- package/dist/platform/slack/signals.js +24 -7
- package/dist/platform/synthesis/routes.js +11 -5
- package/dist/platform/synthesis/schema.sqlite.js +3 -0
- package/dist/platform/work-plan/routes.js +10 -4
- package/dist/platform/work-plan/schema.sqlite.js +3 -0
- package/dist/reference-docs/agent.js +201 -136
- package/dist/reference-docs/chat.js +13 -7
- package/dist/reference-docs/crawl-rank.js +6 -4
- package/dist/reference-docs/manager.js +29 -10
- package/dist/reference-docs/prompts.js +42 -6
- package/dist/reference-docs/review-refs.js +4 -1
- package/dist/reference-docs/routes.js +3 -2
- package/dist/reference-docs/schema.js +9 -1
- package/dist/reference-docs/store.js +8 -1
- package/dist/review/agent-context.js +9 -4
- package/dist/review/agent.js +262 -208
- package/dist/review/agentic.js +9 -0
- package/dist/review/auth.js +48 -1
- package/dist/review/chat-agent.js +99 -88
- package/dist/review/cheap-usage.js +21 -0
- package/dist/review/ci-review/agent.js +63 -73
- package/dist/review/ci-review/auto-post.js +4 -4
- package/dist/review/ci-review/manager.js +20 -4
- package/dist/review/ci-review/persist.js +15 -2
- package/dist/review/ci-review/prompts.js +11 -5
- package/dist/review/ci-review/routes.js +1 -1
- package/dist/review/ci-review/sweep.js +1 -1
- package/dist/review/claude-review/ai-ready.js +8 -4
- package/dist/review/claude-review/auto-post.js +21 -14
- package/dist/review/claude-review/auto-pushback.js +4 -4
- package/dist/review/claude-review/auto-resolve.js +4 -4
- package/dist/review/claude-review/auto-retry.js +142 -0
- package/dist/review/claude-review/auto-settings.js +58 -0
- package/dist/review/claude-review/auto.js +165 -20
- package/dist/review/claude-review/chat.js +32 -10
- package/dist/review/claude-review/follow-up.js +50 -15
- package/dist/review/claude-review/manager.js +168 -31
- package/dist/review/claude-review/persist.js +196 -22
- package/dist/review/claude-review/posted-state.js +3 -3
- package/dist/review/claude-review/prompts.js +80 -1
- package/dist/review/claude-review/related-repos.js +185 -0
- package/dist/review/claude-review/routes.js +40 -13
- package/dist/review/claude-review/specialists.js +8 -3
- package/dist/review/claude-review/thread-check-agent.js +96 -82
- package/dist/review/claude-review/thread-check-persist.js +17 -1
- package/dist/review/claude-review/thread-check-routes.js +1 -1
- package/dist/review/claude-review/thread-check-schema.js +10 -0
- package/dist/review/claude-review/thread-check.js +23 -9
- package/dist/review/claude-review/thread-judgements.js +48 -2
- package/dist/review/clone-manager.js +0 -26
- package/dist/review/codex-review.js +106 -0
- package/dist/review/codex-wording.js +192 -0
- package/dist/review/diagrams/agent.js +88 -82
- package/dist/review/diagrams/manager.js +11 -5
- package/dist/review/diagrams/persist.js +10 -1
- package/dist/review/diagrams/prompts.js +30 -9
- package/dist/review/diagrams/routes.js +3 -2
- package/dist/review/llm.js +11 -120
- package/dist/review/model-options.js +7 -32
- package/dist/review/path-guard.js +13 -3
- package/dist/review/run-provider.js +23 -0
- package/dist/review/sprint-ask/routes.js +17 -10
- package/dist/review/sprint-briefs/index.js +4 -0
- package/dist/review/sprint-briefs/manager.js +9 -5
- package/dist/review/sprint-code/manager.js +94 -0
- package/dist/review/sprint-code/prompt.js +33 -0
- package/dist/review/sprint-code/validate.js +66 -0
- package/dist/review/sprint-paths/agent.js +72 -81
- package/dist/review/sprint-paths/manager.js +10 -4
- package/dist/review/sprint-paths/prompt.js +15 -2
- package/dist/review/sprint-paths/schema.js +17 -0
- package/dist/review/sprint-themes/manager.js +9 -5
- package/dist/review/story-docs/agent.js +74 -71
- package/dist/review/story-docs/manager.js +24 -15
- package/dist/review/story-docs/persist.js +6 -3
- package/dist/review/story-docs/prompts.js +11 -5
- package/dist/review/story-docs/routes.js +3 -2
- package/dist/review/story-docs/shortlist.js +6 -4
- package/dist/review/story-labels/manager.js +18 -10
- package/dist/review/ticket-review/agent.js +64 -73
- package/dist/review/ticket-review/auto-post.js +3 -2
- package/dist/review/ticket-review/cards.js +2 -1
- package/dist/review/ticket-review/manager.js +26 -5
- package/dist/review/ticket-review/persist.js +15 -2
- package/dist/review/ticket-review/prepass.js +68 -65
- package/dist/review/ticket-review/prompts.js +30 -7
- package/dist/review/ticket-review/routes.js +1 -1
- package/dist/review/ticket-review/sprint-checks.js +1 -1
- package/dist/review/ticket-review/sweep.js +1 -1
- package/dist/shared/ai-budget.js +148 -1
- package/dist/shared/ai-effort.js +43 -0
- package/dist/shared/ai-provider-words.js +58 -0
- package/dist/shared/ai-run-label.js +183 -0
- package/dist/shared/claude-review.js +23 -0
- package/dist/shared/index.js +3 -0
- package/dist/shared/reference-docs.js +0 -16
- package/dist/shared/sequence-diagrams.js +4 -17
- package/dist/shared/sprint-graph.js +25 -6
- package/dist/shared/ticket-prs.js +2 -0
- package/dist/shared/types.js +40 -7
- package/dist/sonar/comment.js +6 -4
- package/dist/sonar/post.js +6 -1
- package/dist/sync/bot-detection.js +1 -1
- package/dist/sync/upsert.js +1 -1
- package/dist/tracker/jira/fetch.js +39 -4
- package/dist/tracker/routes.js +4 -0
- package/dist/tracker/status-change.js +268 -0
- package/package.json +4 -4
- package/public/assets/AiFixTab-DwAzsycp.js +3 -0
- package/public/assets/ClaudeReviewTab-CGFvyUOI.js +20 -0
- package/public/assets/{PrBotBehaviourTab-BkY9gzPl.js → PrBotBehaviourTab-CtfRiaES.js} +1 -1
- package/public/assets/index-B23U_yh7.css +1 -0
- package/public/assets/index-BHxZquEA.js +81 -0
- package/public/index.html +2 -2
- package/dist/coding/activity.js +0 -80
- package/dist/coding/safe-path.js +0 -75
- package/dist/platform/advisor/discovery/tier2.js +0 -88
- package/dist/platform/advisor/discovery/tier3.js +0 -57
- package/dist/platform/advisor/emitters/adapter.js +0 -5
- package/dist/platform/advisor/emitters/coderabbit.js +0 -175
- package/dist/platform/advisor/emitters/copilot-prose.js +0 -118
- package/dist/platform/advisor/emitters/greptile.js +0 -176
- package/dist/platform/advisor/emitters/lint.js +0 -37
- package/dist/platform/advisor/emitters/manifest.js +0 -239
- package/dist/platform/advisor/emitters/prose.js +0 -183
- package/dist/platform/advisor/emitters/registry.js +0 -20
- package/dist/platform/advisor/emitters/yaml-doc.js +0 -95
- package/dist/platform/advisor/evidence.js +0 -79
- package/dist/platform/advisor/findings.js +0 -15
- package/dist/platform/advisor/intents.js +0 -269
- package/dist/platform/advisor/measure/effect.js +0 -18
- package/dist/platform/advisor/outputs/brief.js +0 -68
- package/dist/platform/advisor/outputs/config-pr.js +0 -156
- package/dist/platform/advisor/refine/diff-guard.js +0 -38
- package/dist/platform/advisor/refine/refine.js +0 -35
- package/dist/platform/advisor/retro-check.js +0 -98
- package/dist/platform/advisor/routes.js +0 -648
- package/dist/platform/advisor/store.js +0 -321
- package/dist/platform/advisor/taxonomy.js +0 -67
- package/public/assets/AiFixTab-FwTFw_Ub.js +0 -3
- package/public/assets/ClaudeReviewTab-CJ6BRXTq.js +0 -20
- package/public/assets/index-BB6qXSAO.css +0 -1
- 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.
|
|
45
|
-
|
|
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 {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
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
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
/**
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
/**
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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({
|
|
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(
|
|
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 {
|
|
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
|