@kal-elsam/kairo-runtime 0.8.0 → 0.9.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.
@@ -0,0 +1,249 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile } from "node:fs/promises";
3
+ import { harnessHomePaths } from "../../paths.js";
4
+ import { detectGlobalDrift, hasRepairableDrift } from "../../drift.js";
5
+ import { writeAtomicJson } from "../write-atomic-json.js";
6
+ import { listAlerts, saveAlert } from "../alerts/alert-store.js";
7
+ import { ALERT_SEVERITIES, ALERT_STATES } from "../alerts/alert-types.js";
8
+ import { listRunRecords } from "../run-store.js";
9
+ import { isRunAlive } from "../run-liveness.js";
10
+ import { RUN_STATES, isActiveRunState } from "../run-types.js";
11
+ import {
12
+ installAutostart, notifyNewAlert, removeAutostart, resolveMonitorPlatform
13
+ } from "./monitor-platform.js";
14
+
15
+ export {
16
+ installAutostart, notifyNewAlert, removeAutostart, resolveMonitorPlatform
17
+ } from "./monitor-platform.js";
18
+
19
+ const SOURCE = "monitor";
20
+ const INTERVAL = 300;
21
+
22
+ export class MonitorStateError extends Error {
23
+ constructor(message, { code = "corrupt_monitor_state" } = {}) {
24
+ super(message);
25
+ this.name = "MonitorStateError";
26
+ this.code = code;
27
+ }
28
+ }
29
+
30
+ export function defaultMonitorState() {
31
+ return {
32
+ version: 1, enabled: false, intervalSec: INTERVAL,
33
+ lastTickAt: null, lastTick: null,
34
+ autostart: {
35
+ platform: null, installed: false, supported: false, configured: false, loaded: false
36
+ },
37
+ updatedAt: null
38
+ };
39
+ }
40
+
41
+ function assertMonitorState(raw) {
42
+ if (!raw || typeof raw !== "object" || raw.version !== 1 || typeof raw.enabled !== "boolean") {
43
+ throw new MonitorStateError("Monitor state schema invalid.");
44
+ }
45
+ if (!Number.isFinite(raw.intervalSec) || raw.intervalSec < 1) {
46
+ throw new MonitorStateError("Monitor state.intervalSec invalid.");
47
+ }
48
+ const a = raw.autostart;
49
+ if (!a || typeof a !== "object"
50
+ || typeof a.supported !== "boolean"
51
+ || typeof a.configured !== "boolean"
52
+ || typeof a.loaded !== "boolean"
53
+ || typeof a.installed !== "boolean") {
54
+ throw new MonitorStateError("Monitor state.autostart invalid.");
55
+ }
56
+ if (a.loaded && !a.configured) {
57
+ throw new MonitorStateError("Monitor state.autostart loaded requires configured.");
58
+ }
59
+ if (a.installed !== a.loaded) {
60
+ throw new MonitorStateError("Monitor state.autostart installed must equal loaded.");
61
+ }
62
+ if (!a.supported && (a.configured || a.loaded || a.installed)) {
63
+ throw new MonitorStateError("Monitor state.autostart unsupported with lifecycle flags.");
64
+ }
65
+ if (!raw.enabled && (a.configured || a.loaded || a.installed)) {
66
+ throw new MonitorStateError("Monitor disabled with active autostart lifecycle.");
67
+ }
68
+ return {
69
+ ...defaultMonitorState(),
70
+ ...raw,
71
+ version: 1,
72
+ enabled: raw.enabled,
73
+ intervalSec: raw.intervalSec,
74
+ autostart: { ...defaultMonitorState().autostart, ...a }
75
+ };
76
+ }
77
+
78
+ export async function readMonitorState(homeDir, { repair = false } = {}) {
79
+ const path = harnessHomePaths(homeDir).monitorStatePath;
80
+ if (!existsSync(path)) return defaultMonitorState();
81
+ try {
82
+ return assertMonitorState(JSON.parse(await readFile(path, "utf8")));
83
+ } catch (error) {
84
+ if (repair) return defaultMonitorState();
85
+ if (error instanceof MonitorStateError) throw error;
86
+ throw new MonitorStateError("Monitor state unreadable.");
87
+ }
88
+ }
89
+
90
+ export async function writeMonitorState(homeDir, patch, { repair = false } = {}) {
91
+ const { monitorDir, monitorStatePath } = harnessHomePaths(homeDir);
92
+ await mkdir(monitorDir, { recursive: true });
93
+ const next = {
94
+ ...await readMonitorState(homeDir, { repair }), ...patch,
95
+ updatedAt: new Date().toISOString()
96
+ };
97
+ await writeAtomicJson(monitorStatePath, next);
98
+ return next;
99
+ }
100
+
101
+ async function raise(homeDir, input, notifyImpl) {
102
+ const result = await saveAlert({ ...input, source: SOURCE }, { homeDir });
103
+ if (!result.deduped) {
104
+ await notifyImpl({ title: "Kairo", body: `${result.alert.severity} · ${result.alert.title}` });
105
+ }
106
+ return result;
107
+ }
108
+
109
+ function autostartRecord(platform, a) {
110
+ return {
111
+ platform: platform.id, supported: a.supported,
112
+ configured: Boolean(a.configured), loaded: Boolean(a.loaded),
113
+ installed: Boolean(a.loaded), detail: a.detail ?? null
114
+ };
115
+ }
116
+
117
+ export async function runMonitorTick(homeDir, deps = {}) {
118
+ const {
119
+ notifyImpl = notifyNewAlert, detectDriftImpl = detectGlobalDrift,
120
+ listRunsImpl = listRunRecords, isRunAliveImpl = isRunAlive,
121
+ packageRoot = null, workspaceRoot = null
122
+ } = deps;
123
+ const raised = [];
124
+ await mkdir(harnessHomePaths(homeDir).monitorDir, { recursive: true });
125
+ try {
126
+ const paths = harnessHomePaths(homeDir);
127
+ const state = existsSync(paths.statePath)
128
+ ? JSON.parse(await readFile(paths.statePath, "utf8")) : null;
129
+ if (hasRepairableDrift(await detectDriftImpl({
130
+ homeDir, paths, state, packageRoot, workspaceRoot, context: { homeDir }
131
+ }))) {
132
+ raised.push(await raise(homeDir, {
133
+ kind: "monitor.drift", title: "Managed configuration drift",
134
+ summary: "Managed configs drifted. Run kairo sync.", severity: ALERT_SEVERITIES.HIGH
135
+ }, notifyImpl));
136
+ }
137
+ } catch {
138
+ raised.push(await raise(homeDir, {
139
+ kind: "monitor.drift", title: "Governance scan unavailable",
140
+ summary: "Monitor could not complete the drift scan.", severity: ALERT_SEVERITIES.MEDIUM
141
+ }, notifyImpl));
142
+ }
143
+
144
+ let runsOk = true; let dead = 0; let failed = 0;
145
+ try {
146
+ for (const run of await listRunsImpl(homeDir, { limit: 40 })) {
147
+ if (isActiveRunState(run.state) && !(await isRunAliveImpl(homeDir, run))) dead += 1;
148
+ if (run.state === RUN_STATES.FAILED) failed += 1;
149
+ }
150
+ } catch {
151
+ runsOk = false;
152
+ raised.push(await raise(homeDir, {
153
+ kind: "monitor.runs-unavailable", title: "Run monitoring unavailable",
154
+ summary: "Monitor could not inspect agent run health this tick.",
155
+ severity: ALERT_SEVERITIES.MEDIUM
156
+ }, notifyImpl));
157
+ }
158
+ if (runsOk && dead > 0) {
159
+ raised.push(await raise(homeDir, {
160
+ kind: "run.orphaned", title: "Orphaned agent run",
161
+ summary: `${dead} active run(s) have no live process.`, severity: ALERT_SEVERITIES.HIGH
162
+ }, notifyImpl));
163
+ }
164
+ if (runsOk && failed > 0) {
165
+ raised.push(await raise(homeDir, {
166
+ kind: "run.failed", title: "Agent run failed",
167
+ summary: `${failed} failed run(s) need attention.`, severity: ALERT_SEVERITIES.MEDIUM
168
+ }, notifyImpl));
169
+ }
170
+
171
+ const lastTick = {
172
+ raised: raised.length,
173
+ created: raised.filter((r) => !r.deduped).length,
174
+ deduped: raised.filter((r) => r.deduped).length,
175
+ complete: runsOk, runs: runsOk ? "ok" : "unavailable"
176
+ };
177
+ return {
178
+ state: await writeMonitorState(homeDir, {
179
+ lastTickAt: new Date().toISOString(), lastTick
180
+ }),
181
+ raised
182
+ };
183
+ }
184
+
185
+ export async function enableMonitor(homeDir, {
186
+ cliEntry, nodePath = process.execPath, platform = resolveMonitorPlatform(), intervalSec = INTERVAL
187
+ } = {}) {
188
+ const autostart = await installAutostart({ homeDir, platform, nodePath, cliEntry, intervalSec });
189
+ try {
190
+ return await writeMonitorState(homeDir, {
191
+ enabled: true, intervalSec, autostart: autostartRecord(platform, autostart)
192
+ }, { repair: true });
193
+ } catch (error) {
194
+ if (autostart.loaded) await removeAutostart({ platform });
195
+ throw error;
196
+ }
197
+ }
198
+
199
+ export async function disableMonitor(homeDir, { platform = resolveMonitorPlatform() } = {}) {
200
+ return writeMonitorState(homeDir, {
201
+ enabled: false, autostart: autostartRecord(platform, await removeAutostart({ platform }))
202
+ }, { repair: true });
203
+ }
204
+
205
+ export async function getMonitorStatus(homeDir, { platform = resolveMonitorPlatform() } = {}) {
206
+ try {
207
+ const state = await readMonitorState(homeDir);
208
+ let openAlerts = 0;
209
+ try {
210
+ openAlerts = (await listAlerts({ homeDir, state: ALERT_STATES.OPEN })).length;
211
+ } catch { openAlerts = null; }
212
+ return {
213
+ available: true, corrupt: false, enabled: state.enabled, intervalSec: state.intervalSec,
214
+ lastTickAt: state.lastTickAt, lastTick: state.lastTick, autostart: state.autostart,
215
+ platform: platform.id,
216
+ notify: { supported: platform.supportsNotify, backend: platform.id }, openAlerts
217
+ };
218
+ } catch (error) {
219
+ if (error?.code !== "corrupt_monitor_state") throw error;
220
+ return {
221
+ available: false, corrupt: true, enabled: null, intervalSec: null,
222
+ lastTickAt: null, lastTick: null, autostart: null, platform: platform.id,
223
+ notify: { supported: platform.supportsNotify, backend: platform.id },
224
+ openAlerts: null, error: error.message
225
+ };
226
+ }
227
+ }
228
+
229
+ export async function monitorDoctorCheck(homeDir) {
230
+ const s = await getMonitorStatus(homeDir);
231
+ if (s.corrupt || s.available === false) {
232
+ return {
233
+ name: "monitor", status: "stale", category: "monitor",
234
+ detail: "corrupt state — run kairo monitor disable to repair"
235
+ };
236
+ }
237
+ if (s.enabled && s.autostart?.supported && !s.autostart?.loaded) {
238
+ return {
239
+ name: "monitor", status: "stale", category: "monitor",
240
+ detail: "enabled but autostart not loaded"
241
+ };
242
+ }
243
+ return {
244
+ name: "monitor", status: "ok", category: "monitor",
245
+ detail: s.enabled
246
+ ? `enabled · last ${s.lastTickAt ?? "none"} · autostart ${s.autostart?.loaded ? "loaded" : "off"}`
247
+ : "disabled (opt-in · kairo monitor enable)"
248
+ };
249
+ }