@kal-elsam/kairo-runtime 0.17.0 → 0.18.0
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/CHANGELOG.md +29 -0
- package/package.json +1 -1
- package/src/global/cockpit/app.js +20 -2
- package/src/global/cockpit/project-overlay.js +14 -4
- package/src/global/cockpit/view.js +44 -9
- package/src/global/conversation/service.js +28 -1
- package/src/global/intelligence/execution-router.js +25 -3
- package/src/global/observability/codex-models.js +1 -1
- package/src/global/observability/codex-usage.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.18.0 — 2026-09-18 (Kairo Runtime)
|
|
9
|
+
|
|
10
|
+
Minor release. Provider visibility and quota-aware improvements to the
|
|
11
|
+
`/project` PROJECT TEAM view.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- The PROJECT TEAM view (dashboard panel and `/project` overlay) now
|
|
16
|
+
shows the real provider alongside each role's model
|
|
17
|
+
(`CockpitView.teamRoleLabel()`), so it's clear which subscription
|
|
18
|
+
actually serves each role.
|
|
19
|
+
- `/project cursor exhausted|available` — a manual, human-reported
|
|
20
|
+
toggle for Cursor's quota state, persisted through the same
|
|
21
|
+
per-provider usage store Codex/Claude/OpenCode Go already use. Cursor
|
|
22
|
+
exposes no real, zero-cost local usage/billing read, so this is the
|
|
23
|
+
one honest signal Kairo can act on; an exhausted Cursor is excluded
|
|
24
|
+
from team recommendations until cleared.
|
|
25
|
+
- An early "LOW" warning tag (`LOW_QUOTA_WARN_PERCENT`, 20% remaining)
|
|
26
|
+
for Codex/Claude/OpenCode Go quota windows, shown in the compact usage
|
|
27
|
+
bar and `/usage`, strictly before the existing 5% hard-exclusion
|
|
28
|
+
threshold takes effect.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- The `/project` overlay's modal frame now uses a distinct,
|
|
33
|
+
high-contrast border instead of sharing the dashboard card's
|
|
34
|
+
low-contrast outline, and compacted its per-line spacing so a full
|
|
35
|
+
six-role team stays within the overlay's height budget.
|
|
36
|
+
|
|
8
37
|
## 0.17.0 — 2026-09-17 (Kairo Runtime)
|
|
9
38
|
|
|
10
39
|
Minor release. Execution-worktree isolation (real git worktrees, per-role
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kal-elsam/kairo-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Pi, Engram, and Graphify.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Kal-elSam/harness#readme",
|
|
@@ -230,7 +230,7 @@ export async function runCockpitApp({
|
|
|
230
230
|
// blocks with no indication of which command produced which one.
|
|
231
231
|
pushTranscript("user", task);
|
|
232
232
|
if (command === "/help") {
|
|
233
|
-
pushTranscript("kairo", "Shift+Tab cycles ASK/PLAN/AGENT · /project interactive overlay (or analyze/analyst/approve/refresh/status subcommands for scripted use) · /plan <task> force a plan · /usage automatic-provider status (Codex/Claude/Go) · /providers all connections incl. Zen/Cursor (manual) · /models CAPABILITY + EFFICIENT picks (--evidence for raw metrics) · /why eligibility detail · /clear · /quit");
|
|
233
|
+
pushTranscript("kairo", "Shift+Tab cycles ASK/PLAN/AGENT · /project interactive overlay (or analyze/analyst/approve/refresh/status/cursor exhausted|available subcommands for scripted use) · /plan <task> force a plan · /usage automatic-provider status (Codex/Claude/Go) · /providers all connections incl. Zen/Cursor (manual) · /models CAPABILITY + EFFICIENT picks (--evidence for raw metrics) · /why eligibility detail · /clear · /quit");
|
|
234
234
|
} else if (command === "/usage") {
|
|
235
235
|
for (const line of view.usageLines()) pushTranscript("kairo", line);
|
|
236
236
|
} else if (command === "/providers") {
|
|
@@ -343,8 +343,26 @@ export async function runCockpitApp({
|
|
|
343
343
|
const result = await service.refreshProjectStrategy({ cwd });
|
|
344
344
|
pushTranscript("kairo", result ? `Project strategy is now ${result.status.toUpperCase()}.` : "Nothing to refresh yet — use /project analyze first.");
|
|
345
345
|
}).finally(() => { editor.disableSubmit = false; });
|
|
346
|
+
} else if (sub === "cursor") {
|
|
347
|
+
// Cursor exposes no real, zero-cost local usage read (see
|
|
348
|
+
// execution-router.js's checkCandidate doc) — this manual toggle
|
|
349
|
+
// is the only way Kairo learns its quota state, ever. Never
|
|
350
|
+
// auto-detected, never inferred from a failed run.
|
|
351
|
+
const state = args[1]?.toLowerCase();
|
|
352
|
+
if (state !== "exhausted" && state !== "available") {
|
|
353
|
+
pushTranscript("kairo", "Usage: /project cursor exhausted|available");
|
|
354
|
+
editor.setText("");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
editor.setText("");
|
|
358
|
+
return runAction(`Marking Cursor ${state}`, async () => {
|
|
359
|
+
await service.setCursorManualQuota({ exhausted: state === "exhausted" });
|
|
360
|
+
pushTranscript("kairo", state === "exhausted"
|
|
361
|
+
? "Cursor marked out of credits — excluded from team suggestions until you run /project cursor available."
|
|
362
|
+
: "Cursor marked available again — back in team suggestions.");
|
|
363
|
+
});
|
|
346
364
|
} else {
|
|
347
|
-
pushTranscript("kairo", "Usage: /project status|analyze|analyst quality|efficient [--confirm]|approve|refresh");
|
|
365
|
+
pushTranscript("kairo", "Usage: /project status|analyze|analyst quality|efficient [--confirm]|approve|refresh|cursor exhausted|available");
|
|
348
366
|
}
|
|
349
367
|
} else if (command === "/plan") {
|
|
350
368
|
const planTask = task.slice(command.length).trim();
|
|
@@ -33,6 +33,13 @@ export const PROJECT_OVERLAY_STATE = {
|
|
|
33
33
|
|
|
34
34
|
const S = PROJECT_OVERLAY_STATE;
|
|
35
35
|
|
|
36
|
+
// Give the modal frame a distinct, high-contrast outline without changing
|
|
37
|
+
// the shared dashboard-card palette or the semantic state color of its rail.
|
|
38
|
+
const overlayFrameTheme = {
|
|
39
|
+
...theme,
|
|
40
|
+
fg: (role, text) => theme.fg(role === "border" ? "info" : role, text)
|
|
41
|
+
};
|
|
42
|
+
|
|
36
43
|
function teamLines(label, team, aiTeamLabel, tone = "bold") {
|
|
37
44
|
const lines = [tone === "bold" ? theme.bold(label) : theme.fg("muted", label)];
|
|
38
45
|
if (!team?.length) {
|
|
@@ -263,7 +270,7 @@ export class ProjectOverlay {
|
|
|
263
270
|
buildResultRoleList() {
|
|
264
271
|
const team = this.suggestedStrategy?.projectTeam ?? [];
|
|
265
272
|
const items = team.map((entry) => {
|
|
266
|
-
const modelText = entry.model ? this.view.
|
|
273
|
+
const modelText = entry.model ? this.view.teamRoleLabel(entry.model) : "no eligible option";
|
|
267
274
|
const overrideNote = entry.assignmentSource === "override" ? theme.fg("accent", " (override)") : "";
|
|
268
275
|
// Role + model are the real primary information here — explicit
|
|
269
276
|
// `text` color, never left to default/muted.
|
|
@@ -526,8 +533,11 @@ export class ProjectOverlay {
|
|
|
526
533
|
// isn't a modal boundary a human eye reliably notices.
|
|
527
534
|
const box = new Box(2, 1);
|
|
528
535
|
this.box = box;
|
|
529
|
-
const aiTeamLabel = (model) => this.view.
|
|
530
|
-
|
|
536
|
+
const aiTeamLabel = (model) => this.view.teamRoleLabel(model);
|
|
537
|
+
// Text defaults to one blank row above and below every child; inside
|
|
538
|
+
// a framed modal that inflated the height until pi-tui clipped the
|
|
539
|
+
// bottom border. Keep spacing explicit and compact instead.
|
|
540
|
+
const push = (text) => box.addChild(new Text(text, 0, 0));
|
|
531
541
|
// The real, ticking spinner+elapsed-time line every other in-flight
|
|
532
542
|
// action in the cockpit already uses (view.beginAction/tickSpinner/
|
|
533
543
|
// actionStatusLine — app.js's own fast timer keeps it live) — never a
|
|
@@ -655,7 +665,7 @@ export class ProjectOverlay {
|
|
|
655
665
|
break;
|
|
656
666
|
}
|
|
657
667
|
const innerWidth = cardInnerWidth(width);
|
|
658
|
-
return renderPanel("Project", this.panelTone(),
|
|
668
|
+
return renderPanel("Project", this.panelTone(), overlayFrameTheme, width, box.render(innerWidth));
|
|
659
669
|
}
|
|
660
670
|
}
|
|
661
671
|
|
|
@@ -2,6 +2,22 @@ import { matchesKey, Key, truncateToWidth, visibleWidth, wrapTextWithAnsi } from
|
|
|
2
2
|
import { buildTaskRows, clampSelection, isActionAvailable } from "./rows.js";
|
|
3
3
|
import { CARD_TONE, cardBottom, cardInnerWidth, cardLine, cardTop, renderPanel as renderPanelWithTheme } from "./card.js";
|
|
4
4
|
import { theme } from "./theme.js";
|
|
5
|
+
import { LOW_QUOTA_WARN_PERCENT } from "../intelligence/execution-router.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A real, early heads-up — never fabricated, never re-deriving its own
|
|
9
|
+
* threshold (see execution-router.js's LOW_QUOTA_WARN_PERCENT, the same
|
|
10
|
+
* canonical policy checkCandidate itself uses for its harder exclusion
|
|
11
|
+
* cutoff). `alreadyFlagged` skips this for a window a caller already
|
|
12
|
+
* tagged some other way (e.g. Go's own "RATE LIMITED"), so a single
|
|
13
|
+
* window is never double-tagged.
|
|
14
|
+
* @param {number|null|undefined} remainingPercent
|
|
15
|
+
* @param {boolean} [alreadyFlagged]
|
|
16
|
+
*/
|
|
17
|
+
function quotaWarnSuffix(remainingPercent, alreadyFlagged = false) {
|
|
18
|
+
if (alreadyFlagged || remainingPercent == null) return "";
|
|
19
|
+
return remainingPercent < LOW_QUOTA_WARN_PERCENT ? " LOW" : "";
|
|
20
|
+
}
|
|
5
21
|
|
|
6
22
|
/** view.js's own local binding for card.js's real renderPanel, fixed to this module's theme. */
|
|
7
23
|
function renderPanel(title, tone, width, contentLines, targetLineCount = contentLines.length) {
|
|
@@ -560,8 +576,8 @@ export class CockpitView {
|
|
|
560
576
|
};
|
|
561
577
|
}
|
|
562
578
|
const lines = [];
|
|
563
|
-
if (strategy.bootstrapAnalyst) lines.push(`${"Project Analyst".padEnd(18)} ${this.
|
|
564
|
-
if (strategy.orchestrator) lines.push(`${"Orchestrator".padEnd(18)} ${this.
|
|
579
|
+
if (strategy.bootstrapAnalyst) lines.push(`${"Project Analyst".padEnd(18)} ${this.teamRoleLabel(strategy.bootstrapAnalyst)}`);
|
|
580
|
+
if (strategy.orchestrator) lines.push(`${"Orchestrator".padEnd(18)} ${this.teamRoleLabel(strategy.orchestrator)}`);
|
|
565
581
|
// The real OPERATIONAL team (see buildProjectStrategy's own doc) —
|
|
566
582
|
// never qualityTeam, which is comparative reference only. The overlay
|
|
567
583
|
// (project-overlay.js) already shows projectTeam under this exact
|
|
@@ -570,7 +586,7 @@ export class CockpitView {
|
|
|
570
586
|
// remains as a fallback for a strategy persisted before projectTeam
|
|
571
587
|
// existed (see applyProjectTeamOverride's own legacy-entry comment).
|
|
572
588
|
for (const entry of strategy.projectTeam ?? strategy.qualityTeam ?? []) {
|
|
573
|
-
lines.push(`${entry.role.padEnd(18)} ${entry.model ? this.
|
|
589
|
+
lines.push(`${entry.role.padEnd(18)} ${entry.model ? this.teamRoleLabel(entry.model) : theme.fg("warning", "no eligible option")}`);
|
|
574
590
|
}
|
|
575
591
|
if (strategy.status === "suggested") lines.push(theme.fg("muted", "Suggested from real project analysis. Use /project approve to activate."));
|
|
576
592
|
if (strategy.status === "stale") lines.push(theme.fg("warning", "Real evidence changed since approval — use /project refresh."));
|
|
@@ -741,6 +757,19 @@ export class CockpitView {
|
|
|
741
757
|
return `${provider} · ${raw}`;
|
|
742
758
|
}
|
|
743
759
|
|
|
760
|
+
/**
|
|
761
|
+
* Role -> model -> provider, for the PROJECT TEAM listing specifically:
|
|
762
|
+
* keeps aiTeamLabel()'s cleaned modelName (unlike aiTeamLabelWithProvider's
|
|
763
|
+
* raw/technical variant) but still names the real adapter each role would
|
|
764
|
+
* actually run against — two roles can land on visually similar model
|
|
765
|
+
* names from different providers, and knowing which subscription a role
|
|
766
|
+
* draws from is exactly what a real, provider-aware team review needs.
|
|
767
|
+
*/
|
|
768
|
+
teamRoleLabel(model) {
|
|
769
|
+
const provider = model.adapterId.charAt(0).toUpperCase() + model.adapterId.slice(1);
|
|
770
|
+
return `${this.aiTeamLabel(model)} · ${provider}`;
|
|
771
|
+
}
|
|
772
|
+
|
|
744
773
|
/**
|
|
745
774
|
* The global "AI TEAM" widget: one line per role (Explorer / Architect /
|
|
746
775
|
* Builder / Debugger / Tester / Reviewer) naming only the
|
|
@@ -1155,17 +1184,20 @@ export class CockpitView {
|
|
|
1155
1184
|
|
|
1156
1185
|
const codex = usage.codex;
|
|
1157
1186
|
const codexText = codex?.primary
|
|
1158
|
-
? `Codex 5h ${codex.primary.remainingPercent}%${codex.secondary ? ` / W ${codex.secondary.remainingPercent}
|
|
1187
|
+
? `Codex 5h ${codex.primary.remainingPercent}%${quotaWarnSuffix(codex.primary.remainingPercent)}${codex.secondary ? ` / W ${codex.secondary.remainingPercent}%${quotaWarnSuffix(codex.secondary.remainingPercent)}` : ""}`
|
|
1159
1188
|
: `Codex ${status("Codex") ?? "usage unknown"}`;
|
|
1160
1189
|
|
|
1161
1190
|
const claude = usage.claude;
|
|
1162
1191
|
const claudeText = claude?.primary
|
|
1163
|
-
? `Claude S ${claude.primary.remainingPercent}%${claude.secondary ? ` / W ${claude.secondary.remainingPercent}
|
|
1192
|
+
? `Claude S ${claude.primary.remainingPercent}%${quotaWarnSuffix(claude.primary.remainingPercent)}${claude.secondary ? ` / W ${claude.secondary.remainingPercent}%${quotaWarnSuffix(claude.secondary.remainingPercent)}` : ""}`
|
|
1164
1193
|
: `Claude ${status("Claude") ?? "usage unknown"}`;
|
|
1165
1194
|
|
|
1166
1195
|
const go = usage.opencode?.go;
|
|
1167
1196
|
const goText = go?.windows?.length
|
|
1168
|
-
? `Go ${go.windows.map((window) =>
|
|
1197
|
+
? `Go ${go.windows.map((window) => {
|
|
1198
|
+
const limited = window.status === "rate-limited";
|
|
1199
|
+
return `${window.remainingPercent}%${limited ? " LIMITED" : quotaWarnSuffix(window.remainingPercent)}`;
|
|
1200
|
+
}).join(" / ")}`
|
|
1169
1201
|
: `Go ${status("OpenCode") ?? "usage unknown"}`;
|
|
1170
1202
|
|
|
1171
1203
|
const header = `KAIRO · ${project}`;
|
|
@@ -1232,15 +1264,18 @@ export class CockpitView {
|
|
|
1232
1264
|
const lines = [];
|
|
1233
1265
|
const codex = usage.codex;
|
|
1234
1266
|
lines.push(codex?.windows?.length
|
|
1235
|
-
? `Codex ${codex.windows.map((window) => `${window.name} ${window.remainingPercent}% left${window.resetsAtIso ? ` reset ${window.resetsAtIso}` : ""}`).join(" · ")} · source: ${codex.source ?? "measured"}`
|
|
1267
|
+
? `Codex ${codex.windows.map((window) => `${window.name} ${window.remainingPercent}% left${quotaWarnSuffix(window.remainingPercent)}${window.resetsAtIso ? ` reset ${window.resetsAtIso}` : ""}`).join(" · ")} · source: ${codex.source ?? "measured"}`
|
|
1236
1268
|
: "Codex usage unknown · source: Codex app-server · no quota fabricated");
|
|
1237
1269
|
const claude = usage.claude;
|
|
1238
1270
|
lines.push(claude?.windows?.length
|
|
1239
|
-
? `Claude ${claude.windows.map((window) => `${window.label ?? window.name} ${window.remainingPercent}% left`).join(" · ")} · source: ${claude.source ?? "measured"}`
|
|
1271
|
+
? `Claude ${claude.windows.map((window) => `${window.label ?? window.name} ${window.remainingPercent}% left${quotaWarnSuffix(window.remainingPercent)}`).join(" · ")} · source: ${claude.source ?? "measured"}`
|
|
1240
1272
|
: "Claude usage unknown · no quota fabricated");
|
|
1241
1273
|
const go = usage.opencode?.go;
|
|
1242
1274
|
lines.push(go?.windows?.length
|
|
1243
|
-
? `Go ${go.windows.map((window) =>
|
|
1275
|
+
? `Go ${go.windows.map((window) => {
|
|
1276
|
+
const limited = window.status === "rate-limited";
|
|
1277
|
+
return `${shortWindowName(window.name)} ${window.remainingPercent}%${limited ? " RATE LIMITED" : quotaWarnSuffix(window.remainingPercent)}`;
|
|
1278
|
+
}).join(" · ")} · source: ${go.source ?? "measured"}`
|
|
1244
1279
|
: "Go usage unknown · source unavailable");
|
|
1245
1280
|
return lines;
|
|
1246
1281
|
}
|
|
@@ -35,6 +35,7 @@ import { runCodexSandboxedBootstrap } from "./codex-sandbox.js";
|
|
|
35
35
|
import { createBootstrapAnalyzerAdapter } from "./bootstrap-analyzer-adapters.js";
|
|
36
36
|
import { verifyClaudeSubscriptionAuth } from "../runtime/execution-adapters/claude.js";
|
|
37
37
|
import { readProjectStrategy, writeProjectStrategy } from "./project-strategy-store.js";
|
|
38
|
+
import { readProviderUsage, writeProviderUsage } from "../runtime/usage-store.js";
|
|
38
39
|
import { resolveProjectRoute } from "./project-router.js";
|
|
39
40
|
import { readArtificialAnalysisModels } from "../observability/artificial-analysis-models.js";
|
|
40
41
|
import { readHuggingFaceLeaderboard } from "../observability/huggingface-leaderboard.js";
|
|
@@ -269,6 +270,8 @@ export function createConversationService(deps = {}) {
|
|
|
269
270
|
const computeProjectProfileImpl = deps.computeProjectProfile ?? computeProjectProfile;
|
|
270
271
|
const readProjectStrategyImpl = deps.readProjectStrategy ?? readProjectStrategy;
|
|
271
272
|
const writeProjectStrategyImpl = deps.writeProjectStrategy ?? writeProjectStrategy;
|
|
273
|
+
const readProviderUsageImpl = deps.readProviderUsage ?? readProviderUsage;
|
|
274
|
+
const writeProviderUsageImpl = deps.writeProviderUsage ?? writeProviderUsage;
|
|
272
275
|
const parseProjectAnalysisImpl = deps.parseProjectAnalysis ?? parseProjectAnalysis;
|
|
273
276
|
const deriveRoleRequirementsImpl = deps.deriveRoleRequirements ?? deriveRoleRequirements;
|
|
274
277
|
const buildSanitizedSnapshotImpl = deps.buildSanitizedSnapshot ?? buildSanitizedSnapshot;
|
|
@@ -446,10 +449,15 @@ export function createConversationService(deps = {}) {
|
|
|
446
449
|
// launchable), so Kairo never actually picks Go for a real run
|
|
447
450
|
// it's guaranteed to reject at launch. Zen and Cursor stay
|
|
448
451
|
// excluded here regardless (PAYG risk / manual-only).
|
|
452
|
+
// Cursor's own real usage/billing data is never auto-detected (see
|
|
453
|
+
// execution-router.js's checkCandidate doc) — this is only ever
|
|
454
|
+
// the human's last word via /project cursor exhausted|available,
|
|
455
|
+
// persisted as an ordinary provider usage record.
|
|
456
|
+
const cursorManualQuota = await readProviderUsageImpl(homeDir, "cursor");
|
|
449
457
|
const eligibility = {};
|
|
450
458
|
const candidates = [];
|
|
451
459
|
for (const adapterId of ["codex", "claude", "opencode-go", "opencode-zen", "cursor"]) {
|
|
452
|
-
const check = checkCandidate(adapterId, { adapters, codexUsage, claudeUsage, opencodeGoUsage: opencodeUsage?.go }, { requireLaunchable: false });
|
|
460
|
+
const check = checkCandidate(adapterId, { adapters, codexUsage, claudeUsage, opencodeGoUsage: opencodeUsage?.go, cursorManualQuota }, { requireLaunchable: false });
|
|
453
461
|
eligibility[adapterId] = check;
|
|
454
462
|
if (check.ok) candidates.push(adapterId);
|
|
455
463
|
}
|
|
@@ -815,6 +823,25 @@ export function createConversationService(deps = {}) {
|
|
|
815
823
|
}
|
|
816
824
|
return { ...existing, projectRoot, profile };
|
|
817
825
|
},
|
|
826
|
+
/**
|
|
827
|
+
* `/project cursor exhausted|available`: the ONLY way Cursor's quota
|
|
828
|
+
* state ever changes (see execution-router.js's checkCandidate doc —
|
|
829
|
+
* Cursor exposes no real, zero-cost local usage read, so this is never
|
|
830
|
+
* auto-detected). Persisted as an ordinary provider usage record
|
|
831
|
+
* (runtime/usage-store.js), read back by snapshot()'s own eligibility
|
|
832
|
+
* computation on every poll — never held only in memory, so it
|
|
833
|
+
* survives a restart the same way a real detected quota state would.
|
|
834
|
+
* @param {{exhausted: boolean, reason?: string|null}} args
|
|
835
|
+
*/
|
|
836
|
+
async setCursorManualQuota({ exhausted, reason = null }) {
|
|
837
|
+
const record = {
|
|
838
|
+
provider: "cursor", manualExhausted: !!exhausted,
|
|
839
|
+
reason: exhausted ? (reason ?? "Cursor marked out of credits (manual, via /project cursor exhausted)") : null,
|
|
840
|
+
setAt: new Date().toISOString()
|
|
841
|
+
};
|
|
842
|
+
await writeProviderUsageImpl(homeDir, "cursor", record);
|
|
843
|
+
return record;
|
|
844
|
+
},
|
|
818
845
|
/**
|
|
819
846
|
* The real projectTeam edit catalog for one role (section 4 —
|
|
820
847
|
* "Edición persistida del PROJECT TEAM") — every real, non-superseded
|
|
@@ -126,7 +126,13 @@ function remainingPercent(usageEntry) {
|
|
|
126
126
|
// Below this real remaining-quota percentage, a provider is treated as
|
|
127
127
|
// exhausted for automatic routing — conserved for the tests explicitly
|
|
128
128
|
// listed as this increment's scope, not a newly-invented number.
|
|
129
|
-
const MIN_QUOTA_PERCENT = 5;
|
|
129
|
+
export const MIN_QUOTA_PERCENT = 5;
|
|
130
|
+
|
|
131
|
+
// A softer, earlier heads-up threshold — strictly above MIN_QUOTA_PERCENT,
|
|
132
|
+
// so the human sees a warning before a provider actually gets excluded,
|
|
133
|
+
// never after. Display-only: it never affects checkCandidate's own
|
|
134
|
+
// eligibility verdict (see CockpitView's own status-bar consumer).
|
|
135
|
+
export const LOW_QUOTA_WARN_PERCENT = 20;
|
|
130
136
|
|
|
131
137
|
/**
|
|
132
138
|
* The single eligibility policy shared by execution routing, ask routing,
|
|
@@ -135,7 +141,14 @@ const MIN_QUOTA_PERCENT = 5;
|
|
|
135
141
|
* Real availability/launchability/quota only; never a capability judgment
|
|
136
142
|
* (that's scoreAvailableModels' job, applied only to survivors of this).
|
|
137
143
|
* @param {string} adapterId - "codex" | "claude" | "opencode-go" | "opencode-zen" | "cursor"
|
|
138
|
-
* @param {{adapters: object[], codexUsage?: object|null, claudeUsage?: object|null, opencodeGoUsage?: object|null}} context
|
|
144
|
+
* @param {{adapters: object[], codexUsage?: object|null, claudeUsage?: object|null, opencodeGoUsage?: object|null, cursorManualQuota?: {manualExhausted: boolean, reason?: string|null}|null}} context -
|
|
145
|
+
* `cursorManualQuota` is the human-reported override (see
|
|
146
|
+
* runtime/usage-store.js's `cursor.json` record, set via
|
|
147
|
+
* `/project cursor exhausted|available`) — Cursor exposes no real,
|
|
148
|
+
* zero-cost local quota read (its CLI has no usage/billing subcommand
|
|
149
|
+
* and a successful `-p` call only reports per-request token counts, not
|
|
150
|
+
* remaining account balance), so unlike Codex/Claude/OpenCode Go this is
|
|
151
|
+
* never auto-detected, only ever what the human last told Kairo.
|
|
139
152
|
* @returns {{ok: boolean, reason: string|null}}
|
|
140
153
|
* @param {{requireLaunchable?: boolean}} [options] - `requireLaunchable: false`
|
|
141
154
|
* is for AI TEAM's recommendation surface only (service.js's snapshot()):
|
|
@@ -151,7 +164,7 @@ const MIN_QUOTA_PERCENT = 5;
|
|
|
151
164
|
* uses the default `true` — it must never pick something guaranteed to
|
|
152
165
|
* fail at launch (run-manager.js's own launchable gate would reject it).
|
|
153
166
|
*/
|
|
154
|
-
export function checkCandidate(adapterId, { adapters, codexUsage, claudeUsage, opencodeGoUsage }, { requireLaunchable = true } = {}) {
|
|
167
|
+
export function checkCandidate(adapterId, { adapters, codexUsage, claudeUsage, opencodeGoUsage, cursorManualQuota }, { requireLaunchable = true } = {}) {
|
|
155
168
|
// Zen carries real PAYG/billing risk (see conversation/service.js's
|
|
156
169
|
// capabilities.openCodeExecution) — never an automatic pick, regardless
|
|
157
170
|
// of what its real catalog/benchmarks might otherwise say.
|
|
@@ -195,6 +208,15 @@ export function checkCandidate(adapterId, { adapters, codexUsage, claudeUsage, o
|
|
|
195
208
|
const limited = windows.find((window) => window.status === "rate-limited");
|
|
196
209
|
if (limited) return { ok: false, reason: `OpenCode Go ${limited.name} window is rate-limited` };
|
|
197
210
|
}
|
|
211
|
+
// Cursor: only ever the human's own last word (see this function's own
|
|
212
|
+
// doc) — never fabricated from a guess. Checked here, after the
|
|
213
|
+
// requireLaunchable-gated manual-only return above, so it only ever
|
|
214
|
+
// takes effect on the recommendation path (requireLaunchable: false) —
|
|
215
|
+
// real task routing already refuses Cursor unconditionally regardless
|
|
216
|
+
// of quota.
|
|
217
|
+
if (adapterId === "cursor" && cursorManualQuota?.manualExhausted) {
|
|
218
|
+
return { ok: false, reason: cursorManualQuota.reason ?? "Cursor marked out of credits (manual, via /project cursor exhausted)" };
|
|
219
|
+
}
|
|
198
220
|
return { ok: true, reason: null };
|
|
199
221
|
}
|
|
200
222
|
|
|
@@ -89,7 +89,7 @@ export async function readCodexModels({
|
|
|
89
89
|
child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before model list")); });
|
|
90
90
|
|
|
91
91
|
writeRequest(child, 1, "initialize", {
|
|
92
|
-
clientInfo: { name: "kairo", title: "Kairo", version: "0.
|
|
92
|
+
clientInfo: { name: "kairo", title: "Kairo", version: "0.18.0" },
|
|
93
93
|
capabilities: {}
|
|
94
94
|
});
|
|
95
95
|
});
|
|
@@ -151,7 +151,7 @@ export async function readCodexUsage({
|
|
|
151
151
|
child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before rate limits")); });
|
|
152
152
|
|
|
153
153
|
writeRequest(child, 1, "initialize", {
|
|
154
|
-
clientInfo: { name: "kairo", title: "Kairo", version: "0.
|
|
154
|
+
clientInfo: { name: "kairo", title: "Kairo", version: "0.18.0" },
|
|
155
155
|
capabilities: {}
|
|
156
156
|
});
|
|
157
157
|
});
|