@kal-elsam/kairo-runtime 0.23.1 → 0.24.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 +48 -0
- package/package.json +1 -1
- package/src/global/conversation/project-strategy.js +6 -2
- package/src/global/conversation/service.js +30 -2
- package/src/global/intelligence/execution-router.js +14 -2
- package/src/global/intelligence/quick-ask.js +30 -4
- 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,54 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.24.0 — 2026-09-18 (Kairo Runtime)
|
|
9
|
+
|
|
10
|
+
Minor release. ASK mode joins PROJECT TEAM.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- ASK mode now checks PROJECT TEAM's Explorer role first (read-only
|
|
15
|
+
investigation is exactly Explorer's job), via the same routing real
|
|
16
|
+
execution already uses — instead of always going through a fully
|
|
17
|
+
separate, generic quota/effort heuristic. Falls back to that
|
|
18
|
+
heuristic only when there's no active team yet, or Explorer's real
|
|
19
|
+
assignment isn't one ASK can actually invoke (Cursor/OpenCode
|
|
20
|
+
Go/Zen aren't ask-capable yet). Role is always Explorer, never
|
|
21
|
+
inferred from the question's text.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- A flaky timing test in `quick-ask.test.js` (from 0.23.2) used
|
|
26
|
+
margins too tight to reliably survive full-suite load; widened them.
|
|
27
|
+
|
|
28
|
+
Not yet in this release: PLAN mode routing through the Orchestrator
|
|
29
|
+
role (plan creation is currently structurally Codex-only, a bigger
|
|
30
|
+
change than a routing choice), and AGENT mode (an open product
|
|
31
|
+
question on whether it should skip the plan→approve gate).
|
|
32
|
+
|
|
33
|
+
## 0.23.2 — 2026-09-18 (Kairo Runtime)
|
|
34
|
+
|
|
35
|
+
Patch release.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Routing eligibility (`checkCandidate`, shared by real execution
|
|
40
|
+
routing, ASK routing, and the FIT widget) and ASK's own ordering
|
|
41
|
+
only ever read the primary (5h) quota window — a provider whose
|
|
42
|
+
weekly window was nearly exhausted still got picked first and was
|
|
43
|
+
never excluded, as long as its 5h window looked healthy. Now takes
|
|
44
|
+
the worse of the two windows everywhere, fail-closed, matching the
|
|
45
|
+
same principle already applied to OpenCode Go's own windows.
|
|
46
|
+
- `askCodex`/`askClaude` (the ASK-mode question path) used a fixed 30s
|
|
47
|
+
deadline from process start. Codex's ASK path always uses the
|
|
48
|
+
provider's single default model regardless of question complexity
|
|
49
|
+
(no effort-based tiering for Codex today), so a heavier default
|
|
50
|
+
model plus a cold sandboxed `codex exec` start can genuinely exceed
|
|
51
|
+
30s with real quota to spare — a real, slow answer, not a hang. Both
|
|
52
|
+
ask calls now reset their timeout on every real stdout/stderr chunk
|
|
53
|
+
instead, never an absolute one, so only a genuine hang still times
|
|
54
|
+
out.
|
|
55
|
+
|
|
8
56
|
## 0.23.1 — 2026-09-18 (Kairo Runtime)
|
|
9
57
|
|
|
10
58
|
Patch release.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kal-elsam/kairo-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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",
|
|
@@ -23,8 +23,12 @@ import { computeRoleEvaluations } from "../intelligence/capability-scoring.js";
|
|
|
23
23
|
// The Bootstrap Analyst investigates read-only via askProvider
|
|
24
24
|
// (intelligence/quick-ask.js), which only actually supports these two
|
|
25
25
|
// providers today — offering any other real candidate as an "alternative"
|
|
26
|
-
// here would be a menu item Kairo can't actually run.
|
|
27
|
-
|
|
26
|
+
// here would be a menu item Kairo can't actually run. Exported: ASK mode's
|
|
27
|
+
// own real-time routing (service.js's planAsk) needs this exact same real
|
|
28
|
+
// constraint when it tries to route a plain question through a PROJECT
|
|
29
|
+
// TEAM role — a role assigned to, say, opencode-go is a real, valid team
|
|
30
|
+
// assignment, just not one askProvider can call.
|
|
31
|
+
export const ASK_SUPPORTED_ADAPTERS = new Set(["codex", "claude"]);
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* The Bootstrap Analyst as a temporary, read-only WORKFLOW — deliberately
|
|
@@ -26,8 +26,8 @@ import { appendTranscriptEntry, clearTranscript, readTranscript } from "./transc
|
|
|
26
26
|
import { readSession, writeSessionMode } from "./session-store.js";
|
|
27
27
|
import { computeProjectProfile } from "./project-profile.js";
|
|
28
28
|
import {
|
|
29
|
-
buildProjectStrategy, computeBootstrapAnalystAlternatives, computeBootstrapAnalystCatalog,
|
|
30
|
-
computeProjectTeamEditCatalog, applyProjectTeamOverride, resetProjectTeamAssignment
|
|
29
|
+
ASK_SUPPORTED_ADAPTERS, buildProjectStrategy, computeBootstrapAnalystAlternatives, computeBootstrapAnalystCatalog,
|
|
30
|
+
isStrategyStale, computeProjectTeamEditCatalog, applyProjectTeamOverride, resetProjectTeamAssignment
|
|
31
31
|
} from "./project-strategy.js";
|
|
32
32
|
import { buildAnalystPrompt, deriveRoleRequirements, parseProjectAnalysis } from "./project-analysis.js";
|
|
33
33
|
import { buildSanitizedSnapshot } from "./sanitized-snapshot.js";
|
|
@@ -662,6 +662,34 @@ export function createConversationService(deps = {}) {
|
|
|
662
662
|
*/
|
|
663
663
|
async planAsk({ cwd, task }) {
|
|
664
664
|
const projectRoot = await root(cwd);
|
|
665
|
+
// PROJECT TEAM's own Explorer role first — a real, approved,
|
|
666
|
+
// project-specific assignment beats the generic heuristic below,
|
|
667
|
+
// same principle as real execution routing. Explorer, never a
|
|
668
|
+
// guess from the question's text: resolveProjectRoute's whole
|
|
669
|
+
// design is that a role is always the caller's own explicit fixed
|
|
670
|
+
// choice, never inferred per-call — ASK questions are read-only
|
|
671
|
+
// investigation, which is exactly Explorer's job.
|
|
672
|
+
const teamRoute = await this.routeProjectExecution("Explorer", projectRoot);
|
|
673
|
+
const teamModel = teamRoute.decision === "ROUTED" ? teamRoute.model
|
|
674
|
+
: teamRoute.decision === "WAIT_FOR_PROJECT_TEAM" ? teamRoute.suggestedAlternative?.model ?? null
|
|
675
|
+
: null;
|
|
676
|
+
// Only when that real assignment is one askProvider can actually
|
|
677
|
+
// call (see ASK_SUPPORTED_ADAPTERS's own doc) — a role can be
|
|
678
|
+
// validly assigned to Cursor/OpenCode Go/Zen, which ASK simply
|
|
679
|
+
// can't invoke yet, so that's a real reason to fall through below,
|
|
680
|
+
// never an error.
|
|
681
|
+
if (teamModel && ASK_SUPPORTED_ADAPTERS.has(teamModel.adapterId)) {
|
|
682
|
+
return {
|
|
683
|
+
decision: {
|
|
684
|
+
decision: "ROUTED", provider: teamModel.adapterId, model: teamModel.modelId,
|
|
685
|
+
why: `Explorer (PROJECT TEAM): ${teamRoute.why}`
|
|
686
|
+
},
|
|
687
|
+
projectRoot
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
// No active team, or Explorer's real assignment isn't ask-capable —
|
|
691
|
+
// fall back to the generic quota/capability heuristic so ASK stays
|
|
692
|
+
// useful even before a team is approved.
|
|
665
693
|
const adapters = inspectAdapters({ cwd: projectRoot });
|
|
666
694
|
let codexUsage = null;
|
|
667
695
|
let claudeUsage = null;
|
|
@@ -118,9 +118,21 @@ function findAdapter(adapterId, adapters) {
|
|
|
118
118
|
return adapters.find((adapter) => adapter.id === baseId) ?? null;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* The WORST remaining headroom across both real windows (5h "primary" and
|
|
123
|
+
* weekly "secondary") — never just the primary one. A provider whose
|
|
124
|
+
* weekly quota is nearly gone must be treated that way everywhere
|
|
125
|
+
* (exclusion AND ordering) even while its 5h window still looks healthy;
|
|
126
|
+
* otherwise Kairo keeps routing to it, burning through the one budget
|
|
127
|
+
* that's actually about to run out. Mirrors the same fail-closed
|
|
128
|
+
* principle checkCandidate already applies to OpenCode Go's windows (any
|
|
129
|
+
* one window being real trouble is real trouble, full stop).
|
|
130
|
+
* @param {object|null} usageEntry - a codex/claude usage-probe result (primary/secondary windows)
|
|
131
|
+
*/
|
|
122
132
|
function remainingPercent(usageEntry) {
|
|
123
|
-
|
|
133
|
+
const known = [usageEntry?.primary?.remainingPercent, usageEntry?.secondary?.remainingPercent]
|
|
134
|
+
.filter((value) => typeof value === "number");
|
|
135
|
+
return known.length > 0 ? Math.min(...known) : null;
|
|
124
136
|
}
|
|
125
137
|
|
|
126
138
|
// Below this real remaining-quota percentage, a provider is treated as
|
|
@@ -37,6 +37,32 @@ function unknown(error) {
|
|
|
37
37
|
return { status: "error", answer: null, error: String(error) };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Resets on every real stdout/stderr chunk from the child, never an
|
|
42
|
+
* absolute deadline from process start — the same real distinction
|
|
43
|
+
* execution-adapters/opencode.js's own idle timeout draws: a real, live
|
|
44
|
+
* answer that's just taking a while (a heavier reasoning model, a cold
|
|
45
|
+
* sandbox start) must never be killed for merely being slow, only a
|
|
46
|
+
* process that's produced nothing at all for `timeoutMs` really looks
|
|
47
|
+
* hung. A single-shot ask call, so this stays local rather than reusing
|
|
48
|
+
* run-supervisor.js's own detached-run mechanism.
|
|
49
|
+
* @param {import("node:child_process").ChildProcess} child
|
|
50
|
+
* @param {number} timeoutMs
|
|
51
|
+
* @param {() => void} onIdle
|
|
52
|
+
* @returns {() => void} call to clear the timer once the call finishes
|
|
53
|
+
*/
|
|
54
|
+
function armIdleTimeout(child, timeoutMs, onIdle) {
|
|
55
|
+
let handle = null;
|
|
56
|
+
const reset = () => {
|
|
57
|
+
if (handle) clearTimeout(handle);
|
|
58
|
+
handle = setTimeout(onIdle, timeoutMs);
|
|
59
|
+
};
|
|
60
|
+
child.stdout?.on("data", reset);
|
|
61
|
+
child.stderr?.on("data", reset);
|
|
62
|
+
reset();
|
|
63
|
+
return () => { if (handle) clearTimeout(handle); };
|
|
64
|
+
}
|
|
65
|
+
|
|
40
66
|
/** @param {{question:string, model:string|null, cwd:string, spawn:Function, timeoutMs:number, env:object}} args */
|
|
41
67
|
function askClaude({ question, model, cwd, spawn, timeoutMs, env }) {
|
|
42
68
|
// --restricted: removes Bash/code-execution tools and WebFetch, ignores
|
|
@@ -57,11 +83,11 @@ function askClaude({ question, model, cwd, spawn, timeoutMs, env }) {
|
|
|
57
83
|
}
|
|
58
84
|
let stdout = "";
|
|
59
85
|
let finished = false;
|
|
60
|
-
const
|
|
86
|
+
const clearIdleTimer = armIdleTimeout(child, timeoutMs, () => finish(unknown(`claude -p idle-timed out after ${timeoutMs}ms with no output`)));
|
|
61
87
|
function finish(result) {
|
|
62
88
|
if (finished) return;
|
|
63
89
|
finished = true;
|
|
64
|
-
|
|
90
|
+
clearIdleTimer();
|
|
65
91
|
try { child.kill?.(); } catch { /* best effort */ }
|
|
66
92
|
resolve(result);
|
|
67
93
|
}
|
|
@@ -106,11 +132,11 @@ async function askCodex({ question, model, cwd, spawn, timeoutMs, env }) {
|
|
|
106
132
|
return;
|
|
107
133
|
}
|
|
108
134
|
let finished = false;
|
|
109
|
-
const
|
|
135
|
+
const clearIdleTimer = armIdleTimeout(child, timeoutMs, () => finish(unknown(`codex exec idle-timed out after ${timeoutMs}ms with no output`)));
|
|
110
136
|
function finish(result) {
|
|
111
137
|
if (finished) return;
|
|
112
138
|
finished = true;
|
|
113
|
-
|
|
139
|
+
clearIdleTimer();
|
|
114
140
|
try { child.kill?.(); } catch { /* best effort */ }
|
|
115
141
|
resolve(result);
|
|
116
142
|
}
|
|
@@ -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.24.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.24.0" },
|
|
155
155
|
capabilities: {}
|
|
156
156
|
});
|
|
157
157
|
});
|