cronies-cli 0.1.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/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/bin/cronies.d.ts +3 -0
- package/dist/bin/cronies.d.ts.map +1 -0
- package/dist/chunk-FVB2S2MS.js +4229 -0
- package/dist/cronies.js +331 -0
- package/dist/index.js +189 -0
- package/dist/src/adapters/index.d.ts +2 -0
- package/dist/src/adapters/index.d.ts.map +1 -0
- package/dist/src/adapters/manager.d.ts +6 -0
- package/dist/src/adapters/manager.d.ts.map +1 -0
- package/dist/src/cli/index.d.ts +3 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/config/index.d.ts +4 -0
- package/dist/src/config/index.d.ts.map +1 -0
- package/dist/src/config/loader.d.ts +13 -0
- package/dist/src/config/loader.d.ts.map +1 -0
- package/dist/src/config/schema.d.ts +160 -0
- package/dist/src/config/schema.d.ts.map +1 -0
- package/dist/src/config/writer.d.ts +17 -0
- package/dist/src/config/writer.d.ts.map +1 -0
- package/dist/src/daemon.d.ts +11 -0
- package/dist/src/daemon.d.ts.map +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/logger.d.ts +13 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/orchestrator/health.d.ts +20 -0
- package/dist/src/orchestrator/health.d.ts.map +1 -0
- package/dist/src/orchestrator/index.d.ts +2 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -0
- package/dist/src/orchestrator/runner.d.ts +102 -0
- package/dist/src/orchestrator/runner.d.ts.map +1 -0
- package/dist/src/orchestrator/summary.d.ts +19 -0
- package/dist/src/orchestrator/summary.d.ts.map +1 -0
- package/dist/src/pid.d.ts +20 -0
- package/dist/src/pid.d.ts.map +1 -0
- package/dist/src/policy/engine.d.ts +41 -0
- package/dist/src/policy/engine.d.ts.map +1 -0
- package/dist/src/policy/index.d.ts +2 -0
- package/dist/src/policy/index.d.ts.map +1 -0
- package/dist/src/server.d.ts +8 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/sessionHealthMonitor.d.ts +29 -0
- package/dist/src/sessionHealthMonitor.d.ts.map +1 -0
- package/dist/src/storage/database.d.ts +3 -0
- package/dist/src/storage/database.d.ts.map +1 -0
- package/dist/src/storage/index.d.ts +4 -0
- package/dist/src/storage/index.d.ts.map +1 -0
- package/dist/src/storage/migrations.d.ts +3 -0
- package/dist/src/storage/migrations.d.ts.map +1 -0
- package/dist/src/storage/store.d.ts +80 -0
- package/dist/src/storage/store.d.ts.map +1 -0
- package/dist/src/sync/connection.d.ts +37 -0
- package/dist/src/sync/connection.d.ts.map +1 -0
- package/dist/src/sync/index.d.ts +7 -0
- package/dist/src/sync/index.d.ts.map +1 -0
- package/dist/src/sync/queue.d.ts +22 -0
- package/dist/src/sync/queue.d.ts.map +1 -0
- package/dist/src/sync/reconciler.d.ts +42 -0
- package/dist/src/sync/reconciler.d.ts.map +1 -0
- package/dist/src/sync/rest-client.d.ts +62 -0
- package/dist/src/sync/rest-client.d.ts.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,4229 @@
|
|
|
1
|
+
// src/config/schema.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var DaemonConfigSchema = z.object({
|
|
4
|
+
daemon: z.object({
|
|
5
|
+
port: z.number().default(7890),
|
|
6
|
+
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
7
|
+
dataDir: z.string().default("~/.cronies")
|
|
8
|
+
}).default({}),
|
|
9
|
+
cloud: z.object({
|
|
10
|
+
url: z.string().default("https://cronies.dev"),
|
|
11
|
+
token: z.string().optional()
|
|
12
|
+
}).default({}),
|
|
13
|
+
agents: z.object({
|
|
14
|
+
"claude-code": z.object({
|
|
15
|
+
path: z.string().default("claude"),
|
|
16
|
+
defaultFlags: z.array(z.string()).default([])
|
|
17
|
+
}).default({}),
|
|
18
|
+
"codex-cli": z.object({
|
|
19
|
+
path: z.string().default("codex"),
|
|
20
|
+
defaultFlags: z.array(z.string()).default([])
|
|
21
|
+
}).default({}),
|
|
22
|
+
"gemini-cli": z.object({
|
|
23
|
+
path: z.string().default("gemini"),
|
|
24
|
+
defaultFlags: z.array(z.string()).default([])
|
|
25
|
+
}).default({}),
|
|
26
|
+
"cursor-agent": z.object({
|
|
27
|
+
path: z.string().default("agent"),
|
|
28
|
+
defaultFlags: z.array(z.string()).default([])
|
|
29
|
+
}).default({})
|
|
30
|
+
}).default({})
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// src/config/loader.ts
|
|
34
|
+
import { readFileSync, existsSync } from "fs";
|
|
35
|
+
import { resolve } from "path";
|
|
36
|
+
import { homedir } from "os";
|
|
37
|
+
import { parse as parseYaml } from "yaml";
|
|
38
|
+
function resolveTilde(filePath) {
|
|
39
|
+
if (filePath === "~") return homedir();
|
|
40
|
+
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
|
|
41
|
+
return resolve(homedir(), filePath.slice(2));
|
|
42
|
+
}
|
|
43
|
+
return filePath;
|
|
44
|
+
}
|
|
45
|
+
function loadConfig(configPath) {
|
|
46
|
+
const resolvedPath = resolveTilde(configPath ?? "~/.cronies/config.yaml");
|
|
47
|
+
if (!existsSync(resolvedPath)) {
|
|
48
|
+
return DaemonConfigSchema.parse({});
|
|
49
|
+
}
|
|
50
|
+
const raw = readFileSync(resolvedPath, "utf-8");
|
|
51
|
+
const parsed = parseYaml(raw);
|
|
52
|
+
const result = DaemonConfigSchema.safeParse(parsed ?? {});
|
|
53
|
+
if (!result.success) {
|
|
54
|
+
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
55
|
+
throw new Error(`Invalid config at ${resolvedPath}:
|
|
56
|
+
${issues}`);
|
|
57
|
+
}
|
|
58
|
+
return result.data;
|
|
59
|
+
}
|
|
60
|
+
function resolveDataDir(config) {
|
|
61
|
+
return resolveTilde(config.daemon.dataDir);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/config/writer.ts
|
|
65
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
|
|
66
|
+
import { resolve as resolve2, dirname } from "path";
|
|
67
|
+
import { homedir as homedir2 } from "os";
|
|
68
|
+
import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
69
|
+
function getDefaultConfigPath() {
|
|
70
|
+
return resolve2(homedir2(), ".cronies", "config.yaml");
|
|
71
|
+
}
|
|
72
|
+
function writeConfigToken(token, configPath) {
|
|
73
|
+
const resolvedPath = configPath ?? getDefaultConfigPath();
|
|
74
|
+
const dir = dirname(resolvedPath);
|
|
75
|
+
if (!existsSync2(dir)) {
|
|
76
|
+
mkdirSync(dir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
let config = {};
|
|
79
|
+
if (existsSync2(resolvedPath)) {
|
|
80
|
+
const raw = readFileSync2(resolvedPath, "utf-8");
|
|
81
|
+
const parsed = parseYaml2(raw);
|
|
82
|
+
if (parsed && typeof parsed === "object") {
|
|
83
|
+
config = parsed;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const existingCloud = config.cloud && typeof config.cloud === "object" ? config.cloud : {};
|
|
87
|
+
config.cloud = { ...existingCloud, token };
|
|
88
|
+
writeFileSync(resolvedPath, stringifyYaml(config), "utf-8");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/pid.ts
|
|
92
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
93
|
+
import { join } from "path";
|
|
94
|
+
function pidFilePath(dataDir) {
|
|
95
|
+
return join(dataDir, "daemon.pid");
|
|
96
|
+
}
|
|
97
|
+
function writePidFile(dataDir) {
|
|
98
|
+
mkdirSync2(dataDir, { recursive: true });
|
|
99
|
+
writeFileSync2(pidFilePath(dataDir), String(process.pid), "utf-8");
|
|
100
|
+
}
|
|
101
|
+
function readPidFile(dataDir) {
|
|
102
|
+
const filePath = pidFilePath(dataDir);
|
|
103
|
+
if (!existsSync3(filePath)) return null;
|
|
104
|
+
try {
|
|
105
|
+
const content = readFileSync3(filePath, "utf-8").trim();
|
|
106
|
+
const pid = Number.parseInt(content, 10);
|
|
107
|
+
return Number.isNaN(pid) ? null : pid;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function removePidFile(dataDir) {
|
|
113
|
+
const filePath = pidFilePath(dataDir);
|
|
114
|
+
try {
|
|
115
|
+
unlinkSync(filePath);
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function isDaemonRunning(dataDir) {
|
|
120
|
+
const pid = readPidFile(dataDir);
|
|
121
|
+
if (pid === null) return false;
|
|
122
|
+
try {
|
|
123
|
+
process.kill(pid, 0);
|
|
124
|
+
return true;
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/storage/database.ts
|
|
131
|
+
import Database from "better-sqlite3";
|
|
132
|
+
|
|
133
|
+
// src/storage/migrations.ts
|
|
134
|
+
function runMigrations(db) {
|
|
135
|
+
db.exec(`
|
|
136
|
+
CREATE TABLE IF NOT EXISTS daemon_jobs (
|
|
137
|
+
job_id TEXT PRIMARY KEY,
|
|
138
|
+
outcome_id TEXT NOT NULL,
|
|
139
|
+
definition TEXT NOT NULL,
|
|
140
|
+
machine_state TEXT NOT NULL,
|
|
141
|
+
machine_context TEXT NOT NULL,
|
|
142
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
143
|
+
assigned_at INTEGER NOT NULL,
|
|
144
|
+
updated_at INTEGER NOT NULL,
|
|
145
|
+
synced_at INTEGER
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
CREATE TABLE IF NOT EXISTS run_events (
|
|
149
|
+
id TEXT PRIMARY KEY,
|
|
150
|
+
run_id TEXT NOT NULL,
|
|
151
|
+
job_id TEXT NOT NULL,
|
|
152
|
+
source TEXT NOT NULL,
|
|
153
|
+
source_id TEXT NOT NULL,
|
|
154
|
+
type TEXT NOT NULL,
|
|
155
|
+
payload TEXT NOT NULL,
|
|
156
|
+
timestamp INTEGER NOT NULL,
|
|
157
|
+
sequence INTEGER NOT NULL,
|
|
158
|
+
synced INTEGER DEFAULT 0,
|
|
159
|
+
UNIQUE(source, source_id, sequence)
|
|
160
|
+
);
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_run_events_job ON run_events(job_id, timestamp);
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_run_events_run ON run_events(run_id, timestamp);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_run_events_unsynced ON run_events(synced, sequence);
|
|
164
|
+
|
|
165
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
166
|
+
id TEXT PRIMARY KEY,
|
|
167
|
+
run_id TEXT NOT NULL,
|
|
168
|
+
job_id TEXT NOT NULL,
|
|
169
|
+
machine_state TEXT NOT NULL,
|
|
170
|
+
machine_context TEXT NOT NULL,
|
|
171
|
+
agent_state TEXT,
|
|
172
|
+
created_at INTEGER NOT NULL,
|
|
173
|
+
synced INTEGER DEFAULT 0
|
|
174
|
+
);
|
|
175
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_job ON checkpoints(job_id, created_at);
|
|
176
|
+
|
|
177
|
+
CREATE TABLE IF NOT EXISTS policy_cache (
|
|
178
|
+
id TEXT PRIMARY KEY DEFAULT 'default',
|
|
179
|
+
policy TEXT NOT NULL,
|
|
180
|
+
version INTEGER NOT NULL,
|
|
181
|
+
received_at INTEGER NOT NULL
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
CREATE TABLE IF NOT EXISTS outcome_cache (
|
|
185
|
+
outcome_id TEXT PRIMARY KEY,
|
|
186
|
+
definition TEXT NOT NULL,
|
|
187
|
+
version INTEGER NOT NULL,
|
|
188
|
+
received_at INTEGER NOT NULL
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
CREATE TABLE IF NOT EXISTS sync_outbox (
|
|
192
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
193
|
+
message_type TEXT NOT NULL,
|
|
194
|
+
payload TEXT NOT NULL,
|
|
195
|
+
created_at INTEGER NOT NULL,
|
|
196
|
+
attempts INTEGER DEFAULT 0,
|
|
197
|
+
last_attempt INTEGER
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
CREATE TABLE IF NOT EXISTS daemon_meta (
|
|
201
|
+
key TEXT PRIMARY KEY,
|
|
202
|
+
value TEXT NOT NULL
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
CREATE TABLE IF NOT EXISTS spawned_pids (
|
|
206
|
+
pid INTEGER PRIMARY KEY,
|
|
207
|
+
job_id TEXT NOT NULL,
|
|
208
|
+
agent TEXT NOT NULL,
|
|
209
|
+
spawned_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
210
|
+
);
|
|
211
|
+
`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/storage/database.ts
|
|
215
|
+
function createDatabase(dbPath) {
|
|
216
|
+
const db = new Database(dbPath);
|
|
217
|
+
db.pragma("journal_mode = WAL");
|
|
218
|
+
db.pragma("foreign_keys = ON");
|
|
219
|
+
runMigrations(db);
|
|
220
|
+
return db;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/storage/store.ts
|
|
224
|
+
import { ulid } from "ulid";
|
|
225
|
+
var DaemonStore = class {
|
|
226
|
+
db;
|
|
227
|
+
sequence;
|
|
228
|
+
constructor(db) {
|
|
229
|
+
this.db = db;
|
|
230
|
+
this.sequence = this.getLastSequence();
|
|
231
|
+
}
|
|
232
|
+
// -----------------------------------------------------------------------
|
|
233
|
+
// Jobs
|
|
234
|
+
// -----------------------------------------------------------------------
|
|
235
|
+
saveJob(jobId, outcomeId, definition, machineState, machineContext) {
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
const stmt = this.db.prepare(
|
|
238
|
+
`INSERT OR REPLACE INTO daemon_jobs
|
|
239
|
+
(job_id, outcome_id, definition, machine_state, machine_context, status, assigned_at, updated_at)
|
|
240
|
+
VALUES (?, ?, ?, ?, ?, 'active', ?, ?)`
|
|
241
|
+
);
|
|
242
|
+
stmt.run(jobId, outcomeId, JSON.stringify(definition), machineState, machineContext, now, now);
|
|
243
|
+
}
|
|
244
|
+
getJob(jobId) {
|
|
245
|
+
const stmt = this.db.prepare(
|
|
246
|
+
`SELECT * FROM daemon_jobs WHERE job_id = ?`
|
|
247
|
+
);
|
|
248
|
+
const row = stmt.get(jobId);
|
|
249
|
+
return row ? this.mapJobRow(row) : null;
|
|
250
|
+
}
|
|
251
|
+
getActiveJobs() {
|
|
252
|
+
const stmt = this.db.prepare(
|
|
253
|
+
`SELECT * FROM daemon_jobs WHERE status = 'active'`
|
|
254
|
+
);
|
|
255
|
+
return stmt.all().map((row) => this.mapJobRow(row));
|
|
256
|
+
}
|
|
257
|
+
updateJobState(jobId, machineState, machineContext, status) {
|
|
258
|
+
const stmt = this.db.prepare(
|
|
259
|
+
`UPDATE daemon_jobs
|
|
260
|
+
SET machine_state = ?, machine_context = ?, status = ?, updated_at = ?
|
|
261
|
+
WHERE job_id = ?`
|
|
262
|
+
);
|
|
263
|
+
stmt.run(machineState, machineContext, status, Date.now(), jobId);
|
|
264
|
+
}
|
|
265
|
+
deleteJob(jobId) {
|
|
266
|
+
const stmt = this.db.prepare(`DELETE FROM daemon_jobs WHERE job_id = ?`);
|
|
267
|
+
stmt.run(jobId);
|
|
268
|
+
}
|
|
269
|
+
// -----------------------------------------------------------------------
|
|
270
|
+
// Run Events
|
|
271
|
+
// -----------------------------------------------------------------------
|
|
272
|
+
appendEvent(event) {
|
|
273
|
+
this.sequence += 1;
|
|
274
|
+
const seq = this.sequence;
|
|
275
|
+
const stmt = this.db.prepare(
|
|
276
|
+
`INSERT INTO run_events
|
|
277
|
+
(id, run_id, job_id, source, source_id, type, payload, timestamp, sequence, synced)
|
|
278
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)`
|
|
279
|
+
);
|
|
280
|
+
const id = event.id || ulid();
|
|
281
|
+
stmt.run(
|
|
282
|
+
id,
|
|
283
|
+
event.runId,
|
|
284
|
+
event.jobId,
|
|
285
|
+
event.source,
|
|
286
|
+
event.sourceId,
|
|
287
|
+
event.type,
|
|
288
|
+
JSON.stringify(event.payload),
|
|
289
|
+
event.timestamp,
|
|
290
|
+
seq
|
|
291
|
+
);
|
|
292
|
+
return {
|
|
293
|
+
id,
|
|
294
|
+
runId: event.runId,
|
|
295
|
+
jobId: event.jobId,
|
|
296
|
+
source: event.source,
|
|
297
|
+
sourceId: event.sourceId,
|
|
298
|
+
type: event.type,
|
|
299
|
+
payload: event.payload,
|
|
300
|
+
timestamp: event.timestamp,
|
|
301
|
+
sequence: seq
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
getEvents(runId, limit = 100) {
|
|
305
|
+
const stmt = this.db.prepare(
|
|
306
|
+
`SELECT * FROM run_events WHERE run_id = ? ORDER BY timestamp ASC LIMIT ?`
|
|
307
|
+
);
|
|
308
|
+
return stmt.all(runId, limit).map((row) => this.mapEventRow(row));
|
|
309
|
+
}
|
|
310
|
+
getEventsByJob(jobId, limit = 100) {
|
|
311
|
+
const stmt = this.db.prepare(
|
|
312
|
+
`SELECT * FROM run_events WHERE job_id = ? ORDER BY timestamp ASC LIMIT ?`
|
|
313
|
+
);
|
|
314
|
+
return stmt.all(jobId, limit).map((row) => this.mapEventRow(row));
|
|
315
|
+
}
|
|
316
|
+
getUnsyncedEvents(limit = 50) {
|
|
317
|
+
const stmt = this.db.prepare(
|
|
318
|
+
`SELECT * FROM run_events WHERE synced = 0 ORDER BY sequence ASC LIMIT ?`
|
|
319
|
+
);
|
|
320
|
+
return stmt.all(limit).map((row) => ({
|
|
321
|
+
...this.mapEventRow(row),
|
|
322
|
+
jobId: row.job_id
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
markEventsSynced(upToSequence) {
|
|
326
|
+
const stmt = this.db.prepare(
|
|
327
|
+
`UPDATE run_events SET synced = 1 WHERE sequence <= ? AND synced = 0`
|
|
328
|
+
);
|
|
329
|
+
stmt.run(upToSequence);
|
|
330
|
+
}
|
|
331
|
+
getLastSequence() {
|
|
332
|
+
const stmt = this.db.prepare(
|
|
333
|
+
`SELECT MAX(sequence) AS max_seq FROM run_events WHERE source = 'daemon'`
|
|
334
|
+
);
|
|
335
|
+
const row = stmt.get();
|
|
336
|
+
return row?.max_seq ?? 0;
|
|
337
|
+
}
|
|
338
|
+
// -----------------------------------------------------------------------
|
|
339
|
+
// Checkpoints
|
|
340
|
+
// -----------------------------------------------------------------------
|
|
341
|
+
saveCheckpoint(checkpoint) {
|
|
342
|
+
const stmt = this.db.prepare(
|
|
343
|
+
`INSERT OR REPLACE INTO checkpoints
|
|
344
|
+
(id, run_id, job_id, machine_state, machine_context, agent_state, created_at, synced)
|
|
345
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
346
|
+
);
|
|
347
|
+
stmt.run(
|
|
348
|
+
checkpoint.id,
|
|
349
|
+
checkpoint.runId,
|
|
350
|
+
checkpoint.jobId,
|
|
351
|
+
checkpoint.machineState,
|
|
352
|
+
checkpoint.machineContext,
|
|
353
|
+
checkpoint.agentState ?? null,
|
|
354
|
+
checkpoint.createdAt instanceof Date ? checkpoint.createdAt.getTime() : checkpoint.createdAt,
|
|
355
|
+
checkpoint.synced ?? 0
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
getLatestCheckpoint(jobId) {
|
|
359
|
+
const stmt = this.db.prepare(
|
|
360
|
+
`SELECT * FROM checkpoints WHERE job_id = ? ORDER BY created_at DESC LIMIT 1`
|
|
361
|
+
);
|
|
362
|
+
const row = stmt.get(jobId);
|
|
363
|
+
return row ? this.mapCheckpointRow(row) : null;
|
|
364
|
+
}
|
|
365
|
+
getCheckpoint(id) {
|
|
366
|
+
const stmt = this.db.prepare(
|
|
367
|
+
`SELECT * FROM checkpoints WHERE id = ?`
|
|
368
|
+
);
|
|
369
|
+
const row = stmt.get(id);
|
|
370
|
+
return row ? this.mapCheckpointRow(row) : null;
|
|
371
|
+
}
|
|
372
|
+
markCheckpointSynced(id) {
|
|
373
|
+
const stmt = this.db.prepare(
|
|
374
|
+
`UPDATE checkpoints SET synced = 1 WHERE id = ?`
|
|
375
|
+
);
|
|
376
|
+
stmt.run(id);
|
|
377
|
+
}
|
|
378
|
+
// -----------------------------------------------------------------------
|
|
379
|
+
// Policy Cache
|
|
380
|
+
// -----------------------------------------------------------------------
|
|
381
|
+
cachePolicy(policy) {
|
|
382
|
+
const stmt = this.db.prepare(
|
|
383
|
+
`INSERT OR REPLACE INTO policy_cache (id, policy, version, received_at)
|
|
384
|
+
VALUES (?, ?, ?, ?)`
|
|
385
|
+
);
|
|
386
|
+
stmt.run("default", JSON.stringify(policy), policy.version, Date.now());
|
|
387
|
+
}
|
|
388
|
+
getCachedPolicy() {
|
|
389
|
+
const stmt = this.db.prepare(
|
|
390
|
+
`SELECT * FROM policy_cache WHERE id = 'default'`
|
|
391
|
+
);
|
|
392
|
+
const row = stmt.get();
|
|
393
|
+
return row ? JSON.parse(row.policy) : null;
|
|
394
|
+
}
|
|
395
|
+
// -----------------------------------------------------------------------
|
|
396
|
+
// Outcome Cache
|
|
397
|
+
// -----------------------------------------------------------------------
|
|
398
|
+
cacheOutcome(outcomeId, definition, version) {
|
|
399
|
+
const stmt = this.db.prepare(
|
|
400
|
+
`INSERT OR REPLACE INTO outcome_cache (outcome_id, definition, version, received_at)
|
|
401
|
+
VALUES (?, ?, ?, ?)`
|
|
402
|
+
);
|
|
403
|
+
stmt.run(outcomeId, definition, version, Date.now());
|
|
404
|
+
}
|
|
405
|
+
getCachedOutcome(outcomeId) {
|
|
406
|
+
const stmt = this.db.prepare(
|
|
407
|
+
`SELECT * FROM outcome_cache WHERE outcome_id = ?`
|
|
408
|
+
);
|
|
409
|
+
const row = stmt.get(outcomeId);
|
|
410
|
+
return row ? { definition: row.definition, version: row.version } : null;
|
|
411
|
+
}
|
|
412
|
+
// -----------------------------------------------------------------------
|
|
413
|
+
// Sync Outbox
|
|
414
|
+
// -----------------------------------------------------------------------
|
|
415
|
+
enqueueMessage(messageType, payload) {
|
|
416
|
+
const stmt = this.db.prepare(
|
|
417
|
+
`INSERT INTO sync_outbox (message_type, payload, created_at) VALUES (?, ?, ?)`
|
|
418
|
+
);
|
|
419
|
+
stmt.run(messageType, payload, Date.now());
|
|
420
|
+
}
|
|
421
|
+
dequeueMessages(limit = 10) {
|
|
422
|
+
const stmt = this.db.prepare(
|
|
423
|
+
`SELECT * FROM sync_outbox ORDER BY id ASC LIMIT ?`
|
|
424
|
+
);
|
|
425
|
+
return stmt.all(limit).map((row) => ({
|
|
426
|
+
id: row.id,
|
|
427
|
+
messageType: row.message_type,
|
|
428
|
+
payload: row.payload,
|
|
429
|
+
createdAt: row.created_at,
|
|
430
|
+
attempts: row.attempts
|
|
431
|
+
}));
|
|
432
|
+
}
|
|
433
|
+
markMessageSent(id) {
|
|
434
|
+
const stmt = this.db.prepare(`DELETE FROM sync_outbox WHERE id = ?`);
|
|
435
|
+
stmt.run(id);
|
|
436
|
+
}
|
|
437
|
+
retryMessage(id) {
|
|
438
|
+
const stmt = this.db.prepare(
|
|
439
|
+
`UPDATE sync_outbox SET attempts = attempts + 1, last_attempt = ? WHERE id = ?`
|
|
440
|
+
);
|
|
441
|
+
stmt.run(Date.now(), id);
|
|
442
|
+
}
|
|
443
|
+
// -----------------------------------------------------------------------
|
|
444
|
+
// Daemon Meta
|
|
445
|
+
// -----------------------------------------------------------------------
|
|
446
|
+
setMeta(key, value) {
|
|
447
|
+
const stmt = this.db.prepare(
|
|
448
|
+
`INSERT OR REPLACE INTO daemon_meta (key, value) VALUES (?, ?)`
|
|
449
|
+
);
|
|
450
|
+
stmt.run(key, value);
|
|
451
|
+
}
|
|
452
|
+
getMeta(key) {
|
|
453
|
+
const stmt = this.db.prepare(
|
|
454
|
+
`SELECT * FROM daemon_meta WHERE key = ?`
|
|
455
|
+
);
|
|
456
|
+
const row = stmt.get(key);
|
|
457
|
+
return row ? row.value : null;
|
|
458
|
+
}
|
|
459
|
+
// -----------------------------------------------------------------------
|
|
460
|
+
// Cleanup
|
|
461
|
+
// -----------------------------------------------------------------------
|
|
462
|
+
close() {
|
|
463
|
+
this.db.close();
|
|
464
|
+
}
|
|
465
|
+
// -----------------------------------------------------------------------
|
|
466
|
+
// PID tracking — for crash recovery and orphan cleanup
|
|
467
|
+
// -----------------------------------------------------------------------
|
|
468
|
+
trackPid(pid, jobId, agent) {
|
|
469
|
+
this.db.prepare(
|
|
470
|
+
"INSERT OR REPLACE INTO spawned_pids (pid, job_id, agent) VALUES (?, ?, ?)"
|
|
471
|
+
).run(pid, jobId, agent);
|
|
472
|
+
}
|
|
473
|
+
removePid(pid) {
|
|
474
|
+
this.db.prepare("DELETE FROM spawned_pids WHERE pid = ?").run(pid);
|
|
475
|
+
}
|
|
476
|
+
getTrackedPids() {
|
|
477
|
+
return this.db.prepare("SELECT pid, job_id as jobId, agent FROM spawned_pids").all();
|
|
478
|
+
}
|
|
479
|
+
clearAllPids() {
|
|
480
|
+
this.db.prepare("DELETE FROM spawned_pids").run();
|
|
481
|
+
}
|
|
482
|
+
// -----------------------------------------------------------------------
|
|
483
|
+
// Private helpers
|
|
484
|
+
// -----------------------------------------------------------------------
|
|
485
|
+
mapJobRow(row) {
|
|
486
|
+
return {
|
|
487
|
+
jobId: row.job_id,
|
|
488
|
+
outcomeId: row.outcome_id,
|
|
489
|
+
definition: JSON.parse(row.definition),
|
|
490
|
+
machineState: row.machine_state,
|
|
491
|
+
machineContext: row.machine_context,
|
|
492
|
+
status: row.status,
|
|
493
|
+
assignedAt: row.assigned_at,
|
|
494
|
+
updatedAt: row.updated_at,
|
|
495
|
+
syncedAt: row.synced_at
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
mapEventRow(row) {
|
|
499
|
+
return {
|
|
500
|
+
id: row.id,
|
|
501
|
+
runId: row.run_id,
|
|
502
|
+
source: row.source,
|
|
503
|
+
sourceId: row.source_id,
|
|
504
|
+
type: row.type,
|
|
505
|
+
payload: JSON.parse(row.payload),
|
|
506
|
+
timestamp: row.timestamp,
|
|
507
|
+
sequence: row.sequence
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
mapCheckpointRow(row) {
|
|
511
|
+
return {
|
|
512
|
+
id: row.id,
|
|
513
|
+
runId: row.run_id,
|
|
514
|
+
jobId: row.job_id,
|
|
515
|
+
machineState: row.machine_state,
|
|
516
|
+
machineContext: row.machine_context,
|
|
517
|
+
agentState: row.agent_state ?? void 0,
|
|
518
|
+
createdAt: new Date(row.created_at)
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// ../adapters/dist/base.js
|
|
524
|
+
import { spawn as cpSpawn } from "child_process";
|
|
525
|
+
var childProcessMap = /* @__PURE__ */ new WeakMap();
|
|
526
|
+
var AUTH_ERROR_PATTERNS = [
|
|
527
|
+
/unauthorized/,
|
|
528
|
+
/unauthenticated/,
|
|
529
|
+
/invalid.*api.*key/,
|
|
530
|
+
/authentication.*required/,
|
|
531
|
+
/not.*logged.*in/,
|
|
532
|
+
/login.*required/,
|
|
533
|
+
/api.*key.*not.*found/,
|
|
534
|
+
/no.*api.*key/,
|
|
535
|
+
/credentials.*not.*found/,
|
|
536
|
+
/please.*log.*in/,
|
|
537
|
+
/please.*authenticate/,
|
|
538
|
+
/token.*expired/,
|
|
539
|
+
/invalid.*token/,
|
|
540
|
+
/access.*denied/,
|
|
541
|
+
/permission.*denied/,
|
|
542
|
+
/sign.*in.*required/
|
|
543
|
+
];
|
|
544
|
+
var AUTH_CHECK_PROCESS_TIMEOUT = 3e4;
|
|
545
|
+
var AUTH_CHECK_RESPONSE_TIMEOUT = 2e4;
|
|
546
|
+
var BaseAdapter = class {
|
|
547
|
+
/**
|
|
548
|
+
* Check if the agent is authenticated by running a minimal test prompt.
|
|
549
|
+
* Pass `skipAvailabilityCheck: true` if the caller already verified `isAvailable()`.
|
|
550
|
+
*/
|
|
551
|
+
async checkAuth(opts) {
|
|
552
|
+
if (!opts?.skipAvailabilityCheck) {
|
|
553
|
+
const available = await this.isAvailable();
|
|
554
|
+
if (!available) {
|
|
555
|
+
return { status: "unavailable" };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return this.runAuthCheck();
|
|
559
|
+
}
|
|
560
|
+
async runAuthCheck() {
|
|
561
|
+
try {
|
|
562
|
+
const { cmd, args, shell } = this.buildCommand({
|
|
563
|
+
prompt: "respond with the word ok and nothing else",
|
|
564
|
+
workingDirectory: process.cwd()
|
|
565
|
+
});
|
|
566
|
+
const { spawn: cpSpawnLocal } = await import("child_process");
|
|
567
|
+
const child = cpSpawnLocal(cmd, args, {
|
|
568
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
569
|
+
shell: shell ?? process.platform === "win32",
|
|
570
|
+
timeout: AUTH_CHECK_PROCESS_TIMEOUT
|
|
571
|
+
});
|
|
572
|
+
child.stdin.write("respond with the word ok and nothing else");
|
|
573
|
+
child.stdin.end();
|
|
574
|
+
return new Promise((resolve4) => {
|
|
575
|
+
let stdout = "";
|
|
576
|
+
let stderr = "";
|
|
577
|
+
child.stdout.on("data", (chunk) => {
|
|
578
|
+
stdout += chunk.toString();
|
|
579
|
+
});
|
|
580
|
+
child.stderr.on("data", (chunk) => {
|
|
581
|
+
stderr += chunk.toString();
|
|
582
|
+
});
|
|
583
|
+
const timeout = setTimeout(() => {
|
|
584
|
+
try {
|
|
585
|
+
child.kill("SIGKILL");
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
resolve4({ status: "authenticated" });
|
|
589
|
+
}, AUTH_CHECK_RESPONSE_TIMEOUT);
|
|
590
|
+
child.on("exit", (code) => {
|
|
591
|
+
clearTimeout(timeout);
|
|
592
|
+
const combined = `${stdout}
|
|
593
|
+
${stderr}`.toLowerCase();
|
|
594
|
+
for (const pattern of AUTH_ERROR_PATTERNS) {
|
|
595
|
+
if (pattern.test(combined)) {
|
|
596
|
+
return resolve4({
|
|
597
|
+
status: "needs_auth",
|
|
598
|
+
error: stderr.trim() || stdout.trim(),
|
|
599
|
+
authCommand: this.getAuthCommand()
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (code === 0 || stdout.length > 0) {
|
|
604
|
+
resolve4({ status: "authenticated" });
|
|
605
|
+
} else {
|
|
606
|
+
resolve4({
|
|
607
|
+
status: "error",
|
|
608
|
+
error: stderr.trim() || `Exit code ${code}`
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
child.on("error", (err) => {
|
|
613
|
+
clearTimeout(timeout);
|
|
614
|
+
resolve4({ status: "error", error: err.message });
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
} catch (err) {
|
|
618
|
+
return {
|
|
619
|
+
status: "error",
|
|
620
|
+
error: err instanceof Error ? err.message : String(err)
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Return the CLI command the user should run to authenticate this agent.
|
|
626
|
+
* Subclasses should override with agent-specific instructions.
|
|
627
|
+
*/
|
|
628
|
+
getAuthCommand() {
|
|
629
|
+
return void 0;
|
|
630
|
+
}
|
|
631
|
+
async spawn(options) {
|
|
632
|
+
const { cmd, args, shell } = this.buildCommand(options);
|
|
633
|
+
const child = cpSpawn(cmd, args, {
|
|
634
|
+
cwd: options.workingDirectory,
|
|
635
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
636
|
+
shell: shell ?? process.platform === "win32",
|
|
637
|
+
// Default: shell on Windows for .cmd executables
|
|
638
|
+
env: {
|
|
639
|
+
...process.env,
|
|
640
|
+
...options.env ?? {}
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
if (child.pid === void 0) {
|
|
644
|
+
return new Promise((_resolve, reject) => {
|
|
645
|
+
child.once("error", (err) => {
|
|
646
|
+
reject(new Error(`Failed to spawn ${cmd}: ${err.message}`));
|
|
647
|
+
});
|
|
648
|
+
setTimeout(() => {
|
|
649
|
+
reject(new Error(`Failed to spawn ${cmd}: no PID assigned`));
|
|
650
|
+
}, 2e3);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
if (options.prompt) {
|
|
654
|
+
child.stdin.write(options.prompt);
|
|
655
|
+
child.stdin.end();
|
|
656
|
+
}
|
|
657
|
+
const agentProcess = {
|
|
658
|
+
pid: child.pid,
|
|
659
|
+
stdin: child.stdin,
|
|
660
|
+
stdout: child.stdout,
|
|
661
|
+
stderr: child.stderr,
|
|
662
|
+
workingDirectory: options.workingDirectory,
|
|
663
|
+
startedAt: Date.now()
|
|
664
|
+
};
|
|
665
|
+
childProcessMap.set(agentProcess, child);
|
|
666
|
+
return agentProcess;
|
|
667
|
+
}
|
|
668
|
+
async terminate(agentProcess) {
|
|
669
|
+
const SIGTERM_WAIT_MS = 5e3;
|
|
670
|
+
try {
|
|
671
|
+
process.kill(agentProcess.pid, "SIGTERM");
|
|
672
|
+
} catch {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
return new Promise((resolve4) => {
|
|
676
|
+
const checkInterval = setInterval(() => {
|
|
677
|
+
try {
|
|
678
|
+
process.kill(agentProcess.pid, 0);
|
|
679
|
+
} catch {
|
|
680
|
+
clearInterval(checkInterval);
|
|
681
|
+
clearTimeout(killTimeout);
|
|
682
|
+
resolve4();
|
|
683
|
+
}
|
|
684
|
+
}, 200);
|
|
685
|
+
const killTimeout = setTimeout(() => {
|
|
686
|
+
clearInterval(checkInterval);
|
|
687
|
+
try {
|
|
688
|
+
process.kill(agentProcess.pid, "SIGKILL");
|
|
689
|
+
} catch {
|
|
690
|
+
}
|
|
691
|
+
resolve4();
|
|
692
|
+
}, SIGTERM_WAIT_MS);
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
async sendInstruction(agentProcess, instruction) {
|
|
696
|
+
return new Promise((resolve4, reject) => {
|
|
697
|
+
const ok = agentProcess.stdin.write(instruction + "\n", "utf-8", (err) => {
|
|
698
|
+
if (err)
|
|
699
|
+
reject(err);
|
|
700
|
+
else
|
|
701
|
+
resolve4();
|
|
702
|
+
});
|
|
703
|
+
if (!ok) {
|
|
704
|
+
agentProcess.stdin.once("drain", () => resolve4());
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
onOutput(agentProcess, handler) {
|
|
709
|
+
const makeHandler = (stream) => (chunk) => {
|
|
710
|
+
const lines = chunk.toString("utf-8").split("\n");
|
|
711
|
+
for (const line of lines) {
|
|
712
|
+
if (line.length === 0)
|
|
713
|
+
continue;
|
|
714
|
+
const output = this.parseOutput(line, stream);
|
|
715
|
+
handler(output);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
agentProcess.stdout.on("data", makeHandler("stdout"));
|
|
719
|
+
agentProcess.stderr.on("data", makeHandler("stderr"));
|
|
720
|
+
}
|
|
721
|
+
onExit(agentProcess, handler) {
|
|
722
|
+
const child = childProcessMap.get(agentProcess);
|
|
723
|
+
if (child) {
|
|
724
|
+
child.on("exit", (code, signal) => {
|
|
725
|
+
handler(code, signal);
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// ../adapters/dist/classifier.js
|
|
732
|
+
var ERROR_KEYWORDS = /\b(error|fail(ed|ure)?|exception|traceback|panic|crash|abort(ed)?|segfault|ENOENT|EACCES|EPERM|ECONNREFUSED|timeout)\b/i;
|
|
733
|
+
var SUCCESS_KEYWORDS = /\b(success(ful(ly)?)?|complete(d)?|done|finished|pass(ed)?|ok|applied|created|updated|merged|deployed)\b/i;
|
|
734
|
+
var RISK_KEYWORDS = /\b(force\s+push|--force|rm\s+-rf|DROP\s+TABLE|DELETE\s+FROM|TRUNCATE|chmod\s+777|sudo\s+rm|git\s+reset\s+--hard|git\s+clean\s+-fd)\b/i;
|
|
735
|
+
function patternMatch(text, rules) {
|
|
736
|
+
let bestMatch = null;
|
|
737
|
+
for (const rule of rules) {
|
|
738
|
+
if (rule.pattern.test(text)) {
|
|
739
|
+
if (bestMatch === null || rule.confidence > bestMatch.confidence) {
|
|
740
|
+
bestMatch = rule;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (bestMatch !== null && bestMatch.confidence >= 0.5) {
|
|
745
|
+
return {
|
|
746
|
+
category: bestMatch.category,
|
|
747
|
+
confidence: bestMatch.confidence,
|
|
748
|
+
riskScore: bestMatch.riskScore,
|
|
749
|
+
detectedAction: bestMatch.name,
|
|
750
|
+
summary: bestMatch.description,
|
|
751
|
+
rawOutput: text
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
function heuristicScore(text, exitCode) {
|
|
757
|
+
let category = "ambiguous";
|
|
758
|
+
let confidence = 0.3;
|
|
759
|
+
let riskScore = 0;
|
|
760
|
+
const signals = [];
|
|
761
|
+
if (exitCode !== null) {
|
|
762
|
+
if (exitCode === 0) {
|
|
763
|
+
confidence += 0.2;
|
|
764
|
+
category = "success";
|
|
765
|
+
signals.push("exit_code_0");
|
|
766
|
+
} else {
|
|
767
|
+
confidence += 0.15;
|
|
768
|
+
category = "transient_error";
|
|
769
|
+
signals.push(`exit_code_${exitCode}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (ERROR_KEYWORDS.test(text)) {
|
|
773
|
+
if (category !== "transient_error") {
|
|
774
|
+
category = "transient_error";
|
|
775
|
+
}
|
|
776
|
+
confidence = Math.min(confidence + 0.15, 1);
|
|
777
|
+
signals.push("error_keywords");
|
|
778
|
+
}
|
|
779
|
+
if (SUCCESS_KEYWORDS.test(text)) {
|
|
780
|
+
if (category === "ambiguous") {
|
|
781
|
+
category = "success";
|
|
782
|
+
}
|
|
783
|
+
confidence = Math.min(confidence + 0.1, 1);
|
|
784
|
+
signals.push("success_keywords");
|
|
785
|
+
}
|
|
786
|
+
if (RISK_KEYWORDS.test(text)) {
|
|
787
|
+
category = "risky_action";
|
|
788
|
+
riskScore = 80;
|
|
789
|
+
confidence = Math.min(confidence + 0.2, 1);
|
|
790
|
+
signals.push("risk_keywords");
|
|
791
|
+
}
|
|
792
|
+
if (text.length < 10 && exitCode === null) {
|
|
793
|
+
if (category === "ambiguous") {
|
|
794
|
+
category = "idle";
|
|
795
|
+
}
|
|
796
|
+
confidence = Math.max(confidence - 0.1, 0);
|
|
797
|
+
signals.push("very_short_output");
|
|
798
|
+
}
|
|
799
|
+
if (confidence < 0.5) {
|
|
800
|
+
category = "ambiguous";
|
|
801
|
+
confidence = 0.3;
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
category,
|
|
805
|
+
confidence,
|
|
806
|
+
riskScore,
|
|
807
|
+
detectedAction: signals.join(", ") || void 0,
|
|
808
|
+
summary: `Heuristic classification: ${category} (signals: ${signals.join(", ") || "none"})`,
|
|
809
|
+
rawOutput: text
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function classifyOutput(output, exitCode, rules) {
|
|
813
|
+
const patternResult = patternMatch(output.data, rules);
|
|
814
|
+
if (patternResult !== null) {
|
|
815
|
+
return patternResult;
|
|
816
|
+
}
|
|
817
|
+
return heuristicScore(output.data, exitCode);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// ../adapters/dist/manager.js
|
|
821
|
+
var AdapterManager = class {
|
|
822
|
+
adapters = /* @__PURE__ */ new Map();
|
|
823
|
+
/** Register an adapter. Overwrites any previous adapter with the same name. */
|
|
824
|
+
register(adapter) {
|
|
825
|
+
this.adapters.set(adapter.name, adapter);
|
|
826
|
+
}
|
|
827
|
+
/** Get an adapter by name, or undefined if not registered. */
|
|
828
|
+
get(name) {
|
|
829
|
+
return this.adapters.get(name);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Return the names of all registered adapters whose CLIs are actually
|
|
833
|
+
* installed and available on this machine.
|
|
834
|
+
*/
|
|
835
|
+
async getAvailable() {
|
|
836
|
+
const results = [];
|
|
837
|
+
for (const [name, adapter] of this.adapters) {
|
|
838
|
+
try {
|
|
839
|
+
const available = await adapter.isAvailable();
|
|
840
|
+
if (available) {
|
|
841
|
+
results.push(name);
|
|
842
|
+
}
|
|
843
|
+
} catch {
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return results;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Resolve the preferred adapter, falling back to an alternative if the
|
|
850
|
+
* preferred one is not available.
|
|
851
|
+
*
|
|
852
|
+
* @throws Error if neither preferred nor fallback adapter is available.
|
|
853
|
+
*/
|
|
854
|
+
async getPreferred(preferred, fallback) {
|
|
855
|
+
const preferredAdapter = this.adapters.get(preferred);
|
|
856
|
+
if (preferredAdapter) {
|
|
857
|
+
try {
|
|
858
|
+
const available = await preferredAdapter.isAvailable();
|
|
859
|
+
if (available)
|
|
860
|
+
return preferredAdapter;
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (fallback !== void 0) {
|
|
865
|
+
const fallbackAdapter = this.adapters.get(fallback);
|
|
866
|
+
if (fallbackAdapter) {
|
|
867
|
+
try {
|
|
868
|
+
const available = await fallbackAdapter.isAvailable();
|
|
869
|
+
if (available)
|
|
870
|
+
return fallbackAdapter;
|
|
871
|
+
} catch {
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
throw new Error(`No available adapter found. Preferred: ${preferred}` + (fallback ? `, fallback: ${fallback}` : ""));
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
// ../adapters/dist/claude-code/adapter.js
|
|
880
|
+
import { execFile } from "child_process";
|
|
881
|
+
|
|
882
|
+
// ../adapters/dist/claude-code/commands.js
|
|
883
|
+
function buildClaudeCommand(options) {
|
|
884
|
+
const args = [
|
|
885
|
+
"-p",
|
|
886
|
+
"--permission-mode",
|
|
887
|
+
"auto",
|
|
888
|
+
"--output-format",
|
|
889
|
+
"stream-json"
|
|
890
|
+
];
|
|
891
|
+
if (options.allowedTools && options.allowedTools.length > 0) {
|
|
892
|
+
args.push("--allowedTools", options.allowedTools.join(","));
|
|
893
|
+
}
|
|
894
|
+
if (options.disallowedTools && options.disallowedTools.length > 0) {
|
|
895
|
+
args.push("--disallowedTools", options.disallowedTools.join(","));
|
|
896
|
+
}
|
|
897
|
+
if (options.useWorktree) {
|
|
898
|
+
args.push("--worktree");
|
|
899
|
+
}
|
|
900
|
+
return { cmd: "claude", args };
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// ../adapters/dist/claude-code/parser.js
|
|
904
|
+
function parseClaudeJsonStream(line) {
|
|
905
|
+
const trimmed = line.trim();
|
|
906
|
+
if (trimmed.length === 0)
|
|
907
|
+
return null;
|
|
908
|
+
try {
|
|
909
|
+
const parsed = JSON.parse(trimmed);
|
|
910
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
const obj = parsed;
|
|
914
|
+
if (typeof obj["type"] !== "string") {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
return obj;
|
|
918
|
+
} catch {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// ../adapters/dist/claude-code/adapter.js
|
|
924
|
+
var ClaudeCodeAdapter = class extends BaseAdapter {
|
|
925
|
+
name = "claude-code";
|
|
926
|
+
buildCommand(options) {
|
|
927
|
+
return buildClaudeCommand(options);
|
|
928
|
+
}
|
|
929
|
+
parseOutput(raw, stream) {
|
|
930
|
+
const output = {
|
|
931
|
+
stream,
|
|
932
|
+
data: raw,
|
|
933
|
+
timestamp: Date.now()
|
|
934
|
+
};
|
|
935
|
+
if (stream === "stdout") {
|
|
936
|
+
const event = parseClaudeJsonStream(raw);
|
|
937
|
+
if (event !== null) {
|
|
938
|
+
output.parsed = event;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return output;
|
|
942
|
+
}
|
|
943
|
+
getAuthCommand() {
|
|
944
|
+
return "claude";
|
|
945
|
+
}
|
|
946
|
+
async isAvailable() {
|
|
947
|
+
try {
|
|
948
|
+
const version = await this.getVersion();
|
|
949
|
+
return version !== null;
|
|
950
|
+
} catch {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
getVersion() {
|
|
955
|
+
return new Promise((resolve4) => {
|
|
956
|
+
execFile("claude", ["--version"], { timeout: 1e4, shell: process.platform === "win32" }, (error, stdout) => {
|
|
957
|
+
if (error) {
|
|
958
|
+
resolve4(null);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const trimmed = stdout.trim();
|
|
962
|
+
if (trimmed.length === 0) {
|
|
963
|
+
resolve4(null);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const match = trimmed.match(/(\d+\.\d+\.\d+[\w.-]*)/);
|
|
967
|
+
resolve4(match ? match[1] : trimmed);
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
// ../adapters/dist/codex-cli/adapter.js
|
|
974
|
+
import { execFile as execFile2, execSync } from "child_process";
|
|
975
|
+
|
|
976
|
+
// ../adapters/dist/codex-cli/commands.js
|
|
977
|
+
function buildCodexCommand(options) {
|
|
978
|
+
const args = [
|
|
979
|
+
"exec",
|
|
980
|
+
"--full-auto",
|
|
981
|
+
"--json",
|
|
982
|
+
"-"
|
|
983
|
+
// Read prompt from stdin
|
|
984
|
+
];
|
|
985
|
+
return { cmd: "codex", args };
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// ../adapters/dist/codex-cli/parser.js
|
|
989
|
+
var COMMAND_PATTERN = /^\s*[$>]\s+/;
|
|
990
|
+
var ERROR_PATTERN = /^(Error|FATAL|FAIL(ED)?|Exception|Traceback)\b/i;
|
|
991
|
+
var COMPLETION_PATTERN = /^(Done|Complete(d)?|Finished|Success(ful(ly)?)?)\b/i;
|
|
992
|
+
function parseCodexOutput(line) {
|
|
993
|
+
const trimmed = line.trim();
|
|
994
|
+
if (trimmed.length === 0) {
|
|
995
|
+
return { type: "text", content: "" };
|
|
996
|
+
}
|
|
997
|
+
if (ERROR_PATTERN.test(trimmed)) {
|
|
998
|
+
return { type: "error", content: trimmed };
|
|
999
|
+
}
|
|
1000
|
+
if (COMPLETION_PATTERN.test(trimmed)) {
|
|
1001
|
+
return { type: "completion", content: trimmed };
|
|
1002
|
+
}
|
|
1003
|
+
if (COMMAND_PATTERN.test(trimmed)) {
|
|
1004
|
+
return { type: "command", content: trimmed };
|
|
1005
|
+
}
|
|
1006
|
+
return { type: "text", content: trimmed };
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// ../adapters/dist/codex-cli/adapter.js
|
|
1010
|
+
var CodexCliAdapter = class extends BaseAdapter {
|
|
1011
|
+
name = "codex-cli";
|
|
1012
|
+
buildCommand(options) {
|
|
1013
|
+
return buildCodexCommand(options);
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Override spawn to:
|
|
1017
|
+
* 1. Create an isolation branch for Codex to work on
|
|
1018
|
+
* 2. Use the headless piped I/O path (codex exec)
|
|
1019
|
+
* 3. Pipe the prompt via stdin
|
|
1020
|
+
*/
|
|
1021
|
+
async spawn(options) {
|
|
1022
|
+
const branchName = `cronies/codex-${Date.now()}`;
|
|
1023
|
+
try {
|
|
1024
|
+
execSync(`git checkout -b ${branchName}`, {
|
|
1025
|
+
cwd: options.workingDirectory,
|
|
1026
|
+
stdio: "ignore",
|
|
1027
|
+
timeout: 5e3
|
|
1028
|
+
});
|
|
1029
|
+
} catch {
|
|
1030
|
+
console.warn(`[codex] Failed to create isolation branch ${branchName} \u2014 running on current branch`);
|
|
1031
|
+
}
|
|
1032
|
+
return super.spawn({ ...options, mode: "supervised" });
|
|
1033
|
+
}
|
|
1034
|
+
parseOutput(raw, stream) {
|
|
1035
|
+
const output = {
|
|
1036
|
+
stream,
|
|
1037
|
+
data: raw,
|
|
1038
|
+
timestamp: Date.now()
|
|
1039
|
+
};
|
|
1040
|
+
if (stream === "stdout") {
|
|
1041
|
+
try {
|
|
1042
|
+
const obj = JSON.parse(raw.trim());
|
|
1043
|
+
if (typeof obj === "object" && obj !== null && typeof obj.type === "string") {
|
|
1044
|
+
output.parsed = obj;
|
|
1045
|
+
return output;
|
|
1046
|
+
}
|
|
1047
|
+
} catch {
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const parsed = parseCodexOutput(raw);
|
|
1051
|
+
output.parsed = parsed;
|
|
1052
|
+
return output;
|
|
1053
|
+
}
|
|
1054
|
+
getAuthCommand() {
|
|
1055
|
+
return "codex auth login";
|
|
1056
|
+
}
|
|
1057
|
+
async isAvailable() {
|
|
1058
|
+
try {
|
|
1059
|
+
const version = await this.getVersion();
|
|
1060
|
+
return version !== null;
|
|
1061
|
+
} catch {
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
getVersion() {
|
|
1066
|
+
return new Promise((resolve4) => {
|
|
1067
|
+
execFile2("codex", ["--version"], { timeout: 1e4, shell: process.platform === "win32" }, (error, stdout) => {
|
|
1068
|
+
if (error) {
|
|
1069
|
+
resolve4(null);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const trimmed = stdout.trim();
|
|
1073
|
+
if (trimmed.length === 0) {
|
|
1074
|
+
resolve4(null);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const match = trimmed.match(/(\d+\.\d+\.\d+[\w.-]*)/);
|
|
1078
|
+
resolve4(match ? match[1] : trimmed);
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
// ../adapters/dist/gemini-cli/adapter.js
|
|
1085
|
+
import { execFile as execFile3 } from "child_process";
|
|
1086
|
+
|
|
1087
|
+
// ../adapters/dist/gemini-cli/commands.js
|
|
1088
|
+
function buildGeminiCommand(options) {
|
|
1089
|
+
const args = [
|
|
1090
|
+
"--approval-mode",
|
|
1091
|
+
"yolo",
|
|
1092
|
+
"-o",
|
|
1093
|
+
"stream-json",
|
|
1094
|
+
// Prompt is piped via stdin by the base adapter (bypasses cmd.exe char limit).
|
|
1095
|
+
// -p with no value tells Gemini to read from stdin in non-interactive mode.
|
|
1096
|
+
"-p"
|
|
1097
|
+
];
|
|
1098
|
+
return { cmd: "gemini", args };
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// ../adapters/dist/gemini-cli/parser.js
|
|
1102
|
+
function parseGeminiJsonStream(line) {
|
|
1103
|
+
const trimmed = line.trim();
|
|
1104
|
+
if (trimmed.length === 0)
|
|
1105
|
+
return null;
|
|
1106
|
+
try {
|
|
1107
|
+
const parsed = JSON.parse(trimmed);
|
|
1108
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
const obj = parsed;
|
|
1112
|
+
if (typeof obj["type"] !== "string")
|
|
1113
|
+
return null;
|
|
1114
|
+
return obj;
|
|
1115
|
+
} catch {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// ../adapters/dist/gemini-cli/adapter.js
|
|
1121
|
+
var GeminiCliAdapter = class extends BaseAdapter {
|
|
1122
|
+
name = "gemini-cli";
|
|
1123
|
+
buildCommand(options) {
|
|
1124
|
+
return buildGeminiCommand(options);
|
|
1125
|
+
}
|
|
1126
|
+
parseOutput(raw, stream) {
|
|
1127
|
+
const output = {
|
|
1128
|
+
stream,
|
|
1129
|
+
data: raw,
|
|
1130
|
+
timestamp: Date.now()
|
|
1131
|
+
};
|
|
1132
|
+
if (stream === "stdout") {
|
|
1133
|
+
const event = parseGeminiJsonStream(raw);
|
|
1134
|
+
if (event !== null) {
|
|
1135
|
+
output.parsed = event;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return output;
|
|
1139
|
+
}
|
|
1140
|
+
getAuthCommand() {
|
|
1141
|
+
return "gemini auth login";
|
|
1142
|
+
}
|
|
1143
|
+
async isAvailable() {
|
|
1144
|
+
try {
|
|
1145
|
+
const version = await this.getVersion();
|
|
1146
|
+
return version !== null;
|
|
1147
|
+
} catch {
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
getVersion() {
|
|
1152
|
+
return new Promise((resolve4) => {
|
|
1153
|
+
execFile3("gemini", ["--version"], { timeout: 1e4, shell: process.platform === "win32" }, (error, stdout) => {
|
|
1154
|
+
if (error) {
|
|
1155
|
+
resolve4(null);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const trimmed = stdout.trim();
|
|
1159
|
+
if (trimmed.length === 0) {
|
|
1160
|
+
resolve4(null);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
const match = trimmed.match(/(\d+\.\d+\.\d+[\w.-]*)/);
|
|
1164
|
+
resolve4(match ? match[1] : trimmed);
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
// ../adapters/dist/cursor-agent/adapter.js
|
|
1171
|
+
import { execFile as execFile4 } from "child_process";
|
|
1172
|
+
|
|
1173
|
+
// ../adapters/dist/cursor-agent/commands.js
|
|
1174
|
+
function toWslPath(winPath) {
|
|
1175
|
+
const normalized = winPath.replace(/\\/g, "/");
|
|
1176
|
+
const match = normalized.match(/^([A-Za-z]):\/(.*)/);
|
|
1177
|
+
if (!match)
|
|
1178
|
+
return normalized;
|
|
1179
|
+
return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
|
|
1180
|
+
}
|
|
1181
|
+
function buildCursorAgentCommand(options) {
|
|
1182
|
+
const isWindows = process.platform === "win32";
|
|
1183
|
+
const workspace = isWindows ? toWslPath(options.workingDirectory) : options.workingDirectory;
|
|
1184
|
+
const args = [
|
|
1185
|
+
"-p",
|
|
1186
|
+
"--trust",
|
|
1187
|
+
"--yolo",
|
|
1188
|
+
"--output-format",
|
|
1189
|
+
"stream-json",
|
|
1190
|
+
"--workspace",
|
|
1191
|
+
workspace
|
|
1192
|
+
];
|
|
1193
|
+
if (options.useWorktree) {
|
|
1194
|
+
args.push("--worktree");
|
|
1195
|
+
}
|
|
1196
|
+
if (isWindows) {
|
|
1197
|
+
const agentArgs = ["agent", ...args];
|
|
1198
|
+
const escaped = agentArgs.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
|
|
1199
|
+
return { cmd: "wsl", args: ["bash", "-ic", escaped], shell: false };
|
|
1200
|
+
}
|
|
1201
|
+
return { cmd: "agent", args };
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// ../adapters/dist/cursor-agent/parser.js
|
|
1205
|
+
function parseCursorJsonStream(line) {
|
|
1206
|
+
const trimmed = line.trim();
|
|
1207
|
+
if (trimmed.length === 0)
|
|
1208
|
+
return null;
|
|
1209
|
+
try {
|
|
1210
|
+
const parsed = JSON.parse(trimmed);
|
|
1211
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
const obj = parsed;
|
|
1215
|
+
if (typeof obj["type"] !== "string")
|
|
1216
|
+
return null;
|
|
1217
|
+
return obj;
|
|
1218
|
+
} catch {
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// ../adapters/dist/cursor-agent/adapter.js
|
|
1224
|
+
var CursorAgentAdapter = class extends BaseAdapter {
|
|
1225
|
+
name = "cursor-agent";
|
|
1226
|
+
buildCommand(options) {
|
|
1227
|
+
return buildCursorAgentCommand(options);
|
|
1228
|
+
}
|
|
1229
|
+
parseOutput(raw, stream) {
|
|
1230
|
+
const output = {
|
|
1231
|
+
stream,
|
|
1232
|
+
data: raw,
|
|
1233
|
+
timestamp: Date.now()
|
|
1234
|
+
};
|
|
1235
|
+
if (stream === "stdout") {
|
|
1236
|
+
const event = parseCursorJsonStream(raw);
|
|
1237
|
+
if (event !== null) {
|
|
1238
|
+
output.parsed = event;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
return output;
|
|
1242
|
+
}
|
|
1243
|
+
getAuthCommand() {
|
|
1244
|
+
return "agent auth login";
|
|
1245
|
+
}
|
|
1246
|
+
async isAvailable() {
|
|
1247
|
+
try {
|
|
1248
|
+
const version = await this.getVersion();
|
|
1249
|
+
return version !== null;
|
|
1250
|
+
} catch {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
getVersion() {
|
|
1255
|
+
const isWindows = process.platform === "win32";
|
|
1256
|
+
const cmd = isWindows ? "wsl" : "agent";
|
|
1257
|
+
const args = isWindows ? ["bash", "-ic", "agent --version"] : ["--version"];
|
|
1258
|
+
return new Promise((resolve4) => {
|
|
1259
|
+
execFile4(cmd, args, { timeout: 1e4 }, (error, stdout) => {
|
|
1260
|
+
if (error) {
|
|
1261
|
+
resolve4(null);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const trimmed = stdout.trim();
|
|
1265
|
+
if (trimmed.length === 0) {
|
|
1266
|
+
resolve4(null);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
const match = trimmed.match(/(\d+[\d.]+[\w.-]*)/);
|
|
1270
|
+
resolve4(match ? match[1] : trimmed);
|
|
1271
|
+
});
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
// src/adapters/manager.ts
|
|
1277
|
+
function createAdapterManager() {
|
|
1278
|
+
const manager = new AdapterManager();
|
|
1279
|
+
manager.register(new ClaudeCodeAdapter());
|
|
1280
|
+
manager.register(new CodexCliAdapter());
|
|
1281
|
+
manager.register(new GeminiCliAdapter());
|
|
1282
|
+
manager.register(new CursorAgentAdapter());
|
|
1283
|
+
return manager;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// src/policy/engine.ts
|
|
1287
|
+
var LocalPolicyEngine = class {
|
|
1288
|
+
snapshot;
|
|
1289
|
+
constructor(snapshot) {
|
|
1290
|
+
this.snapshot = snapshot;
|
|
1291
|
+
}
|
|
1292
|
+
/** Replace the active policy snapshot (e.g. after a cloud policy update). */
|
|
1293
|
+
updatePolicy(snapshot) {
|
|
1294
|
+
this.snapshot = snapshot;
|
|
1295
|
+
}
|
|
1296
|
+
/** Return the current policy snapshot. */
|
|
1297
|
+
getSnapshot() {
|
|
1298
|
+
return this.snapshot;
|
|
1299
|
+
}
|
|
1300
|
+
// -------------------------------------------------------------------------
|
|
1301
|
+
// Action evaluation
|
|
1302
|
+
// -------------------------------------------------------------------------
|
|
1303
|
+
/**
|
|
1304
|
+
* Evaluate whether a classified action should proceed.
|
|
1305
|
+
*
|
|
1306
|
+
* Order of checks:
|
|
1307
|
+
* 1. Deny list (hard block)
|
|
1308
|
+
* 2. Escalation patterns (regex match -> escalate)
|
|
1309
|
+
* 3. Risk threshold (score exceeds limit -> escalate)
|
|
1310
|
+
* 4. Allowed list (if non-empty, action must be listed)
|
|
1311
|
+
* 5. Default allow
|
|
1312
|
+
*/
|
|
1313
|
+
evaluateAction(classification) {
|
|
1314
|
+
const action = classification.detectedAction;
|
|
1315
|
+
if (action && this.isDenied(action)) {
|
|
1316
|
+
return {
|
|
1317
|
+
allowed: false,
|
|
1318
|
+
action: "deny",
|
|
1319
|
+
reason: `Action '${action}' is in deny list`
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
if (action && this.matchesEscalationPattern(action)) {
|
|
1323
|
+
return {
|
|
1324
|
+
allowed: false,
|
|
1325
|
+
action: "escalate",
|
|
1326
|
+
reason: `Action '${action}' matches escalation pattern`
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
if (classification.riskScore > this.snapshot.localRiskThreshold) {
|
|
1330
|
+
return {
|
|
1331
|
+
allowed: false,
|
|
1332
|
+
action: "escalate",
|
|
1333
|
+
reason: `Risk score ${classification.riskScore} exceeds threshold ${this.snapshot.localRiskThreshold}`
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
if (this.snapshot.allowedActions.length > 0 && action) {
|
|
1337
|
+
if (!this.snapshot.allowedActions.includes(action)) {
|
|
1338
|
+
return {
|
|
1339
|
+
allowed: false,
|
|
1340
|
+
action: "escalate",
|
|
1341
|
+
reason: `Action '${action}' not in allowed list`
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return { allowed: true, action: "allow", reason: "Action permitted by policy" };
|
|
1346
|
+
}
|
|
1347
|
+
// -------------------------------------------------------------------------
|
|
1348
|
+
// Limit checks
|
|
1349
|
+
// -------------------------------------------------------------------------
|
|
1350
|
+
/** True when retryCount is below the local retry threshold. */
|
|
1351
|
+
canRetryLocally(retryCount) {
|
|
1352
|
+
return retryCount < this.snapshot.localRetryThreshold;
|
|
1353
|
+
}
|
|
1354
|
+
/** True when the job is still within its maximum wall-clock duration. */
|
|
1355
|
+
isWithinTimeLimit(startedAt) {
|
|
1356
|
+
return Date.now() - startedAt < this.snapshot.maxJobDurationMs;
|
|
1357
|
+
}
|
|
1358
|
+
/** True when accumulated agent CPU time is below the policy limit. */
|
|
1359
|
+
isWithinCostLimit(totalAgentTimeMs) {
|
|
1360
|
+
return totalAgentTimeMs < this.snapshot.maxAgentTimeMs;
|
|
1361
|
+
}
|
|
1362
|
+
// -------------------------------------------------------------------------
|
|
1363
|
+
// Private helpers
|
|
1364
|
+
// -------------------------------------------------------------------------
|
|
1365
|
+
isDenied(action) {
|
|
1366
|
+
return this.snapshot.deniedActions.some((d) => action.includes(d));
|
|
1367
|
+
}
|
|
1368
|
+
matchesEscalationPattern(action) {
|
|
1369
|
+
return this.snapshot.escalatePatterns.some((pattern) => {
|
|
1370
|
+
try {
|
|
1371
|
+
return new RegExp(pattern).test(action);
|
|
1372
|
+
} catch {
|
|
1373
|
+
return false;
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
// ../protocol/dist/version.js
|
|
1380
|
+
var PROTOCOL_VERSION = 1;
|
|
1381
|
+
|
|
1382
|
+
// ../protocol/dist/messages.js
|
|
1383
|
+
import { z as z12 } from "zod";
|
|
1384
|
+
|
|
1385
|
+
// ../core/dist/types/common.js
|
|
1386
|
+
import { z as z2 } from "zod";
|
|
1387
|
+
var AgentTypeSchema = z2.enum(["claude-code", "codex-cli", "gemini-cli", "cursor-agent"]);
|
|
1388
|
+
var EventSourceSchema = z2.enum(["daemon", "cloud"]);
|
|
1389
|
+
|
|
1390
|
+
// ../core/dist/types/outcome.js
|
|
1391
|
+
import { z as z3 } from "zod";
|
|
1392
|
+
var TimeHorizonSchema = z3.enum(["ongoing", "deadline"]);
|
|
1393
|
+
var RiskLevelSchema = z3.enum(["low", "medium", "high"]);
|
|
1394
|
+
var OutcomeStatusSchema = z3.enum(["active", "paused", "completed", "failed"]);
|
|
1395
|
+
var OutcomeSchema = z3.object({
|
|
1396
|
+
id: z3.string(),
|
|
1397
|
+
teamId: z3.string(),
|
|
1398
|
+
name: z3.string(),
|
|
1399
|
+
objective: z3.string(),
|
|
1400
|
+
successCriteria: z3.array(z3.string()),
|
|
1401
|
+
timeHorizon: TimeHorizonSchema,
|
|
1402
|
+
deadline: z3.date().optional(),
|
|
1403
|
+
riskLevel: RiskLevelSchema,
|
|
1404
|
+
policyId: z3.string(),
|
|
1405
|
+
status: OutcomeStatusSchema,
|
|
1406
|
+
createdAt: z3.date(),
|
|
1407
|
+
updatedAt: z3.date()
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
// ../core/dist/types/job.js
|
|
1411
|
+
import { z as z4 } from "zod";
|
|
1412
|
+
var ScheduleTypeSchema = z4.enum(["cron", "event", "manual"]);
|
|
1413
|
+
var JobScheduleSchema = z4.object({
|
|
1414
|
+
type: ScheduleTypeSchema,
|
|
1415
|
+
cron: z4.string().optional(),
|
|
1416
|
+
event: z4.string().optional()
|
|
1417
|
+
});
|
|
1418
|
+
var JobStatusSchema = z4.enum(["active", "paused", "completed"]);
|
|
1419
|
+
var JobSchema = z4.object({
|
|
1420
|
+
id: z4.string(),
|
|
1421
|
+
outcomeId: z4.string(),
|
|
1422
|
+
name: z4.string(),
|
|
1423
|
+
prompt: z4.string(),
|
|
1424
|
+
schedule: JobScheduleSchema,
|
|
1425
|
+
preferredAgent: AgentTypeSchema,
|
|
1426
|
+
fallbackAgent: AgentTypeSchema.optional(),
|
|
1427
|
+
workingDirectory: z4.string(),
|
|
1428
|
+
repository: z4.string().optional(),
|
|
1429
|
+
maxAttempts: z4.number().default(10),
|
|
1430
|
+
timeout: z4.number().default(36e5),
|
|
1431
|
+
status: JobStatusSchema,
|
|
1432
|
+
createdAt: z4.date(),
|
|
1433
|
+
updatedAt: z4.date()
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
// ../core/dist/types/run.js
|
|
1437
|
+
import { z as z5 } from "zod";
|
|
1438
|
+
var RunCostSchema = z5.object({
|
|
1439
|
+
agentTimeMs: z5.number(),
|
|
1440
|
+
tokenEstimate: z5.number().optional()
|
|
1441
|
+
});
|
|
1442
|
+
var RunSchema = z5.object({
|
|
1443
|
+
id: z5.string(),
|
|
1444
|
+
jobId: z5.string(),
|
|
1445
|
+
daemonId: z5.string(),
|
|
1446
|
+
agentUsed: AgentTypeSchema,
|
|
1447
|
+
status: z5.string(),
|
|
1448
|
+
startedAt: z5.date(),
|
|
1449
|
+
completedAt: z5.date().optional(),
|
|
1450
|
+
cost: RunCostSchema,
|
|
1451
|
+
artifacts: z5.array(z5.string()),
|
|
1452
|
+
exitReason: z5.string().optional(),
|
|
1453
|
+
checkpointId: z5.string().optional()
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
// ../core/dist/types/event.js
|
|
1457
|
+
import { z as z6 } from "zod";
|
|
1458
|
+
var RunEventTypeSchema = z6.enum([
|
|
1459
|
+
"state_transition",
|
|
1460
|
+
"agent_output",
|
|
1461
|
+
"classification",
|
|
1462
|
+
"decision",
|
|
1463
|
+
"checkpoint_created",
|
|
1464
|
+
"escalation_sent",
|
|
1465
|
+
"escalation_resolved",
|
|
1466
|
+
"policy_updated",
|
|
1467
|
+
"cost_accrued",
|
|
1468
|
+
"health_check"
|
|
1469
|
+
]);
|
|
1470
|
+
var RunEventSchema = z6.object({
|
|
1471
|
+
id: z6.string(),
|
|
1472
|
+
runId: z6.string(),
|
|
1473
|
+
source: EventSourceSchema,
|
|
1474
|
+
sourceId: z6.string(),
|
|
1475
|
+
type: RunEventTypeSchema,
|
|
1476
|
+
payload: z6.record(z6.string(), z6.unknown()),
|
|
1477
|
+
timestamp: z6.number(),
|
|
1478
|
+
sequence: z6.number()
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
// ../core/dist/types/classification.js
|
|
1482
|
+
import { z as z7 } from "zod";
|
|
1483
|
+
var ClassificationCategorySchema = z7.enum([
|
|
1484
|
+
"success",
|
|
1485
|
+
"transient_error",
|
|
1486
|
+
"fatal_error",
|
|
1487
|
+
"milestone",
|
|
1488
|
+
"complete",
|
|
1489
|
+
"risky_action",
|
|
1490
|
+
"ambiguous",
|
|
1491
|
+
"idle"
|
|
1492
|
+
]);
|
|
1493
|
+
var ClassificationSchema = z7.object({
|
|
1494
|
+
category: ClassificationCategorySchema,
|
|
1495
|
+
confidence: z7.number().min(0).max(1),
|
|
1496
|
+
riskScore: z7.number().min(0).max(100),
|
|
1497
|
+
detectedAction: z7.string().optional(),
|
|
1498
|
+
summary: z7.string(),
|
|
1499
|
+
rawOutput: z7.string()
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
// ../core/dist/types/policy.js
|
|
1503
|
+
import { z as z8 } from "zod";
|
|
1504
|
+
var OfflineModeSchema = z8.enum(["pause", "continue-safe", "continue-all"]);
|
|
1505
|
+
var PolicySnapshotSchema = z8.object({
|
|
1506
|
+
version: z8.number(),
|
|
1507
|
+
updatedAt: z8.number(),
|
|
1508
|
+
localRetryThreshold: z8.number().default(3),
|
|
1509
|
+
localRiskThreshold: z8.number().default(60),
|
|
1510
|
+
maxRetries: z8.number().default(10),
|
|
1511
|
+
maxJobDurationMs: z8.number().default(36e5),
|
|
1512
|
+
maxAgentTimeMs: z8.number().default(72e5),
|
|
1513
|
+
allowedActions: z8.array(z8.string()),
|
|
1514
|
+
deniedActions: z8.array(z8.string()),
|
|
1515
|
+
escalatePatterns: z8.array(z8.string()),
|
|
1516
|
+
allowAgentSwitching: z8.boolean(),
|
|
1517
|
+
preferredAgent: AgentTypeSchema,
|
|
1518
|
+
offlineMode: OfflineModeSchema,
|
|
1519
|
+
offlineMaxRetries: z8.number().default(3)
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
// ../core/dist/types/escalation.js
|
|
1523
|
+
import { z as z9 } from "zod";
|
|
1524
|
+
var EscalationTypeSchema = z9.enum([
|
|
1525
|
+
"STUCK_LOOP",
|
|
1526
|
+
"POLICY_VIOLATION",
|
|
1527
|
+
"HIGH_RISK",
|
|
1528
|
+
"AMBIGUOUS_OUTPUT",
|
|
1529
|
+
"LIMIT_EXCEEDED",
|
|
1530
|
+
"CAPABILITY_GAP",
|
|
1531
|
+
"CONFLICT"
|
|
1532
|
+
]);
|
|
1533
|
+
var EscalationSeveritySchema = z9.enum(["low", "medium", "high", "critical"]);
|
|
1534
|
+
var EscalationContextSchema = z9.object({
|
|
1535
|
+
currentState: z9.string(),
|
|
1536
|
+
recentOutput: z9.array(z9.string()),
|
|
1537
|
+
classificationHistory: z9.array(ClassificationSchema),
|
|
1538
|
+
checkpointId: z9.string().optional()
|
|
1539
|
+
});
|
|
1540
|
+
var EscalationRequestSchema = z9.object({
|
|
1541
|
+
id: z9.string(),
|
|
1542
|
+
daemonId: z9.string(),
|
|
1543
|
+
jobId: z9.string(),
|
|
1544
|
+
outcomeId: z9.string(),
|
|
1545
|
+
runId: z9.string(),
|
|
1546
|
+
escalationType: EscalationTypeSchema,
|
|
1547
|
+
severity: EscalationSeveritySchema,
|
|
1548
|
+
context: EscalationContextSchema,
|
|
1549
|
+
localAssessment: z9.string(),
|
|
1550
|
+
timestamp: z9.number()
|
|
1551
|
+
});
|
|
1552
|
+
var EscalationDecisionSchema = z9.discriminatedUnion("action", [
|
|
1553
|
+
z9.object({ action: z9.literal("continue"), instruction: z9.string().optional() }),
|
|
1554
|
+
z9.object({ action: z9.literal("retry"), modifiedPrompt: z9.string().optional() }),
|
|
1555
|
+
z9.object({ action: z9.literal("switch_agent"), targetAgent: AgentTypeSchema }),
|
|
1556
|
+
z9.object({ action: z9.literal("abort"), reason: z9.string() }),
|
|
1557
|
+
z9.object({ action: z9.literal("human_escalation"), message: z9.string() }),
|
|
1558
|
+
z9.object({ action: z9.literal("approve_action") }),
|
|
1559
|
+
z9.object({ action: z9.literal("deny_action"), alternative: z9.string().optional() }),
|
|
1560
|
+
z9.object({
|
|
1561
|
+
action: z9.literal("wait"),
|
|
1562
|
+
durationMs: z9.number(),
|
|
1563
|
+
then: z9.lazy(() => EscalationDecisionSchema)
|
|
1564
|
+
})
|
|
1565
|
+
]);
|
|
1566
|
+
var EscalationResponseSchema = z9.object({
|
|
1567
|
+
id: z9.string(),
|
|
1568
|
+
decision: EscalationDecisionSchema,
|
|
1569
|
+
reasoning: z9.string(),
|
|
1570
|
+
overrides: z9.record(z9.string(), z9.unknown()).optional(),
|
|
1571
|
+
timestamp: z9.number()
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
// ../core/dist/types/checkpoint.js
|
|
1575
|
+
import { z as z10 } from "zod";
|
|
1576
|
+
var CheckpointSchema = z10.object({
|
|
1577
|
+
id: z10.string(),
|
|
1578
|
+
runId: z10.string(),
|
|
1579
|
+
machineState: z10.string(),
|
|
1580
|
+
machineContext: z10.string(),
|
|
1581
|
+
agentState: z10.string().optional(),
|
|
1582
|
+
createdAt: z10.date()
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
// ../core/dist/types/health.js
|
|
1586
|
+
import { z as z11 } from "zod";
|
|
1587
|
+
var SessionHealthStatusSchema = z11.enum([
|
|
1588
|
+
"healthy",
|
|
1589
|
+
// Normal operation, recent activity, no issues
|
|
1590
|
+
"degraded",
|
|
1591
|
+
// Retrying, elevated errors, but still progressing
|
|
1592
|
+
"stalled",
|
|
1593
|
+
// No output for extended period, may be stuck
|
|
1594
|
+
"failing",
|
|
1595
|
+
// Repeated errors, escalations pending
|
|
1596
|
+
"terminated"
|
|
1597
|
+
// Session ended (completed or failed)
|
|
1598
|
+
]);
|
|
1599
|
+
var HealthSignalsSchema = z11.object({
|
|
1600
|
+
/** Seconds since last agent output */
|
|
1601
|
+
idleSeconds: z11.number(),
|
|
1602
|
+
/** Rolling error rate over last N classifications (0-1) */
|
|
1603
|
+
errorRate: z11.number().min(0).max(1),
|
|
1604
|
+
/** Current retry count vs max */
|
|
1605
|
+
retryRatio: z11.number().min(0).max(1),
|
|
1606
|
+
/** Number of pending escalations */
|
|
1607
|
+
pendingEscalations: z11.number(),
|
|
1608
|
+
/** Whether the agent process is alive */
|
|
1609
|
+
processAlive: z11.boolean(),
|
|
1610
|
+
/** Total wall-clock duration in ms */
|
|
1611
|
+
wallClockMs: z11.number(),
|
|
1612
|
+
/** Total agent CPU time in ms */
|
|
1613
|
+
agentTimeMs: z11.number(),
|
|
1614
|
+
/** Percentage of max job duration consumed (0-1) */
|
|
1615
|
+
timebudgetUsed: z11.number().min(0).max(1),
|
|
1616
|
+
/** Number of checkpoints created */
|
|
1617
|
+
checkpointCount: z11.number(),
|
|
1618
|
+
/** Most recent classification category, if any */
|
|
1619
|
+
lastClassification: ClassificationCategorySchema.nullable(),
|
|
1620
|
+
/** Most recent classification confidence */
|
|
1621
|
+
lastConfidence: z11.number().min(0).max(1).nullable(),
|
|
1622
|
+
/** Most recent risk score */
|
|
1623
|
+
lastRiskScore: z11.number().min(0).max(100).nullable(),
|
|
1624
|
+
/** Whether a stuck-loop pattern has been detected */
|
|
1625
|
+
stuckLoopDetected: z11.boolean(),
|
|
1626
|
+
/** Count of consecutive errors */
|
|
1627
|
+
consecutiveErrors: z11.number()
|
|
1628
|
+
});
|
|
1629
|
+
var SessionHealthSchema = z11.object({
|
|
1630
|
+
/** Job ID this health report belongs to */
|
|
1631
|
+
jobId: z11.string(),
|
|
1632
|
+
/** Run ID for the current execution */
|
|
1633
|
+
runId: z11.string(),
|
|
1634
|
+
/** Current agent type */
|
|
1635
|
+
agent: AgentTypeSchema,
|
|
1636
|
+
/** Current XState machine state (serialized) */
|
|
1637
|
+
machineState: z11.string(),
|
|
1638
|
+
/** Overall health status */
|
|
1639
|
+
status: SessionHealthStatusSchema,
|
|
1640
|
+
/** Individual health signals */
|
|
1641
|
+
signals: HealthSignalsSchema,
|
|
1642
|
+
/** Human-readable summary of current health */
|
|
1643
|
+
summary: z11.string(),
|
|
1644
|
+
/** Timestamp of this health snapshot */
|
|
1645
|
+
timestamp: z11.number()
|
|
1646
|
+
});
|
|
1647
|
+
var HealthThresholdsSchema = z11.object({
|
|
1648
|
+
/** Seconds of idle before marking as stalled (default: 120) */
|
|
1649
|
+
stallIdleSeconds: z11.number().default(120),
|
|
1650
|
+
/** Error rate threshold for degraded status (default: 0.3) */
|
|
1651
|
+
degradedErrorRate: z11.number().default(0.3),
|
|
1652
|
+
/** Error rate threshold for failing status (default: 0.6) */
|
|
1653
|
+
failingErrorRate: z11.number().default(0.6),
|
|
1654
|
+
/** Consecutive errors before failing (default: 3) */
|
|
1655
|
+
failingConsecutiveErrors: z11.number().default(3),
|
|
1656
|
+
/** Number of recent classifications to consider for error rate (default: 10) */
|
|
1657
|
+
errorRateWindow: z11.number().default(10),
|
|
1658
|
+
/** Repeated classification pattern length to detect stuck loops (default: 3) */
|
|
1659
|
+
stuckLoopPatternLength: z11.number().default(3)
|
|
1660
|
+
});
|
|
1661
|
+
var DEFAULT_HEALTH_THRESHOLDS = {
|
|
1662
|
+
stallIdleSeconds: 120,
|
|
1663
|
+
degradedErrorRate: 0.3,
|
|
1664
|
+
failingErrorRate: 0.6,
|
|
1665
|
+
failingConsecutiveErrors: 3,
|
|
1666
|
+
errorRateWindow: 10,
|
|
1667
|
+
stuckLoopPatternLength: 3
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
// ../core/dist/state-machine/orchestrator.js
|
|
1671
|
+
import { setup } from "xstate";
|
|
1672
|
+
var initialContext = {
|
|
1673
|
+
jobId: "",
|
|
1674
|
+
outcomeId: "",
|
|
1675
|
+
daemonId: "",
|
|
1676
|
+
currentAgent: "claude-code",
|
|
1677
|
+
agentProcessId: null,
|
|
1678
|
+
retryCount: 0,
|
|
1679
|
+
maxRetries: 10,
|
|
1680
|
+
retryBackoffMs: 1e3,
|
|
1681
|
+
lastClassification: null,
|
|
1682
|
+
classificationHistory: [],
|
|
1683
|
+
lastCheckpointId: null,
|
|
1684
|
+
checkpointCount: 0,
|
|
1685
|
+
pendingEscalation: null,
|
|
1686
|
+
escalationCount: 0,
|
|
1687
|
+
jobStartedAt: 0,
|
|
1688
|
+
lastActivityAt: 0,
|
|
1689
|
+
totalAgentTimeMs: 0,
|
|
1690
|
+
policySnapshot: {
|
|
1691
|
+
version: 0,
|
|
1692
|
+
updatedAt: 0,
|
|
1693
|
+
localRetryThreshold: 3,
|
|
1694
|
+
localRiskThreshold: 60,
|
|
1695
|
+
maxRetries: 10,
|
|
1696
|
+
maxJobDurationMs: 36e5,
|
|
1697
|
+
maxAgentTimeMs: 72e5,
|
|
1698
|
+
allowedActions: [],
|
|
1699
|
+
deniedActions: [],
|
|
1700
|
+
escalatePatterns: [],
|
|
1701
|
+
allowAgentSwitching: false,
|
|
1702
|
+
preferredAgent: "claude-code",
|
|
1703
|
+
offlineMode: "pause",
|
|
1704
|
+
offlineMaxRetries: 3
|
|
1705
|
+
},
|
|
1706
|
+
pendingRunEvents: []
|
|
1707
|
+
};
|
|
1708
|
+
var orchestratorMachine = setup({
|
|
1709
|
+
types: {
|
|
1710
|
+
context: {},
|
|
1711
|
+
events: {}
|
|
1712
|
+
},
|
|
1713
|
+
guards: {
|
|
1714
|
+
canRetry: () => false,
|
|
1715
|
+
retriesExhausted: () => false,
|
|
1716
|
+
exceedsLocalPolicy: () => false,
|
|
1717
|
+
tooManyRetries: () => false,
|
|
1718
|
+
patternDetected: () => false,
|
|
1719
|
+
highRiskAction: () => false,
|
|
1720
|
+
exceedsTimeLimit: () => false,
|
|
1721
|
+
exceedsCostEstimate: () => false,
|
|
1722
|
+
hasResumePlan: () => false,
|
|
1723
|
+
needsRestart: () => false,
|
|
1724
|
+
isDecisionSwitchAgent: () => false,
|
|
1725
|
+
isDecisionRetry: () => false,
|
|
1726
|
+
isDecisionAbort: () => false,
|
|
1727
|
+
isDecisionHumanEscalation: () => false
|
|
1728
|
+
},
|
|
1729
|
+
actions: {
|
|
1730
|
+
spawnAgent: () => {
|
|
1731
|
+
},
|
|
1732
|
+
terminateAgent: () => {
|
|
1733
|
+
},
|
|
1734
|
+
incrementRetry: () => {
|
|
1735
|
+
},
|
|
1736
|
+
resetRetry: () => {
|
|
1737
|
+
},
|
|
1738
|
+
recordClassification: () => {
|
|
1739
|
+
},
|
|
1740
|
+
createCheckpoint: () => {
|
|
1741
|
+
},
|
|
1742
|
+
sendEscalation: () => {
|
|
1743
|
+
},
|
|
1744
|
+
recordRunEvent: () => {
|
|
1745
|
+
},
|
|
1746
|
+
updateTimings: () => {
|
|
1747
|
+
},
|
|
1748
|
+
assignJob: () => {
|
|
1749
|
+
},
|
|
1750
|
+
assignAgent: () => {
|
|
1751
|
+
},
|
|
1752
|
+
assignDecision: () => {
|
|
1753
|
+
},
|
|
1754
|
+
assignEscalation: () => {
|
|
1755
|
+
},
|
|
1756
|
+
clearEscalation: () => {
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}).createMachine({
|
|
1760
|
+
id: "orchestrator",
|
|
1761
|
+
context: initialContext,
|
|
1762
|
+
initial: "idle",
|
|
1763
|
+
states: {
|
|
1764
|
+
// ------------------------------------------------------------------
|
|
1765
|
+
// idle — waiting for a job assignment
|
|
1766
|
+
// ------------------------------------------------------------------
|
|
1767
|
+
idle: {
|
|
1768
|
+
on: {
|
|
1769
|
+
JOB_ASSIGNED: {
|
|
1770
|
+
target: "starting",
|
|
1771
|
+
actions: ["assignJob"]
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
},
|
|
1775
|
+
// ------------------------------------------------------------------
|
|
1776
|
+
// starting — spawn the agent process
|
|
1777
|
+
// ------------------------------------------------------------------
|
|
1778
|
+
starting: {
|
|
1779
|
+
entry: ["spawnAgent"],
|
|
1780
|
+
on: {
|
|
1781
|
+
AGENT_SPAWNED: {
|
|
1782
|
+
target: "observing",
|
|
1783
|
+
actions: ["assignAgent"]
|
|
1784
|
+
},
|
|
1785
|
+
AGENT_SPAWN_FAILED: {
|
|
1786
|
+
target: "failed"
|
|
1787
|
+
},
|
|
1788
|
+
CANCEL: { target: "cancelled" }
|
|
1789
|
+
}
|
|
1790
|
+
},
|
|
1791
|
+
// ------------------------------------------------------------------
|
|
1792
|
+
// observing — watching agent output
|
|
1793
|
+
// ------------------------------------------------------------------
|
|
1794
|
+
observing: {
|
|
1795
|
+
entry: ["updateTimings"],
|
|
1796
|
+
on: {
|
|
1797
|
+
OUTPUT_RECEIVED: {
|
|
1798
|
+
target: "classifying",
|
|
1799
|
+
actions: ["recordRunEvent"]
|
|
1800
|
+
},
|
|
1801
|
+
AGENT_IDLE_TIMEOUT: [
|
|
1802
|
+
{
|
|
1803
|
+
target: "acting",
|
|
1804
|
+
guard: "exceedsTimeLimit"
|
|
1805
|
+
},
|
|
1806
|
+
{
|
|
1807
|
+
target: "classifying"
|
|
1808
|
+
}
|
|
1809
|
+
],
|
|
1810
|
+
PAUSE: { target: "paused", actions: ["createCheckpoint"] },
|
|
1811
|
+
CANCEL: { target: "cancelled", actions: ["terminateAgent"] }
|
|
1812
|
+
}
|
|
1813
|
+
},
|
|
1814
|
+
// ------------------------------------------------------------------
|
|
1815
|
+
// classifying — evaluating agent output
|
|
1816
|
+
// ------------------------------------------------------------------
|
|
1817
|
+
classifying: {
|
|
1818
|
+
on: {
|
|
1819
|
+
CLASSIFIED_SUCCESS: {
|
|
1820
|
+
target: "observing",
|
|
1821
|
+
actions: ["recordClassification", "resetRetry", "updateTimings"]
|
|
1822
|
+
},
|
|
1823
|
+
CLASSIFIED_MILESTONE: {
|
|
1824
|
+
target: "acting",
|
|
1825
|
+
actions: ["recordClassification", "createCheckpoint"]
|
|
1826
|
+
},
|
|
1827
|
+
CLASSIFIED_DONE: {
|
|
1828
|
+
target: "completed",
|
|
1829
|
+
actions: ["recordClassification", "createCheckpoint"]
|
|
1830
|
+
},
|
|
1831
|
+
CLASSIFIED_TRANSIENT_ERROR: [
|
|
1832
|
+
{
|
|
1833
|
+
target: "acting",
|
|
1834
|
+
guard: "patternDetected",
|
|
1835
|
+
actions: ["recordClassification"]
|
|
1836
|
+
},
|
|
1837
|
+
{
|
|
1838
|
+
target: "acting",
|
|
1839
|
+
guard: "canRetry",
|
|
1840
|
+
actions: ["recordClassification"]
|
|
1841
|
+
},
|
|
1842
|
+
{
|
|
1843
|
+
target: "failed",
|
|
1844
|
+
actions: ["recordClassification"]
|
|
1845
|
+
}
|
|
1846
|
+
],
|
|
1847
|
+
CLASSIFIED_COMPLEX_FAILURE: {
|
|
1848
|
+
target: "acting",
|
|
1849
|
+
actions: ["recordClassification"]
|
|
1850
|
+
},
|
|
1851
|
+
CLASSIFIED_RISKY_ACTION: [
|
|
1852
|
+
{
|
|
1853
|
+
target: "acting",
|
|
1854
|
+
guard: "highRiskAction",
|
|
1855
|
+
actions: ["recordClassification"]
|
|
1856
|
+
},
|
|
1857
|
+
{
|
|
1858
|
+
target: "acting",
|
|
1859
|
+
guard: "exceedsLocalPolicy",
|
|
1860
|
+
actions: ["recordClassification"]
|
|
1861
|
+
},
|
|
1862
|
+
{
|
|
1863
|
+
target: "observing",
|
|
1864
|
+
actions: ["recordClassification"]
|
|
1865
|
+
}
|
|
1866
|
+
],
|
|
1867
|
+
CLASSIFIED_FATAL: {
|
|
1868
|
+
target: "failed",
|
|
1869
|
+
actions: ["recordClassification", "terminateAgent", "createCheckpoint"]
|
|
1870
|
+
},
|
|
1871
|
+
CLASSIFIED_AMBIGUOUS: {
|
|
1872
|
+
target: "acting",
|
|
1873
|
+
actions: ["recordClassification"]
|
|
1874
|
+
},
|
|
1875
|
+
CANCEL: { target: "cancelled", actions: ["terminateAgent"] }
|
|
1876
|
+
}
|
|
1877
|
+
},
|
|
1878
|
+
// ------------------------------------------------------------------
|
|
1879
|
+
// acting — compound state for taking action based on classification
|
|
1880
|
+
// ------------------------------------------------------------------
|
|
1881
|
+
acting: {
|
|
1882
|
+
initial: "continuing",
|
|
1883
|
+
on: {
|
|
1884
|
+
CANCEL: { target: "cancelled", actions: ["terminateAgent"] },
|
|
1885
|
+
PAUSE: { target: "paused", actions: ["createCheckpoint"] }
|
|
1886
|
+
},
|
|
1887
|
+
states: {
|
|
1888
|
+
// Continue normal operation
|
|
1889
|
+
continuing: {
|
|
1890
|
+
always: [
|
|
1891
|
+
{
|
|
1892
|
+
target: "escalating",
|
|
1893
|
+
guard: "exceedsTimeLimit"
|
|
1894
|
+
},
|
|
1895
|
+
{
|
|
1896
|
+
target: "escalating",
|
|
1897
|
+
guard: "exceedsCostEstimate"
|
|
1898
|
+
},
|
|
1899
|
+
{
|
|
1900
|
+
target: "escalating",
|
|
1901
|
+
guard: "tooManyRetries"
|
|
1902
|
+
},
|
|
1903
|
+
{
|
|
1904
|
+
target: "escalating",
|
|
1905
|
+
guard: "patternDetected"
|
|
1906
|
+
}
|
|
1907
|
+
],
|
|
1908
|
+
on: {
|
|
1909
|
+
ACTION_COMPLETE: {
|
|
1910
|
+
target: "#orchestrator.observing",
|
|
1911
|
+
actions: ["updateTimings"]
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
},
|
|
1915
|
+
// Retry with backoff
|
|
1916
|
+
retrying: {
|
|
1917
|
+
entry: ["incrementRetry"],
|
|
1918
|
+
always: [
|
|
1919
|
+
{
|
|
1920
|
+
target: "escalating",
|
|
1921
|
+
guard: "retriesExhausted"
|
|
1922
|
+
}
|
|
1923
|
+
],
|
|
1924
|
+
on: {
|
|
1925
|
+
ACTION_COMPLETE: {
|
|
1926
|
+
target: "#orchestrator.observing",
|
|
1927
|
+
actions: ["updateTimings"]
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
},
|
|
1931
|
+
// Checkpointing machine + agent state
|
|
1932
|
+
checkpointing: {
|
|
1933
|
+
entry: ["createCheckpoint"],
|
|
1934
|
+
on: {
|
|
1935
|
+
ACTION_COMPLETE: {
|
|
1936
|
+
target: "#orchestrator.observing"
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
},
|
|
1940
|
+
// Escalating to cloud for a decision
|
|
1941
|
+
escalating: {
|
|
1942
|
+
entry: ["sendEscalation"],
|
|
1943
|
+
on: {
|
|
1944
|
+
ESCALATION_SENT: {
|
|
1945
|
+
target: "#orchestrator.paused.cloud_decision",
|
|
1946
|
+
actions: ["assignEscalation"]
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
},
|
|
1950
|
+
// Switching to fallback agent
|
|
1951
|
+
switching: {
|
|
1952
|
+
entry: ["terminateAgent", "spawnAgent"],
|
|
1953
|
+
on: {
|
|
1954
|
+
AGENT_SPAWNED: {
|
|
1955
|
+
target: "#orchestrator.observing",
|
|
1956
|
+
actions: ["assignAgent", "resetRetry"]
|
|
1957
|
+
},
|
|
1958
|
+
AGENT_SPAWN_FAILED: {
|
|
1959
|
+
target: "#orchestrator.failed"
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
},
|
|
1965
|
+
// ------------------------------------------------------------------
|
|
1966
|
+
// paused — waiting for external input
|
|
1967
|
+
// ------------------------------------------------------------------
|
|
1968
|
+
paused: {
|
|
1969
|
+
initial: "user_requested",
|
|
1970
|
+
on: {
|
|
1971
|
+
CANCEL: { target: "cancelled", actions: ["terminateAgent"] }
|
|
1972
|
+
},
|
|
1973
|
+
states: {
|
|
1974
|
+
// Human escalation — waiting for human operator
|
|
1975
|
+
human_escalation: {
|
|
1976
|
+
on: {
|
|
1977
|
+
CLOUD_DECISION_RECEIVED: {
|
|
1978
|
+
target: "#orchestrator.observing",
|
|
1979
|
+
actions: ["assignDecision", "clearEscalation"]
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
},
|
|
1983
|
+
// Cloud decision — waiting for automated cloud response
|
|
1984
|
+
cloud_decision: {
|
|
1985
|
+
on: {
|
|
1986
|
+
CLOUD_DECISION_RECEIVED: [
|
|
1987
|
+
{
|
|
1988
|
+
target: "#orchestrator.acting.switching",
|
|
1989
|
+
guard: "isDecisionSwitchAgent",
|
|
1990
|
+
actions: ["assignDecision", "clearEscalation"]
|
|
1991
|
+
},
|
|
1992
|
+
{
|
|
1993
|
+
target: "#orchestrator.acting.retrying",
|
|
1994
|
+
guard: "isDecisionRetry",
|
|
1995
|
+
actions: ["assignDecision", "clearEscalation"]
|
|
1996
|
+
},
|
|
1997
|
+
{
|
|
1998
|
+
target: "#orchestrator.failed",
|
|
1999
|
+
guard: "isDecisionAbort",
|
|
2000
|
+
actions: ["assignDecision", "clearEscalation", "terminateAgent"]
|
|
2001
|
+
},
|
|
2002
|
+
{
|
|
2003
|
+
target: "human_escalation",
|
|
2004
|
+
guard: "isDecisionHumanEscalation",
|
|
2005
|
+
actions: ["assignDecision"]
|
|
2006
|
+
},
|
|
2007
|
+
{
|
|
2008
|
+
target: "#orchestrator.observing",
|
|
2009
|
+
actions: ["assignDecision", "clearEscalation"]
|
|
2010
|
+
}
|
|
2011
|
+
]
|
|
2012
|
+
}
|
|
2013
|
+
},
|
|
2014
|
+
// Cooldown — brief pause before retrying
|
|
2015
|
+
cooldown: {
|
|
2016
|
+
on: {
|
|
2017
|
+
RESUME: [
|
|
2018
|
+
{
|
|
2019
|
+
target: "#orchestrator.starting",
|
|
2020
|
+
guard: "needsRestart"
|
|
2021
|
+
},
|
|
2022
|
+
{
|
|
2023
|
+
target: "#orchestrator.observing",
|
|
2024
|
+
guard: "hasResumePlan"
|
|
2025
|
+
}
|
|
2026
|
+
]
|
|
2027
|
+
}
|
|
2028
|
+
},
|
|
2029
|
+
// User-requested pause
|
|
2030
|
+
user_requested: {
|
|
2031
|
+
on: {
|
|
2032
|
+
RESUME: [
|
|
2033
|
+
{
|
|
2034
|
+
target: "#orchestrator.starting",
|
|
2035
|
+
guard: "needsRestart"
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
target: "#orchestrator.observing",
|
|
2039
|
+
guard: "hasResumePlan"
|
|
2040
|
+
}
|
|
2041
|
+
]
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
},
|
|
2046
|
+
// ------------------------------------------------------------------
|
|
2047
|
+
// Terminal states
|
|
2048
|
+
// ------------------------------------------------------------------
|
|
2049
|
+
completed: {
|
|
2050
|
+
type: "final",
|
|
2051
|
+
entry: ["terminateAgent", "createCheckpoint", "recordRunEvent"]
|
|
2052
|
+
},
|
|
2053
|
+
failed: {
|
|
2054
|
+
type: "final",
|
|
2055
|
+
entry: ["terminateAgent", "recordRunEvent"]
|
|
2056
|
+
},
|
|
2057
|
+
cancelled: {
|
|
2058
|
+
type: "final",
|
|
2059
|
+
entry: ["recordRunEvent"]
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
// ../core/dist/state-machine/guards.js
|
|
2065
|
+
function canRetry({ context }) {
|
|
2066
|
+
return context.retryCount < context.maxRetries;
|
|
2067
|
+
}
|
|
2068
|
+
function retriesExhausted({ context }) {
|
|
2069
|
+
return context.retryCount >= context.maxRetries;
|
|
2070
|
+
}
|
|
2071
|
+
function exceedsLocalPolicy({ context }) {
|
|
2072
|
+
const action = context.lastClassification?.detectedAction;
|
|
2073
|
+
if (!action)
|
|
2074
|
+
return false;
|
|
2075
|
+
const { allowedActions, deniedActions, escalatePatterns } = context.policySnapshot;
|
|
2076
|
+
if (deniedActions.includes(action))
|
|
2077
|
+
return true;
|
|
2078
|
+
if (allowedActions.length > 0 && !allowedActions.includes(action))
|
|
2079
|
+
return true;
|
|
2080
|
+
return escalatePatterns.some((pattern) => {
|
|
2081
|
+
try {
|
|
2082
|
+
return new RegExp(pattern).test(action);
|
|
2083
|
+
} catch {
|
|
2084
|
+
return false;
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
function tooManyRetries({ context }) {
|
|
2089
|
+
return context.retryCount > context.policySnapshot.localRetryThreshold;
|
|
2090
|
+
}
|
|
2091
|
+
function patternDetected({ context }) {
|
|
2092
|
+
const recent = context.classificationHistory.slice(-5);
|
|
2093
|
+
const errorCount = recent.filter((c) => c.category === "transient_error" || c.category === "fatal_error").length;
|
|
2094
|
+
return errorCount >= 3;
|
|
2095
|
+
}
|
|
2096
|
+
function highRiskAction({ context }) {
|
|
2097
|
+
if (!context.lastClassification)
|
|
2098
|
+
return false;
|
|
2099
|
+
return context.lastClassification.riskScore > context.policySnapshot.localRiskThreshold;
|
|
2100
|
+
}
|
|
2101
|
+
function exceedsTimeLimit({ context }) {
|
|
2102
|
+
const elapsed = Date.now() - context.jobStartedAt;
|
|
2103
|
+
return elapsed > context.policySnapshot.maxJobDurationMs;
|
|
2104
|
+
}
|
|
2105
|
+
function exceedsCostEstimate({ context }) {
|
|
2106
|
+
return context.totalAgentTimeMs > context.policySnapshot.maxAgentTimeMs;
|
|
2107
|
+
}
|
|
2108
|
+
function hasResumePlan({ context }) {
|
|
2109
|
+
return context.lastCheckpointId !== null;
|
|
2110
|
+
}
|
|
2111
|
+
function needsRestart({ context }) {
|
|
2112
|
+
return context.lastCheckpointId === null;
|
|
2113
|
+
}
|
|
2114
|
+
function isDecisionSwitchAgent({ event }) {
|
|
2115
|
+
return event.decision?.action === "switch_agent";
|
|
2116
|
+
}
|
|
2117
|
+
function isDecisionRetry({ event }) {
|
|
2118
|
+
return event.decision?.action === "retry";
|
|
2119
|
+
}
|
|
2120
|
+
function isDecisionAbort({ event }) {
|
|
2121
|
+
return event.decision?.action === "abort";
|
|
2122
|
+
}
|
|
2123
|
+
function isDecisionHumanEscalation({ event }) {
|
|
2124
|
+
return event.decision?.action === "human_escalation";
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// ../core/dist/constants.js
|
|
2128
|
+
var DEFAULT_POLICY = {
|
|
2129
|
+
localRetryThreshold: 3,
|
|
2130
|
+
localRiskThreshold: 60,
|
|
2131
|
+
maxRetries: 10,
|
|
2132
|
+
maxJobDurationMs: 36e5,
|
|
2133
|
+
maxAgentTimeMs: 72e5,
|
|
2134
|
+
offlineMaxRetries: 3
|
|
2135
|
+
};
|
|
2136
|
+
|
|
2137
|
+
// ../protocol/dist/messages.js
|
|
2138
|
+
var ProtocolEnvelopeSchema = z12.object({
|
|
2139
|
+
v: z12.number(),
|
|
2140
|
+
id: z12.string(),
|
|
2141
|
+
type: z12.string(),
|
|
2142
|
+
ts: z12.number()
|
|
2143
|
+
});
|
|
2144
|
+
var JobDefinitionSchema = z12.object({
|
|
2145
|
+
id: z12.string(),
|
|
2146
|
+
outcomeId: z12.string(),
|
|
2147
|
+
name: z12.string(),
|
|
2148
|
+
prompt: z12.string(),
|
|
2149
|
+
preferredAgent: AgentTypeSchema,
|
|
2150
|
+
fallbackAgent: AgentTypeSchema.optional(),
|
|
2151
|
+
workingDirectory: z12.string(),
|
|
2152
|
+
repository: z12.string().optional(),
|
|
2153
|
+
maxAttempts: z12.number(),
|
|
2154
|
+
timeout: z12.number()
|
|
2155
|
+
});
|
|
2156
|
+
var StateOverrideSchema = z12.object({
|
|
2157
|
+
jobId: z12.string(),
|
|
2158
|
+
machineState: z12.string(),
|
|
2159
|
+
machineContext: z12.string(),
|
|
2160
|
+
reason: z12.string()
|
|
2161
|
+
});
|
|
2162
|
+
var baseEnvelope = (type) => ProtocolEnvelopeSchema.extend({ type: z12.literal(type) });
|
|
2163
|
+
var DaemonHelloSchema = baseEnvelope("daemon:hello").extend({
|
|
2164
|
+
daemonId: z12.string(),
|
|
2165
|
+
lastCloudSeq: z12.number(),
|
|
2166
|
+
protocolVersion: z12.number(),
|
|
2167
|
+
daemonVersion: z12.string(),
|
|
2168
|
+
hostname: z12.string()
|
|
2169
|
+
});
|
|
2170
|
+
var DaemonHeartbeatSchema = baseEnvelope("daemon:heartbeat").extend({
|
|
2171
|
+
daemonId: z12.string(),
|
|
2172
|
+
activeJobs: z12.number(),
|
|
2173
|
+
cpuUsage: z12.number(),
|
|
2174
|
+
memoryUsage: z12.number()
|
|
2175
|
+
});
|
|
2176
|
+
var DaemonEventBatchSchema = baseEnvelope("daemon:event_batch").extend({
|
|
2177
|
+
daemonId: z12.string(),
|
|
2178
|
+
events: z12.array(RunEventSchema)
|
|
2179
|
+
});
|
|
2180
|
+
var DaemonStateSnapshotSchema = baseEnvelope("daemon:state_snapshot").extend({
|
|
2181
|
+
daemonId: z12.string(),
|
|
2182
|
+
jobId: z12.string(),
|
|
2183
|
+
machineState: z12.string(),
|
|
2184
|
+
machineContext: z12.string()
|
|
2185
|
+
});
|
|
2186
|
+
var DaemonEscalationRequestSchema = baseEnvelope("daemon:escalation_request").extend({
|
|
2187
|
+
escalation: EscalationRequestSchema
|
|
2188
|
+
});
|
|
2189
|
+
var DaemonAgentOutputSchema = baseEnvelope("daemon:agent_output").extend({
|
|
2190
|
+
daemonId: z12.string(),
|
|
2191
|
+
jobId: z12.string(),
|
|
2192
|
+
runId: z12.string(),
|
|
2193
|
+
output: z12.string(),
|
|
2194
|
+
stream: z12.enum(["stdout", "stderr"]),
|
|
2195
|
+
timestamp: z12.number()
|
|
2196
|
+
});
|
|
2197
|
+
var DaemonJobStatusSchema = baseEnvelope("daemon:job_status").extend({
|
|
2198
|
+
daemonId: z12.string(),
|
|
2199
|
+
jobId: z12.string(),
|
|
2200
|
+
runId: z12.string(),
|
|
2201
|
+
status: z12.string(),
|
|
2202
|
+
classification: ClassificationSchema.optional()
|
|
2203
|
+
});
|
|
2204
|
+
var DaemonMessageSchema = z12.discriminatedUnion("type", [
|
|
2205
|
+
DaemonHelloSchema,
|
|
2206
|
+
DaemonHeartbeatSchema,
|
|
2207
|
+
DaemonEventBatchSchema,
|
|
2208
|
+
DaemonStateSnapshotSchema,
|
|
2209
|
+
DaemonEscalationRequestSchema,
|
|
2210
|
+
DaemonAgentOutputSchema,
|
|
2211
|
+
DaemonJobStatusSchema
|
|
2212
|
+
]);
|
|
2213
|
+
var CloudWelcomeSchema = baseEnvelope("cloud:welcome").extend({
|
|
2214
|
+
lastDaemonSeq: z12.number(),
|
|
2215
|
+
pendingJobs: z12.array(JobDefinitionSchema),
|
|
2216
|
+
policySnapshot: PolicySnapshotSchema,
|
|
2217
|
+
pendingOverrides: z12.array(StateOverrideSchema)
|
|
2218
|
+
});
|
|
2219
|
+
var CloudJobAssignSchema = baseEnvelope("cloud:job_assign").extend({
|
|
2220
|
+
jobId: z12.string(),
|
|
2221
|
+
outcomeId: z12.string(),
|
|
2222
|
+
job: JobDefinitionSchema,
|
|
2223
|
+
policySnapshot: PolicySnapshotSchema
|
|
2224
|
+
});
|
|
2225
|
+
var CloudJobCancelSchema = baseEnvelope("cloud:job_cancel").extend({
|
|
2226
|
+
jobId: z12.string(),
|
|
2227
|
+
reason: z12.string()
|
|
2228
|
+
});
|
|
2229
|
+
var CloudJobPauseSchema = baseEnvelope("cloud:job_pause").extend({
|
|
2230
|
+
jobId: z12.string()
|
|
2231
|
+
});
|
|
2232
|
+
var CloudJobResumeSchema = baseEnvelope("cloud:job_resume").extend({
|
|
2233
|
+
jobId: z12.string(),
|
|
2234
|
+
checkpoint: z12.string().optional()
|
|
2235
|
+
});
|
|
2236
|
+
var CloudEscalationResponseSchema = baseEnvelope("cloud:escalation_response").extend({
|
|
2237
|
+
escalationResponse: EscalationResponseSchema
|
|
2238
|
+
});
|
|
2239
|
+
var CloudPolicyUpdateSchema = baseEnvelope("cloud:policy_update").extend({
|
|
2240
|
+
policySnapshot: PolicySnapshotSchema
|
|
2241
|
+
});
|
|
2242
|
+
var CloudStateOverrideSchema = baseEnvelope("cloud:state_override").extend({
|
|
2243
|
+
jobId: z12.string(),
|
|
2244
|
+
machineState: z12.string(),
|
|
2245
|
+
machineContext: z12.string(),
|
|
2246
|
+
reason: z12.string()
|
|
2247
|
+
});
|
|
2248
|
+
var CloudEventBatchSchema = baseEnvelope("cloud:event_batch").extend({
|
|
2249
|
+
events: z12.array(RunEventSchema)
|
|
2250
|
+
});
|
|
2251
|
+
var CloudAckSchema = baseEnvelope("cloud:ack").extend({
|
|
2252
|
+
lastSeq: z12.number()
|
|
2253
|
+
});
|
|
2254
|
+
var CloudTokenRefreshSchema = baseEnvelope("cloud:token_refresh").extend({
|
|
2255
|
+
token: z12.string(),
|
|
2256
|
+
expiresAt: z12.number()
|
|
2257
|
+
});
|
|
2258
|
+
var CloudMessageSchema = z12.discriminatedUnion("type", [
|
|
2259
|
+
CloudWelcomeSchema,
|
|
2260
|
+
CloudJobAssignSchema,
|
|
2261
|
+
CloudJobCancelSchema,
|
|
2262
|
+
CloudJobPauseSchema,
|
|
2263
|
+
CloudJobResumeSchema,
|
|
2264
|
+
CloudEscalationResponseSchema,
|
|
2265
|
+
CloudPolicyUpdateSchema,
|
|
2266
|
+
CloudStateOverrideSchema,
|
|
2267
|
+
CloudEventBatchSchema,
|
|
2268
|
+
CloudAckSchema,
|
|
2269
|
+
CloudTokenRefreshSchema
|
|
2270
|
+
]);
|
|
2271
|
+
var ProtocolMessageSchema = z12.union([DaemonMessageSchema, CloudMessageSchema]);
|
|
2272
|
+
|
|
2273
|
+
// ../protocol/dist/codec.js
|
|
2274
|
+
import { randomUUID } from "crypto";
|
|
2275
|
+
function createMessageId() {
|
|
2276
|
+
return randomUUID();
|
|
2277
|
+
}
|
|
2278
|
+
function createEnvelope(type, payload) {
|
|
2279
|
+
return {
|
|
2280
|
+
v: PROTOCOL_VERSION,
|
|
2281
|
+
id: createMessageId(),
|
|
2282
|
+
type,
|
|
2283
|
+
ts: Date.now(),
|
|
2284
|
+
...payload
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
function encodeMessage(msg) {
|
|
2288
|
+
return JSON.stringify(msg);
|
|
2289
|
+
}
|
|
2290
|
+
function decodeMessage(raw) {
|
|
2291
|
+
const json2 = JSON.parse(raw);
|
|
2292
|
+
const daemonResult = DaemonMessageSchema.safeParse(json2);
|
|
2293
|
+
if (daemonResult.success) {
|
|
2294
|
+
return daemonResult.data;
|
|
2295
|
+
}
|
|
2296
|
+
const cloudResult = CloudMessageSchema.safeParse(json2);
|
|
2297
|
+
if (cloudResult.success) {
|
|
2298
|
+
return cloudResult.data;
|
|
2299
|
+
}
|
|
2300
|
+
const parsed = json2;
|
|
2301
|
+
if (typeof parsed?.type === "string" && parsed.type.startsWith("daemon:")) {
|
|
2302
|
+
throw daemonResult.error;
|
|
2303
|
+
}
|
|
2304
|
+
throw cloudResult.error;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// ../protocol/dist/auth.js
|
|
2308
|
+
import { z as z13 } from "zod";
|
|
2309
|
+
var DaemonRegistrationRequestSchema = z13.object({
|
|
2310
|
+
clerkToken: z13.string(),
|
|
2311
|
+
hostname: z13.string(),
|
|
2312
|
+
daemonVersion: z13.string(),
|
|
2313
|
+
capabilities: z13.array(AgentTypeSchema)
|
|
2314
|
+
});
|
|
2315
|
+
var DaemonRegistrationResponseSchema = z13.object({
|
|
2316
|
+
daemonId: z13.string(),
|
|
2317
|
+
daemonToken: z13.string(),
|
|
2318
|
+
expiresAt: z13.number(),
|
|
2319
|
+
wsUrl: z13.string()
|
|
2320
|
+
});
|
|
2321
|
+
|
|
2322
|
+
// src/sync/queue.ts
|
|
2323
|
+
var SyncQueue = class {
|
|
2324
|
+
store;
|
|
2325
|
+
constructor(store) {
|
|
2326
|
+
this.store = store;
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Enqueue a message for later delivery.
|
|
2330
|
+
* Called when the SyncClient is disconnected.
|
|
2331
|
+
*/
|
|
2332
|
+
enqueue(message) {
|
|
2333
|
+
this.store.enqueueMessage(message.type, encodeMessage(message));
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* Drain queued messages by passing each to the send function.
|
|
2337
|
+
* Stops draining if a send fails (connection lost again).
|
|
2338
|
+
* Returns the number of messages successfully sent.
|
|
2339
|
+
*/
|
|
2340
|
+
drain(sendFn) {
|
|
2341
|
+
const messages = this.store.dequeueMessages(100);
|
|
2342
|
+
let sent = 0;
|
|
2343
|
+
for (const msg of messages) {
|
|
2344
|
+
let decoded;
|
|
2345
|
+
try {
|
|
2346
|
+
decoded = JSON.parse(msg.payload);
|
|
2347
|
+
} catch {
|
|
2348
|
+
this.store.markMessageSent(msg.id);
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
if (sendFn(decoded)) {
|
|
2352
|
+
this.store.markMessageSent(msg.id);
|
|
2353
|
+
sent++;
|
|
2354
|
+
} else {
|
|
2355
|
+
break;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
return sent;
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Get the count of messages waiting to be sent.
|
|
2362
|
+
*/
|
|
2363
|
+
getPendingCount() {
|
|
2364
|
+
return this.store.dequeueMessages(1e4).length;
|
|
2365
|
+
}
|
|
2366
|
+
};
|
|
2367
|
+
|
|
2368
|
+
// src/sync/reconciler.ts
|
|
2369
|
+
var SyncReconciler = class {
|
|
2370
|
+
store;
|
|
2371
|
+
constructor(store) {
|
|
2372
|
+
this.store = store;
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Called when the cloud sends a welcome message after our hello.
|
|
2376
|
+
* Reconciles pending jobs, policy, and state overrides from the cloud.
|
|
2377
|
+
*/
|
|
2378
|
+
handleWelcome(welcome) {
|
|
2379
|
+
for (const job of welcome.pendingJobs) {
|
|
2380
|
+
this.store.saveJob(
|
|
2381
|
+
job.id,
|
|
2382
|
+
job.outcomeId,
|
|
2383
|
+
job,
|
|
2384
|
+
"idle",
|
|
2385
|
+
// initial machine state
|
|
2386
|
+
"{}"
|
|
2387
|
+
// initial machine context
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
this.store.cachePolicy(welcome.policySnapshot);
|
|
2391
|
+
for (const override of welcome.pendingOverrides) {
|
|
2392
|
+
this.store.updateJobState(
|
|
2393
|
+
override.jobId,
|
|
2394
|
+
override.machineState,
|
|
2395
|
+
override.machineContext,
|
|
2396
|
+
"active"
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
console.log(
|
|
2400
|
+
`[sync:reconciler] Welcome received: ${welcome.pendingJobs.length} pending jobs, ${welcome.pendingOverrides.length} overrides, policy v${welcome.policySnapshot.version}`
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Called when the cloud sends a batch of events (e.g., events from other daemons
|
|
2405
|
+
* or cloud-generated events).
|
|
2406
|
+
*/
|
|
2407
|
+
handleCloudEvents(batch) {
|
|
2408
|
+
for (const event of batch.events) {
|
|
2409
|
+
const jobId = event.payload["jobId"] ?? "unknown";
|
|
2410
|
+
this.store.appendEvent({
|
|
2411
|
+
id: event.id,
|
|
2412
|
+
runId: event.runId,
|
|
2413
|
+
jobId,
|
|
2414
|
+
source: event.source,
|
|
2415
|
+
sourceId: event.sourceId,
|
|
2416
|
+
type: event.type,
|
|
2417
|
+
payload: event.payload,
|
|
2418
|
+
timestamp: event.timestamp
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
console.log(`[sync:reconciler] Stored ${batch.events.length} cloud events`);
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Called when the cloud acknowledges receipt of our events.
|
|
2425
|
+
* Marks events as synced up to the acknowledged sequence number.
|
|
2426
|
+
*/
|
|
2427
|
+
handleAck(ack) {
|
|
2428
|
+
this.store.markEventsSynced(ack.lastSeq);
|
|
2429
|
+
console.log(`[sync:reconciler] Events synced up to seq ${ack.lastSeq}`);
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Called when the cloud sends a policy update.
|
|
2433
|
+
* Updates the cached policy so the daemon uses the latest rules.
|
|
2434
|
+
*/
|
|
2435
|
+
handlePolicyUpdate(update) {
|
|
2436
|
+
this.store.cachePolicy(update.policySnapshot);
|
|
2437
|
+
console.log(`[sync:reconciler] Policy updated to v${update.policySnapshot.version}`);
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Get events that need to be synced to the cloud.
|
|
2441
|
+
*/
|
|
2442
|
+
getUnsyncedEvents(limit) {
|
|
2443
|
+
return this.store.getUnsyncedEvents(limit);
|
|
2444
|
+
}
|
|
2445
|
+
};
|
|
2446
|
+
|
|
2447
|
+
// src/orchestrator/health.ts
|
|
2448
|
+
var SessionHealthMonitor = class {
|
|
2449
|
+
thresholds;
|
|
2450
|
+
lastOutputAt;
|
|
2451
|
+
processAlive = false;
|
|
2452
|
+
runId;
|
|
2453
|
+
constructor(runId, thresholds) {
|
|
2454
|
+
this.runId = runId;
|
|
2455
|
+
this.thresholds = { ...DEFAULT_HEALTH_THRESHOLDS, ...thresholds };
|
|
2456
|
+
this.lastOutputAt = Date.now();
|
|
2457
|
+
}
|
|
2458
|
+
/** Call when agent output is received to reset the idle timer. */
|
|
2459
|
+
recordOutput() {
|
|
2460
|
+
this.lastOutputAt = Date.now();
|
|
2461
|
+
}
|
|
2462
|
+
/** Call when agent process spawns or dies. */
|
|
2463
|
+
setProcessAlive(alive) {
|
|
2464
|
+
this.processAlive = alive;
|
|
2465
|
+
}
|
|
2466
|
+
/** Compute the current health snapshot from orchestrator context. */
|
|
2467
|
+
compute(context, machineState) {
|
|
2468
|
+
const now = Date.now();
|
|
2469
|
+
const signals = this.computeSignals(context, now);
|
|
2470
|
+
const status = this.deriveStatus(signals, machineState);
|
|
2471
|
+
const summary = this.buildSummary(status, signals, context.currentAgent, machineState);
|
|
2472
|
+
return {
|
|
2473
|
+
jobId: context.jobId,
|
|
2474
|
+
runId: this.runId,
|
|
2475
|
+
agent: context.currentAgent,
|
|
2476
|
+
machineState,
|
|
2477
|
+
status,
|
|
2478
|
+
signals,
|
|
2479
|
+
summary,
|
|
2480
|
+
timestamp: now
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
// -------------------------------------------------------------------------
|
|
2484
|
+
// Signal computation
|
|
2485
|
+
// -------------------------------------------------------------------------
|
|
2486
|
+
computeSignals(context, now) {
|
|
2487
|
+
const history = context.classificationHistory;
|
|
2488
|
+
const window = history.slice(-this.thresholds.errorRateWindow);
|
|
2489
|
+
const errorCategories = [
|
|
2490
|
+
"transient_error",
|
|
2491
|
+
"fatal_error"
|
|
2492
|
+
];
|
|
2493
|
+
const errorCount = window.filter((c) => errorCategories.includes(c.category)).length;
|
|
2494
|
+
const errorRate = window.length > 0 ? errorCount / window.length : 0;
|
|
2495
|
+
const consecutiveErrors = this.countConsecutiveErrors(history);
|
|
2496
|
+
const stuckLoopDetected = this.detectStuckLoop(history);
|
|
2497
|
+
const maxDuration = context.policySnapshot.maxJobDurationMs || 36e5;
|
|
2498
|
+
const wallClockMs = now - context.jobStartedAt;
|
|
2499
|
+
const timebudgetUsed = Math.min(wallClockMs / maxDuration, 1);
|
|
2500
|
+
const last = context.lastClassification;
|
|
2501
|
+
return {
|
|
2502
|
+
idleSeconds: Math.floor((now - this.lastOutputAt) / 1e3),
|
|
2503
|
+
errorRate,
|
|
2504
|
+
retryRatio: context.maxRetries > 0 ? context.retryCount / context.maxRetries : 0,
|
|
2505
|
+
pendingEscalations: context.pendingEscalation ? 1 : 0,
|
|
2506
|
+
processAlive: this.processAlive,
|
|
2507
|
+
wallClockMs,
|
|
2508
|
+
agentTimeMs: context.totalAgentTimeMs,
|
|
2509
|
+
timebudgetUsed,
|
|
2510
|
+
checkpointCount: context.checkpointCount,
|
|
2511
|
+
lastClassification: last?.category ?? null,
|
|
2512
|
+
lastConfidence: last?.confidence ?? null,
|
|
2513
|
+
lastRiskScore: last?.riskScore ?? null,
|
|
2514
|
+
stuckLoopDetected,
|
|
2515
|
+
consecutiveErrors
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
// -------------------------------------------------------------------------
|
|
2519
|
+
// Status derivation — maps signals to a single health status
|
|
2520
|
+
// -------------------------------------------------------------------------
|
|
2521
|
+
deriveStatus(signals, machineState) {
|
|
2522
|
+
if (machineState === "completed" || machineState === "failed" || machineState === "cancelled") {
|
|
2523
|
+
return "terminated";
|
|
2524
|
+
}
|
|
2525
|
+
if (signals.errorRate >= this.thresholds.failingErrorRate || signals.consecutiveErrors >= this.thresholds.failingConsecutiveErrors || signals.pendingEscalations > 0) {
|
|
2526
|
+
return "failing";
|
|
2527
|
+
}
|
|
2528
|
+
if (signals.idleSeconds >= this.thresholds.stallIdleSeconds && signals.processAlive) {
|
|
2529
|
+
return "stalled";
|
|
2530
|
+
}
|
|
2531
|
+
if (!signals.processAlive && machineState !== "idle" && machineState !== "starting") {
|
|
2532
|
+
return "stalled";
|
|
2533
|
+
}
|
|
2534
|
+
if (signals.errorRate >= this.thresholds.degradedErrorRate || signals.retryRatio > 0.5 || signals.stuckLoopDetected || signals.timebudgetUsed > 0.8) {
|
|
2535
|
+
return "degraded";
|
|
2536
|
+
}
|
|
2537
|
+
return "healthy";
|
|
2538
|
+
}
|
|
2539
|
+
// -------------------------------------------------------------------------
|
|
2540
|
+
// Helpers
|
|
2541
|
+
// -------------------------------------------------------------------------
|
|
2542
|
+
countConsecutiveErrors(history) {
|
|
2543
|
+
let count = 0;
|
|
2544
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
2545
|
+
if (history[i].category === "transient_error" || history[i].category === "fatal_error") {
|
|
2546
|
+
count++;
|
|
2547
|
+
} else {
|
|
2548
|
+
break;
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
return count;
|
|
2552
|
+
}
|
|
2553
|
+
detectStuckLoop(history) {
|
|
2554
|
+
const len = this.thresholds.stuckLoopPatternLength;
|
|
2555
|
+
if (history.length < len * 2) return false;
|
|
2556
|
+
const recent = history.slice(-len).map((c) => c.category);
|
|
2557
|
+
const prior = history.slice(-len * 2, -len).map((c) => c.category);
|
|
2558
|
+
return recent.every((cat, i) => cat === prior[i]);
|
|
2559
|
+
}
|
|
2560
|
+
buildSummary(status, signals, agent, machineState) {
|
|
2561
|
+
const parts = [];
|
|
2562
|
+
switch (status) {
|
|
2563
|
+
case "healthy":
|
|
2564
|
+
parts.push(`${agent} running normally`);
|
|
2565
|
+
break;
|
|
2566
|
+
case "degraded":
|
|
2567
|
+
parts.push(`${agent} degraded`);
|
|
2568
|
+
if (signals.stuckLoopDetected) parts.push("stuck loop detected");
|
|
2569
|
+
if (signals.retryRatio > 0.5) parts.push(`retries at ${Math.round(signals.retryRatio * 100)}%`);
|
|
2570
|
+
if (signals.timebudgetUsed > 0.8) parts.push(`${Math.round(signals.timebudgetUsed * 100)}% time budget used`);
|
|
2571
|
+
break;
|
|
2572
|
+
case "stalled":
|
|
2573
|
+
parts.push(`${agent} stalled`);
|
|
2574
|
+
parts.push(`idle ${signals.idleSeconds}s`);
|
|
2575
|
+
if (!signals.processAlive) parts.push("process not responding");
|
|
2576
|
+
break;
|
|
2577
|
+
case "failing":
|
|
2578
|
+
parts.push(`${agent} failing`);
|
|
2579
|
+
if (signals.consecutiveErrors > 0) parts.push(`${signals.consecutiveErrors} consecutive errors`);
|
|
2580
|
+
if (signals.pendingEscalations > 0) parts.push("escalation pending");
|
|
2581
|
+
break;
|
|
2582
|
+
case "terminated":
|
|
2583
|
+
parts.push(`${agent} ${machineState}`);
|
|
2584
|
+
break;
|
|
2585
|
+
}
|
|
2586
|
+
if (signals.lastClassification && status !== "terminated") {
|
|
2587
|
+
parts.push(`last: ${signals.lastClassification}`);
|
|
2588
|
+
}
|
|
2589
|
+
return parts.join(" \u2014 ");
|
|
2590
|
+
}
|
|
2591
|
+
};
|
|
2592
|
+
|
|
2593
|
+
// src/orchestrator/runner.ts
|
|
2594
|
+
import { createActor, assign } from "xstate";
|
|
2595
|
+
|
|
2596
|
+
// src/orchestrator/summary.ts
|
|
2597
|
+
var FILE_EXT = /\.(ts|tsx|js|jsx|json|md|css|html|py|rs|go|yaml|yml|toml|sql|sh|bat|ps1)$/i;
|
|
2598
|
+
var FILE_CREATED_PATTERNS = [
|
|
2599
|
+
/^A\s+(.+)$/,
|
|
2600
|
+
// git-style "A path/to/file"
|
|
2601
|
+
/Created\s+\[?([^\]\s]+\.\w+)/i,
|
|
2602
|
+
// "Created [file.ts]" or "Created file.ts"
|
|
2603
|
+
/new file mode/,
|
|
2604
|
+
// git diff header (skip, use the A line)
|
|
2605
|
+
/^\+\+\+ b\/(.+)$/
|
|
2606
|
+
// git diff "+++ b/path"
|
|
2607
|
+
];
|
|
2608
|
+
var FILE_MODIFIED_PATTERNS = [
|
|
2609
|
+
/^M\s+(.+)$/,
|
|
2610
|
+
// git-style "M path/to/file"
|
|
2611
|
+
/Modified\s+\[?([^\]\s]+\.\w+)/i,
|
|
2612
|
+
// "Modified file.ts"
|
|
2613
|
+
/Updated\s+\[?([^\]\s]+\.\w+)/i
|
|
2614
|
+
// "Updated file.ts"
|
|
2615
|
+
];
|
|
2616
|
+
var TOOL_PATTERNS = [
|
|
2617
|
+
[/apply_patch/i, "apply_patch"],
|
|
2618
|
+
[/\bexec\b.*exited/i, "shell"],
|
|
2619
|
+
[/Tool:\s*(\w+)/i, "$1"],
|
|
2620
|
+
[/tool_call.*"name":\s*"([^"]+)"/i, "$1"],
|
|
2621
|
+
[/\bBash\b/i, "Bash"],
|
|
2622
|
+
[/\bEdit\b/i, "Edit"],
|
|
2623
|
+
[/\bWrite\b/i, "Write"],
|
|
2624
|
+
[/\bRead\b/i, "Read"]
|
|
2625
|
+
];
|
|
2626
|
+
var ERROR_PATTERNS = [
|
|
2627
|
+
/\bERROR\b/i,
|
|
2628
|
+
/\bfailed\b/i,
|
|
2629
|
+
/\berror:\b/i,
|
|
2630
|
+
/exited\s+(?!0\b)\d+/i,
|
|
2631
|
+
// non-zero exit
|
|
2632
|
+
/\bpanic\b/i,
|
|
2633
|
+
/\btimeout\b/i,
|
|
2634
|
+
/rate.?limit/i
|
|
2635
|
+
];
|
|
2636
|
+
var NOISE_PATTERNS = [
|
|
2637
|
+
/^[\s{}[\]]+$/,
|
|
2638
|
+
// braces, brackets
|
|
2639
|
+
/^[-=]{3,}$/,
|
|
2640
|
+
// dividers
|
|
2641
|
+
/^(diff --git|index \w|---|\+\+\+|@@)/,
|
|
2642
|
+
// diff headers
|
|
2643
|
+
/^[+-]\s*[{})\]]/,
|
|
2644
|
+
// diff context lines
|
|
2645
|
+
/^new file mode/,
|
|
2646
|
+
/^file update:?$/i,
|
|
2647
|
+
/^\d[\d,]+$/,
|
|
2648
|
+
// bare numbers
|
|
2649
|
+
/^(tokens?\s+used|mcp:|session\s+id:)/i,
|
|
2650
|
+
// metadata
|
|
2651
|
+
/^```/
|
|
2652
|
+
// code fences
|
|
2653
|
+
];
|
|
2654
|
+
function extractSessionSummary(outputBuffer) {
|
|
2655
|
+
const filesCreated = /* @__PURE__ */ new Set();
|
|
2656
|
+
const filesModified = /* @__PURE__ */ new Set();
|
|
2657
|
+
const toolsUsed = /* @__PURE__ */ new Set();
|
|
2658
|
+
const errors = [];
|
|
2659
|
+
let tokensUsed = null;
|
|
2660
|
+
let agentVersion = null;
|
|
2661
|
+
let model = null;
|
|
2662
|
+
let lastSubstantiveLine = "";
|
|
2663
|
+
for (let i = 0; i < outputBuffer.length; i++) {
|
|
2664
|
+
const line = outputBuffer[i].trim();
|
|
2665
|
+
if (!line) continue;
|
|
2666
|
+
if (!agentVersion) {
|
|
2667
|
+
const vMatch = line.match(/(Claude Code|OpenAI Codex|Gemini CLI|Cursor Agent)\s+v?([\d.]+[^\s]*)/i);
|
|
2668
|
+
if (vMatch) {
|
|
2669
|
+
agentVersion = `${vMatch[1]} ${vMatch[2]}`;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
if (!model) {
|
|
2673
|
+
const mMatch = line.match(/^model:\s+(.+)$/i);
|
|
2674
|
+
if (mMatch) model = mMatch[1].trim();
|
|
2675
|
+
}
|
|
2676
|
+
if (tokensUsed === null) {
|
|
2677
|
+
if (/tokens?\s+used/i.test(line)) {
|
|
2678
|
+
const next = outputBuffer[i + 1]?.trim() ?? "";
|
|
2679
|
+
const num = next.replace(/,/g, "");
|
|
2680
|
+
if (/^\d+$/.test(num)) {
|
|
2681
|
+
tokensUsed = parseInt(num, 10);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
const tMatch = line.match(/total_tokens["\s:]+(\d+)/);
|
|
2685
|
+
if (tMatch) tokensUsed = parseInt(tMatch[1], 10);
|
|
2686
|
+
}
|
|
2687
|
+
for (const pattern of FILE_CREATED_PATTERNS) {
|
|
2688
|
+
const match = line.match(pattern);
|
|
2689
|
+
if (match?.[1] && FILE_EXT.test(match[1])) {
|
|
2690
|
+
filesCreated.add(match[1].replace(/\\/g, "/"));
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
for (const pattern of FILE_MODIFIED_PATTERNS) {
|
|
2694
|
+
const match = line.match(pattern);
|
|
2695
|
+
if (match?.[1] && FILE_EXT.test(match[1])) {
|
|
2696
|
+
filesModified.add(match[1].replace(/\\/g, "/"));
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
for (const [pattern, name] of TOOL_PATTERNS) {
|
|
2700
|
+
if (pattern.test(line)) {
|
|
2701
|
+
const toolName = name.startsWith("$") ? line.match(pattern)?.[1] ?? name : name;
|
|
2702
|
+
toolsUsed.add(toolName);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
if (errors.length < 5) {
|
|
2706
|
+
for (const pattern of ERROR_PATTERNS) {
|
|
2707
|
+
if (pattern.test(line) && !line.includes("failed to load skill")) {
|
|
2708
|
+
errors.push(line.length > 200 ? line.slice(0, 200) + "..." : line);
|
|
2709
|
+
break;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
const isNoise = NOISE_PATTERNS.some((p) => p.test(line));
|
|
2714
|
+
if (!isNoise && line.length > 10) {
|
|
2715
|
+
lastSubstantiveLine = line;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
for (const f of filesCreated) {
|
|
2719
|
+
filesModified.delete(f);
|
|
2720
|
+
}
|
|
2721
|
+
return {
|
|
2722
|
+
filesCreated: [...filesCreated],
|
|
2723
|
+
filesModified: [...filesModified],
|
|
2724
|
+
toolsUsed: [...toolsUsed],
|
|
2725
|
+
errors,
|
|
2726
|
+
finalResult: lastSubstantiveLine.length > 500 ? lastSubstantiveLine.slice(0, 500) + "..." : lastSubstantiveLine,
|
|
2727
|
+
tokensUsed,
|
|
2728
|
+
agentVersion,
|
|
2729
|
+
model
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
// src/orchestrator/runner.ts
|
|
2734
|
+
import { ulid as ulid2 } from "ulid";
|
|
2735
|
+
var JobRunner = class {
|
|
2736
|
+
actor;
|
|
2737
|
+
adapter;
|
|
2738
|
+
process = null;
|
|
2739
|
+
policy;
|
|
2740
|
+
prompt;
|
|
2741
|
+
workingDirectory;
|
|
2742
|
+
// sessionMode removed — all sessions run headless (supervised)
|
|
2743
|
+
classificationRules;
|
|
2744
|
+
outputBuffer = [];
|
|
2745
|
+
lastExitCode = null;
|
|
2746
|
+
runId;
|
|
2747
|
+
callbacks;
|
|
2748
|
+
eventSequence = 0;
|
|
2749
|
+
jobId;
|
|
2750
|
+
daemonId;
|
|
2751
|
+
hardTimeoutTimer = null;
|
|
2752
|
+
healthMonitor;
|
|
2753
|
+
healthInterval = null;
|
|
2754
|
+
constructor(options, callbacks = {}) {
|
|
2755
|
+
this.adapter = options.adapter;
|
|
2756
|
+
this.policy = options.policy;
|
|
2757
|
+
this.prompt = options.prompt;
|
|
2758
|
+
this.workingDirectory = options.workingDirectory;
|
|
2759
|
+
this.classificationRules = options.classificationRules ?? [];
|
|
2760
|
+
this.runId = options.runId;
|
|
2761
|
+
this.callbacks = callbacks;
|
|
2762
|
+
this.jobId = options.jobId;
|
|
2763
|
+
this.daemonId = options.daemonId;
|
|
2764
|
+
this.healthMonitor = new SessionHealthMonitor(options.runId, options.healthThresholds);
|
|
2765
|
+
const self = this;
|
|
2766
|
+
const providedMachine = orchestratorMachine.provide({
|
|
2767
|
+
guards: {
|
|
2768
|
+
canRetry,
|
|
2769
|
+
retriesExhausted,
|
|
2770
|
+
exceedsLocalPolicy,
|
|
2771
|
+
tooManyRetries,
|
|
2772
|
+
patternDetected,
|
|
2773
|
+
highRiskAction,
|
|
2774
|
+
exceedsTimeLimit,
|
|
2775
|
+
exceedsCostEstimate,
|
|
2776
|
+
hasResumePlan,
|
|
2777
|
+
needsRestart,
|
|
2778
|
+
isDecisionSwitchAgent,
|
|
2779
|
+
isDecisionRetry,
|
|
2780
|
+
isDecisionAbort,
|
|
2781
|
+
isDecisionHumanEscalation
|
|
2782
|
+
},
|
|
2783
|
+
actions: {
|
|
2784
|
+
// -- Context-mutating (assign) actions ----------------------------
|
|
2785
|
+
assignJob: assign(({ event }) => {
|
|
2786
|
+
const e = event;
|
|
2787
|
+
return {
|
|
2788
|
+
jobId: e.jobId,
|
|
2789
|
+
outcomeId: e.outcomeId,
|
|
2790
|
+
daemonId: e.daemonId,
|
|
2791
|
+
policySnapshot: e.policySnapshot,
|
|
2792
|
+
maxRetries: e.policySnapshot.maxRetries,
|
|
2793
|
+
jobStartedAt: Date.now(),
|
|
2794
|
+
lastActivityAt: Date.now()
|
|
2795
|
+
};
|
|
2796
|
+
}),
|
|
2797
|
+
assignAgent: assign(({ event }) => {
|
|
2798
|
+
const e = event;
|
|
2799
|
+
return {
|
|
2800
|
+
agentProcessId: e.agentProcessId,
|
|
2801
|
+
currentAgent: e.agent,
|
|
2802
|
+
lastActivityAt: Date.now()
|
|
2803
|
+
};
|
|
2804
|
+
}),
|
|
2805
|
+
assignDecision: assign(({ event }) => {
|
|
2806
|
+
const e = event;
|
|
2807
|
+
return {
|
|
2808
|
+
lastActivityAt: Date.now()
|
|
2809
|
+
// Store decision info if needed for logging
|
|
2810
|
+
};
|
|
2811
|
+
}),
|
|
2812
|
+
assignEscalation: assign(({ context, event }) => {
|
|
2813
|
+
const e = event;
|
|
2814
|
+
return {
|
|
2815
|
+
pendingEscalation: e.request,
|
|
2816
|
+
escalationCount: context.escalationCount + 1
|
|
2817
|
+
};
|
|
2818
|
+
}),
|
|
2819
|
+
clearEscalation: assign({
|
|
2820
|
+
pendingEscalation: null
|
|
2821
|
+
}),
|
|
2822
|
+
incrementRetry: assign(({ context }) => ({
|
|
2823
|
+
retryCount: context.retryCount + 1,
|
|
2824
|
+
retryBackoffMs: Math.min(context.retryBackoffMs * 2, 3e4)
|
|
2825
|
+
})),
|
|
2826
|
+
resetRetry: assign({
|
|
2827
|
+
retryCount: 0,
|
|
2828
|
+
retryBackoffMs: 1e3
|
|
2829
|
+
}),
|
|
2830
|
+
recordClassification: assign(({ context, event }) => {
|
|
2831
|
+
const classification = event.classification;
|
|
2832
|
+
if (!classification) return {};
|
|
2833
|
+
return {
|
|
2834
|
+
lastClassification: classification,
|
|
2835
|
+
classificationHistory: [...context.classificationHistory, classification]
|
|
2836
|
+
};
|
|
2837
|
+
}),
|
|
2838
|
+
updateTimings: assign(({ context }) => {
|
|
2839
|
+
const now = Date.now();
|
|
2840
|
+
const elapsed = now - context.lastActivityAt;
|
|
2841
|
+
return {
|
|
2842
|
+
lastActivityAt: now,
|
|
2843
|
+
totalAgentTimeMs: context.totalAgentTimeMs + Math.max(elapsed, 0)
|
|
2844
|
+
};
|
|
2845
|
+
}),
|
|
2846
|
+
// -- Side-effect actions ------------------------------------------
|
|
2847
|
+
spawnAgent: ({ context }) => {
|
|
2848
|
+
void self.spawnAgent(context.currentAgent);
|
|
2849
|
+
},
|
|
2850
|
+
terminateAgent: () => {
|
|
2851
|
+
void self.terminateProcess();
|
|
2852
|
+
},
|
|
2853
|
+
createCheckpoint: ({ context }) => {
|
|
2854
|
+
self.emitCheckpoint(context);
|
|
2855
|
+
},
|
|
2856
|
+
sendEscalation: ({ context }) => {
|
|
2857
|
+
self.emitEscalation(context);
|
|
2858
|
+
},
|
|
2859
|
+
recordRunEvent: ({ context, event }) => {
|
|
2860
|
+
self.emitRunEvent(context, event);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
if (options.checkpoint) {
|
|
2865
|
+
const restoredSnapshot = JSON.parse(options.checkpoint.machineState);
|
|
2866
|
+
this.actor = createActor(providedMachine, {
|
|
2867
|
+
snapshot: restoredSnapshot
|
|
2868
|
+
});
|
|
2869
|
+
} else {
|
|
2870
|
+
this.actor = createActor(providedMachine);
|
|
2871
|
+
}
|
|
2872
|
+
this.actor.subscribe((snapshot) => {
|
|
2873
|
+
const stateValue = this.serializeStateValue(snapshot.value);
|
|
2874
|
+
this.callbacks.onStateChange?.(stateValue);
|
|
2875
|
+
this.emitHealth();
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
// =========================================================================
|
|
2879
|
+
// Public API
|
|
2880
|
+
// =========================================================================
|
|
2881
|
+
/** Start the actor and send the JOB_ASSIGNED event to kick things off. */
|
|
2882
|
+
async start() {
|
|
2883
|
+
this.actor.start();
|
|
2884
|
+
const policySnapshot = this.policy.getSnapshot();
|
|
2885
|
+
this.actor.send({
|
|
2886
|
+
type: "JOB_ASSIGNED",
|
|
2887
|
+
jobId: this.jobId,
|
|
2888
|
+
outcomeId: "",
|
|
2889
|
+
// Will be set from the actual job
|
|
2890
|
+
daemonId: this.daemonId,
|
|
2891
|
+
policySnapshot
|
|
2892
|
+
});
|
|
2893
|
+
this.healthInterval = setInterval(() => this.emitHealth(), 1e4);
|
|
2894
|
+
const maxDuration = policySnapshot.maxJobDurationMs ?? 36e5;
|
|
2895
|
+
this.hardTimeoutTimer = setTimeout(() => {
|
|
2896
|
+
if (this.process) {
|
|
2897
|
+
this.emitRunEvent(
|
|
2898
|
+
this.actor.getSnapshot().context,
|
|
2899
|
+
{ type: "CANCEL" }
|
|
2900
|
+
);
|
|
2901
|
+
void this.adapter.terminate(this.process);
|
|
2902
|
+
}
|
|
2903
|
+
}, maxDuration);
|
|
2904
|
+
}
|
|
2905
|
+
/** Pause the running job (checkpoints machine state). */
|
|
2906
|
+
pause() {
|
|
2907
|
+
this.actor.send({ type: "PAUSE" });
|
|
2908
|
+
}
|
|
2909
|
+
/** Resume from a paused state. */
|
|
2910
|
+
resume() {
|
|
2911
|
+
this.actor.send({ type: "RESUME" });
|
|
2912
|
+
}
|
|
2913
|
+
/** Cancel the job and terminate any running agent. */
|
|
2914
|
+
cancel() {
|
|
2915
|
+
this.actor.send({ type: "CANCEL" });
|
|
2916
|
+
}
|
|
2917
|
+
/** Forward a cloud escalation decision into the machine. */
|
|
2918
|
+
deliverDecision(decision) {
|
|
2919
|
+
this.actor.send({
|
|
2920
|
+
type: "CLOUD_DECISION_RECEIVED",
|
|
2921
|
+
decision
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
/** Send ACTION_COMPLETE to advance past acting sub-states. */
|
|
2925
|
+
completeAction() {
|
|
2926
|
+
this.actor.send({ type: "ACTION_COMPLETE" });
|
|
2927
|
+
}
|
|
2928
|
+
/** Return the current state value as a string. */
|
|
2929
|
+
getState() {
|
|
2930
|
+
return this.serializeStateValue(this.actor.getSnapshot().value);
|
|
2931
|
+
}
|
|
2932
|
+
/** Return the current agent process (for PID tracking). */
|
|
2933
|
+
getProcess() {
|
|
2934
|
+
return this.process;
|
|
2935
|
+
}
|
|
2936
|
+
/** Return the current machine context. */
|
|
2937
|
+
getContext() {
|
|
2938
|
+
return this.actor.getSnapshot().context;
|
|
2939
|
+
}
|
|
2940
|
+
/** Return the run ID for this runner. */
|
|
2941
|
+
getRunId() {
|
|
2942
|
+
return this.runId;
|
|
2943
|
+
}
|
|
2944
|
+
/** Extract a structured summary from the session's output. */
|
|
2945
|
+
getSessionSummary() {
|
|
2946
|
+
return extractSessionSummary(this.outputBuffer);
|
|
2947
|
+
}
|
|
2948
|
+
/** Compute and return the current session health snapshot. */
|
|
2949
|
+
getHealth() {
|
|
2950
|
+
return this.healthMonitor.compute(this.getContext(), this.getState());
|
|
2951
|
+
}
|
|
2952
|
+
/** Serialise the full machine snapshot for checkpointing. */
|
|
2953
|
+
getPersistedSnapshot() {
|
|
2954
|
+
return JSON.stringify(this.actor.getSnapshot());
|
|
2955
|
+
}
|
|
2956
|
+
/** Stop the actor and terminate any running agent process. */
|
|
2957
|
+
stop() {
|
|
2958
|
+
if (this.hardTimeoutTimer) {
|
|
2959
|
+
clearTimeout(this.hardTimeoutTimer);
|
|
2960
|
+
this.hardTimeoutTimer = null;
|
|
2961
|
+
}
|
|
2962
|
+
if (this.healthInterval) {
|
|
2963
|
+
clearInterval(this.healthInterval);
|
|
2964
|
+
this.healthInterval = null;
|
|
2965
|
+
}
|
|
2966
|
+
this.actor.stop();
|
|
2967
|
+
void this.terminateProcess();
|
|
2968
|
+
}
|
|
2969
|
+
/** Update the policy engine (e.g. after cloud sends a policy update). */
|
|
2970
|
+
updatePolicy(snapshot) {
|
|
2971
|
+
this.policy.updatePolicy(snapshot);
|
|
2972
|
+
}
|
|
2973
|
+
// =========================================================================
|
|
2974
|
+
// Agent lifecycle (private)
|
|
2975
|
+
// =========================================================================
|
|
2976
|
+
async spawnAgent(agentName) {
|
|
2977
|
+
try {
|
|
2978
|
+
this.process = await this.adapter.spawn({
|
|
2979
|
+
prompt: this.prompt,
|
|
2980
|
+
workingDirectory: this.workingDirectory,
|
|
2981
|
+
mode: "supervised"
|
|
2982
|
+
});
|
|
2983
|
+
this.lastExitCode = null;
|
|
2984
|
+
this.actor.send({
|
|
2985
|
+
type: "AGENT_SPAWNED",
|
|
2986
|
+
agentProcessId: String(this.process.pid),
|
|
2987
|
+
agent: agentName ?? this.adapter.name
|
|
2988
|
+
});
|
|
2989
|
+
this.callbacks.onSpawned?.(this.process.pid);
|
|
2990
|
+
this.healthMonitor.setProcessAlive(true);
|
|
2991
|
+
this.adapter.onOutput(this.process, (output) => {
|
|
2992
|
+
this.handleOutput(output);
|
|
2993
|
+
});
|
|
2994
|
+
this.adapter.onExit(this.process, (code, _signal) => {
|
|
2995
|
+
this.lastExitCode = code;
|
|
2996
|
+
this.handleExit(code);
|
|
2997
|
+
});
|
|
2998
|
+
} catch (err) {
|
|
2999
|
+
this.actor.send({
|
|
3000
|
+
type: "AGENT_SPAWN_FAILED",
|
|
3001
|
+
error: String(err)
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
async terminateProcess() {
|
|
3006
|
+
if (this.process) {
|
|
3007
|
+
try {
|
|
3008
|
+
await this.adapter.terminate(this.process);
|
|
3009
|
+
} catch {
|
|
3010
|
+
}
|
|
3011
|
+
this.process = null;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
// =========================================================================
|
|
3015
|
+
// Output handling — observe → classify → act
|
|
3016
|
+
// =========================================================================
|
|
3017
|
+
handleOutput(output) {
|
|
3018
|
+
this.outputBuffer.push(output.data);
|
|
3019
|
+
this.healthMonitor.recordOutput();
|
|
3020
|
+
const parsed = output.parsed;
|
|
3021
|
+
const summary = this.summarizeOutput(parsed, output.data);
|
|
3022
|
+
const runEvent = {
|
|
3023
|
+
id: ulid2(),
|
|
3024
|
+
runId: this.runId,
|
|
3025
|
+
source: "daemon",
|
|
3026
|
+
sourceId: this.runId,
|
|
3027
|
+
type: "agent_output",
|
|
3028
|
+
payload: { summary, stream: output.stream },
|
|
3029
|
+
timestamp: output.timestamp,
|
|
3030
|
+
sequence: this.eventSequence++
|
|
3031
|
+
};
|
|
3032
|
+
this.callbacks.onRunEvent?.(runEvent);
|
|
3033
|
+
this.actor.send({
|
|
3034
|
+
type: "OUTPUT_RECEIVED",
|
|
3035
|
+
output: output.data
|
|
3036
|
+
});
|
|
3037
|
+
this.classifyAndAct(output);
|
|
3038
|
+
}
|
|
3039
|
+
summarizeOutput(parsed, raw) {
|
|
3040
|
+
if (!parsed) return raw.slice(0, 200);
|
|
3041
|
+
const type = parsed.type;
|
|
3042
|
+
const subtype = parsed.subtype;
|
|
3043
|
+
if (type === "system") {
|
|
3044
|
+
if (subtype === "init") return "Session started";
|
|
3045
|
+
if (subtype?.startsWith("hook_")) return "";
|
|
3046
|
+
return "";
|
|
3047
|
+
}
|
|
3048
|
+
if (type === "assistant") {
|
|
3049
|
+
const msg = parsed.message;
|
|
3050
|
+
const content = msg?.content;
|
|
3051
|
+
if (content) {
|
|
3052
|
+
for (const block of content) {
|
|
3053
|
+
if (block.type === "tool_use") {
|
|
3054
|
+
const name = block.name;
|
|
3055
|
+
const input = block.input;
|
|
3056
|
+
const path = input?.file_path ?? input?.path ?? input?.command ?? "";
|
|
3057
|
+
const short = path.split(/[/\\]/).pop() ?? "";
|
|
3058
|
+
return short ? `Tool: ${name}(${short})` : `Tool: ${name}`;
|
|
3059
|
+
}
|
|
3060
|
+
if (block.type === "text") {
|
|
3061
|
+
return (block.text ?? "").slice(0, 200);
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
return "";
|
|
3066
|
+
}
|
|
3067
|
+
if (type === "user") return "";
|
|
3068
|
+
if (type === "message") {
|
|
3069
|
+
const role = parsed.role;
|
|
3070
|
+
if (role === "user") return "";
|
|
3071
|
+
const content = parsed.content;
|
|
3072
|
+
if (content) return content.slice(0, 200);
|
|
3073
|
+
return "";
|
|
3074
|
+
}
|
|
3075
|
+
if (type === "init") return "Session started";
|
|
3076
|
+
if (type === "rate_limit_event") return "Rate limited";
|
|
3077
|
+
if (type === "result") {
|
|
3078
|
+
const cost = parsed.cost_usd;
|
|
3079
|
+
const duration = parsed.duration_ms;
|
|
3080
|
+
const status = parsed.status;
|
|
3081
|
+
const ds = duration ? `${(duration / 1e3).toFixed(1)}s` : "";
|
|
3082
|
+
if (cost != null) return `Completed (${ds}, $${cost.toFixed(4)})`;
|
|
3083
|
+
if (status === "success") return `Completed (${ds})`;
|
|
3084
|
+
if (status) return `${status} (${ds})`;
|
|
3085
|
+
return `Completed (${ds})`;
|
|
3086
|
+
}
|
|
3087
|
+
if (parsed.tool && typeof parsed.tool === "object") {
|
|
3088
|
+
const tool = parsed.tool;
|
|
3089
|
+
return `Tool: ${tool.name ?? "unknown"}`;
|
|
3090
|
+
}
|
|
3091
|
+
if (typeof parsed.content === "string") {
|
|
3092
|
+
return parsed.content.slice(0, 200);
|
|
3093
|
+
}
|
|
3094
|
+
if (parsed.result && typeof parsed.result === "object") {
|
|
3095
|
+
const result = parsed.result;
|
|
3096
|
+
return result.success ? "Success" : `Error: ${result.output ?? "unknown"}`;
|
|
3097
|
+
}
|
|
3098
|
+
if (typeof parsed.error === "object" && parsed.error !== null) {
|
|
3099
|
+
return `Error: ${parsed.error.message ?? "unknown"}`;
|
|
3100
|
+
}
|
|
3101
|
+
return String(parsed.type ?? "") || raw.slice(0, 200);
|
|
3102
|
+
}
|
|
3103
|
+
classifyAndAct(output) {
|
|
3104
|
+
const classification = classifyOutput(
|
|
3105
|
+
output,
|
|
3106
|
+
this.lastExitCode,
|
|
3107
|
+
this.classificationRules
|
|
3108
|
+
);
|
|
3109
|
+
const verdict = this.policy.evaluateAction(classification);
|
|
3110
|
+
switch (classification.category) {
|
|
3111
|
+
case "success":
|
|
3112
|
+
this.actor.send({
|
|
3113
|
+
type: "CLASSIFIED_SUCCESS",
|
|
3114
|
+
classification
|
|
3115
|
+
});
|
|
3116
|
+
break;
|
|
3117
|
+
case "transient_error":
|
|
3118
|
+
this.actor.send({
|
|
3119
|
+
type: "CLASSIFIED_TRANSIENT_ERROR",
|
|
3120
|
+
classification
|
|
3121
|
+
});
|
|
3122
|
+
break;
|
|
3123
|
+
case "fatal_error":
|
|
3124
|
+
this.actor.send({
|
|
3125
|
+
type: "CLASSIFIED_FATAL",
|
|
3126
|
+
classification
|
|
3127
|
+
});
|
|
3128
|
+
break;
|
|
3129
|
+
case "milestone":
|
|
3130
|
+
this.actor.send({
|
|
3131
|
+
type: "CLASSIFIED_MILESTONE",
|
|
3132
|
+
classification
|
|
3133
|
+
});
|
|
3134
|
+
break;
|
|
3135
|
+
case "complete":
|
|
3136
|
+
this.actor.send({
|
|
3137
|
+
type: "CLASSIFIED_DONE",
|
|
3138
|
+
classification
|
|
3139
|
+
});
|
|
3140
|
+
break;
|
|
3141
|
+
case "risky_action":
|
|
3142
|
+
if (!verdict.allowed) {
|
|
3143
|
+
this.actor.send({
|
|
3144
|
+
type: "CLASSIFIED_RISKY_ACTION",
|
|
3145
|
+
classification
|
|
3146
|
+
});
|
|
3147
|
+
} else {
|
|
3148
|
+
this.actor.send({
|
|
3149
|
+
type: "CLASSIFIED_SUCCESS",
|
|
3150
|
+
classification
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
break;
|
|
3154
|
+
case "ambiguous":
|
|
3155
|
+
this.actor.send({
|
|
3156
|
+
type: "CLASSIFIED_AMBIGUOUS",
|
|
3157
|
+
classification
|
|
3158
|
+
});
|
|
3159
|
+
break;
|
|
3160
|
+
case "idle":
|
|
3161
|
+
this.actor.send({
|
|
3162
|
+
type: "CLASSIFIED_AMBIGUOUS",
|
|
3163
|
+
classification
|
|
3164
|
+
});
|
|
3165
|
+
break;
|
|
3166
|
+
default:
|
|
3167
|
+
this.actor.send({
|
|
3168
|
+
type: "CLASSIFIED_SUCCESS",
|
|
3169
|
+
classification
|
|
3170
|
+
});
|
|
3171
|
+
break;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
handleExit(code) {
|
|
3175
|
+
this.healthMonitor.setProcessAlive(false);
|
|
3176
|
+
if (code === 0) {
|
|
3177
|
+
this.actor.send({
|
|
3178
|
+
type: "CLASSIFIED_DONE",
|
|
3179
|
+
classification: {
|
|
3180
|
+
category: "complete",
|
|
3181
|
+
confidence: 0.9,
|
|
3182
|
+
riskScore: 0,
|
|
3183
|
+
summary: "Agent exited successfully",
|
|
3184
|
+
rawOutput: ""
|
|
3185
|
+
}
|
|
3186
|
+
});
|
|
3187
|
+
this.callbacks.onComplete?.(true);
|
|
3188
|
+
} else if (code === null) {
|
|
3189
|
+
this.actor.send({
|
|
3190
|
+
type: "CLASSIFIED_DONE",
|
|
3191
|
+
classification: {
|
|
3192
|
+
category: "complete",
|
|
3193
|
+
confidence: 0.8,
|
|
3194
|
+
riskScore: 0,
|
|
3195
|
+
summary: "Agent process terminated externally",
|
|
3196
|
+
rawOutput: ""
|
|
3197
|
+
}
|
|
3198
|
+
});
|
|
3199
|
+
this.callbacks.onComplete?.(false);
|
|
3200
|
+
} else {
|
|
3201
|
+
this.actor.send({
|
|
3202
|
+
type: "CLASSIFIED_TRANSIENT_ERROR",
|
|
3203
|
+
classification: {
|
|
3204
|
+
category: "transient_error",
|
|
3205
|
+
confidence: 0.7,
|
|
3206
|
+
riskScore: 20,
|
|
3207
|
+
summary: `Agent exited with code ${code}`,
|
|
3208
|
+
rawOutput: ""
|
|
3209
|
+
}
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
// =========================================================================
|
|
3214
|
+
// Side-effect emitters
|
|
3215
|
+
// =========================================================================
|
|
3216
|
+
emitHealth() {
|
|
3217
|
+
const health = this.getHealth();
|
|
3218
|
+
this.callbacks.onHealthChange?.(health);
|
|
3219
|
+
}
|
|
3220
|
+
emitCheckpoint(context) {
|
|
3221
|
+
const checkpoint = {
|
|
3222
|
+
id: ulid2(),
|
|
3223
|
+
runId: this.runId,
|
|
3224
|
+
machineState: this.getPersistedSnapshot(),
|
|
3225
|
+
machineContext: JSON.stringify(context),
|
|
3226
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
3227
|
+
};
|
|
3228
|
+
this.callbacks.onCheckpoint?.(checkpoint);
|
|
3229
|
+
}
|
|
3230
|
+
emitEscalation(context) {
|
|
3231
|
+
const request = {
|
|
3232
|
+
id: ulid2(),
|
|
3233
|
+
daemonId: context.daemonId,
|
|
3234
|
+
jobId: context.jobId,
|
|
3235
|
+
outcomeId: context.outcomeId,
|
|
3236
|
+
runId: this.runId,
|
|
3237
|
+
escalationType: this.inferEscalationType(context),
|
|
3238
|
+
severity: this.inferSeverity(context),
|
|
3239
|
+
context: {
|
|
3240
|
+
currentState: this.getState(),
|
|
3241
|
+
recentOutput: this.outputBuffer.slice(-10),
|
|
3242
|
+
classificationHistory: context.classificationHistory.slice(-5),
|
|
3243
|
+
checkpointId: context.lastCheckpointId ?? void 0
|
|
3244
|
+
},
|
|
3245
|
+
localAssessment: this.buildLocalAssessment(context),
|
|
3246
|
+
timestamp: Date.now()
|
|
3247
|
+
};
|
|
3248
|
+
this.callbacks.onEscalation?.(request);
|
|
3249
|
+
this.actor.send({
|
|
3250
|
+
type: "ESCALATION_SENT",
|
|
3251
|
+
request
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
emitRunEvent(context, event) {
|
|
3255
|
+
const runEvent = {
|
|
3256
|
+
id: ulid2(),
|
|
3257
|
+
runId: this.runId,
|
|
3258
|
+
source: "daemon",
|
|
3259
|
+
sourceId: this.runId,
|
|
3260
|
+
type: "state_transition",
|
|
3261
|
+
payload: {
|
|
3262
|
+
machineEvent: event.type,
|
|
3263
|
+
state: this.getState()
|
|
3264
|
+
},
|
|
3265
|
+
timestamp: Date.now(),
|
|
3266
|
+
sequence: this.eventSequence++
|
|
3267
|
+
};
|
|
3268
|
+
this.callbacks.onRunEvent?.(runEvent);
|
|
3269
|
+
}
|
|
3270
|
+
// =========================================================================
|
|
3271
|
+
// Helpers
|
|
3272
|
+
// =========================================================================
|
|
3273
|
+
inferEscalationType(context) {
|
|
3274
|
+
const last = context.lastClassification;
|
|
3275
|
+
if (context.retryCount > context.policySnapshot.localRetryThreshold) {
|
|
3276
|
+
return "STUCK_LOOP";
|
|
3277
|
+
}
|
|
3278
|
+
if (last?.category === "risky_action") {
|
|
3279
|
+
return "HIGH_RISK";
|
|
3280
|
+
}
|
|
3281
|
+
if (last?.category === "ambiguous") {
|
|
3282
|
+
return "AMBIGUOUS_OUTPUT";
|
|
3283
|
+
}
|
|
3284
|
+
if (Date.now() - context.jobStartedAt > context.policySnapshot.maxJobDurationMs || context.totalAgentTimeMs > context.policySnapshot.maxAgentTimeMs) {
|
|
3285
|
+
return "LIMIT_EXCEEDED";
|
|
3286
|
+
}
|
|
3287
|
+
return "CAPABILITY_GAP";
|
|
3288
|
+
}
|
|
3289
|
+
inferSeverity(context) {
|
|
3290
|
+
if (context.retryCount >= context.maxRetries) return "critical";
|
|
3291
|
+
if ((context.lastClassification?.riskScore ?? 0) > 80) return "high";
|
|
3292
|
+
if (context.escalationCount > 2) return "high";
|
|
3293
|
+
return "medium";
|
|
3294
|
+
}
|
|
3295
|
+
buildLocalAssessment(context) {
|
|
3296
|
+
const parts = [];
|
|
3297
|
+
parts.push(`State: ${this.getState()}`);
|
|
3298
|
+
parts.push(`Retries: ${context.retryCount}/${context.maxRetries}`);
|
|
3299
|
+
parts.push(`Escalations: ${context.escalationCount}`);
|
|
3300
|
+
parts.push(`Agent time: ${context.totalAgentTimeMs}ms`);
|
|
3301
|
+
if (context.lastClassification) {
|
|
3302
|
+
parts.push(
|
|
3303
|
+
`Last classification: ${context.lastClassification.category} (confidence: ${context.lastClassification.confidence}, risk: ${context.lastClassification.riskScore})`
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
return parts.join("; ");
|
|
3307
|
+
}
|
|
3308
|
+
/**
|
|
3309
|
+
* Convert an XState state value (which can be a string or nested object)
|
|
3310
|
+
* to a flat dot-separated string for logging and reporting.
|
|
3311
|
+
*/
|
|
3312
|
+
serializeStateValue(value) {
|
|
3313
|
+
if (typeof value === "string") return value;
|
|
3314
|
+
if (typeof value === "object" && value !== null) {
|
|
3315
|
+
const entries = Object.entries(value);
|
|
3316
|
+
return entries.map(([key, val]) => `${key}.${this.serializeStateValue(val)}`).join(", ");
|
|
3317
|
+
}
|
|
3318
|
+
return String(value);
|
|
3319
|
+
}
|
|
3320
|
+
};
|
|
3321
|
+
|
|
3322
|
+
// src/daemon.ts
|
|
3323
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
3324
|
+
import { join as join3 } from "path";
|
|
3325
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3326
|
+
|
|
3327
|
+
// src/server.ts
|
|
3328
|
+
import { createServer } from "http";
|
|
3329
|
+
import { readFile, readdir, writeFile } from "fs/promises";
|
|
3330
|
+
import { join as join2, resolve as resolve3, isAbsolute, basename } from "path";
|
|
3331
|
+
var SKIP_NAMES = /* @__PURE__ */ new Set([
|
|
3332
|
+
".git",
|
|
3333
|
+
"node_modules",
|
|
3334
|
+
"dist",
|
|
3335
|
+
".next",
|
|
3336
|
+
".env",
|
|
3337
|
+
".env.local",
|
|
3338
|
+
".env.production",
|
|
3339
|
+
".env.development",
|
|
3340
|
+
"__pycache__",
|
|
3341
|
+
".DS_Store"
|
|
3342
|
+
]);
|
|
3343
|
+
var BLOCKED_EXTENSIONS = /* @__PURE__ */ new Set([".env", ".pem", ".key", ".p12", ".pfx"]);
|
|
3344
|
+
function isBlocked(name) {
|
|
3345
|
+
if (SKIP_NAMES.has(name)) return true;
|
|
3346
|
+
if (name.startsWith(".env")) return true;
|
|
3347
|
+
for (const ext of BLOCKED_EXTENSIONS) {
|
|
3348
|
+
if (name.endsWith(ext)) return true;
|
|
3349
|
+
}
|
|
3350
|
+
return false;
|
|
3351
|
+
}
|
|
3352
|
+
function getParam(url, key) {
|
|
3353
|
+
return url.searchParams.get(key);
|
|
3354
|
+
}
|
|
3355
|
+
function json(res, status, body) {
|
|
3356
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
3357
|
+
res.end(JSON.stringify(body));
|
|
3358
|
+
}
|
|
3359
|
+
async function handleReadFile(url, res) {
|
|
3360
|
+
const filePath = getParam(url, "path");
|
|
3361
|
+
const maxLines = parseInt(getParam(url, "maxLines") ?? "200", 10);
|
|
3362
|
+
if (!filePath || !isAbsolute(filePath)) {
|
|
3363
|
+
return json(res, 400, { error: 'Absolute "path" parameter required' });
|
|
3364
|
+
}
|
|
3365
|
+
const resolved = resolve3(filePath);
|
|
3366
|
+
const name = resolved.split(/[\\/]/).pop() ?? "";
|
|
3367
|
+
if (isBlocked(name)) {
|
|
3368
|
+
return json(res, 403, { error: "Access to this file is restricted" });
|
|
3369
|
+
}
|
|
3370
|
+
try {
|
|
3371
|
+
const content = await readFile(resolved, "utf-8");
|
|
3372
|
+
const lines = content.split("\n");
|
|
3373
|
+
const truncated = lines.length > maxLines;
|
|
3374
|
+
json(res, 200, {
|
|
3375
|
+
path: resolved,
|
|
3376
|
+
content: lines.slice(0, maxLines).join("\n"),
|
|
3377
|
+
totalLines: lines.length,
|
|
3378
|
+
truncated
|
|
3379
|
+
});
|
|
3380
|
+
} catch (err) {
|
|
3381
|
+
json(res, 404, { error: `Cannot read: ${err instanceof Error ? err.message : String(err)}` });
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
async function handleListDirectory(url, res) {
|
|
3385
|
+
const dirPath = getParam(url, "path");
|
|
3386
|
+
const recursive = getParam(url, "recursive") === "true";
|
|
3387
|
+
if (!dirPath || !isAbsolute(dirPath)) {
|
|
3388
|
+
return json(res, 400, { error: 'Absolute "path" parameter required' });
|
|
3389
|
+
}
|
|
3390
|
+
const resolved = resolve3(dirPath);
|
|
3391
|
+
try {
|
|
3392
|
+
const entries = await readdir(resolved, { withFileTypes: true });
|
|
3393
|
+
const items = [];
|
|
3394
|
+
for (const entry of entries) {
|
|
3395
|
+
if (isBlocked(entry.name)) continue;
|
|
3396
|
+
items.push({
|
|
3397
|
+
name: entry.name,
|
|
3398
|
+
type: entry.isDirectory() ? "directory" : "file"
|
|
3399
|
+
});
|
|
3400
|
+
}
|
|
3401
|
+
if (recursive) {
|
|
3402
|
+
const dirs = items.filter((i) => i.type === "directory").map((i) => i.name);
|
|
3403
|
+
for (const dir of dirs) {
|
|
3404
|
+
try {
|
|
3405
|
+
const subEntries = await readdir(join2(resolved, dir), { withFileTypes: true });
|
|
3406
|
+
for (const sub of subEntries) {
|
|
3407
|
+
if (isBlocked(sub.name)) continue;
|
|
3408
|
+
items.push({
|
|
3409
|
+
name: `${dir}/${sub.name}`,
|
|
3410
|
+
type: sub.isDirectory() ? "directory" : "file"
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
} catch {
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
json(res, 200, { path: resolved, items });
|
|
3418
|
+
} catch (err) {
|
|
3419
|
+
json(res, 404, { error: `Cannot list: ${err instanceof Error ? err.message : String(err)}` });
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
async function handleSearchFiles(url, res) {
|
|
3423
|
+
const directory = getParam(url, "dir");
|
|
3424
|
+
const pattern = getParam(url, "pattern");
|
|
3425
|
+
const maxResults = parseInt(getParam(url, "max") ?? "20", 10);
|
|
3426
|
+
if (!directory || !isAbsolute(directory)) {
|
|
3427
|
+
return json(res, 400, { error: 'Absolute "dir" parameter required' });
|
|
3428
|
+
}
|
|
3429
|
+
if (!pattern) {
|
|
3430
|
+
return json(res, 400, { error: '"pattern" parameter required' });
|
|
3431
|
+
}
|
|
3432
|
+
const resolved = resolve3(directory);
|
|
3433
|
+
const lowerPattern = pattern.toLowerCase();
|
|
3434
|
+
const results = [];
|
|
3435
|
+
async function walk(dir, depth) {
|
|
3436
|
+
if (depth > 4 || results.length >= maxResults) return;
|
|
3437
|
+
try {
|
|
3438
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
3439
|
+
for (const entry of entries) {
|
|
3440
|
+
if (results.length >= maxResults) break;
|
|
3441
|
+
if (isBlocked(entry.name)) continue;
|
|
3442
|
+
const fullPath = join2(dir, entry.name);
|
|
3443
|
+
if (entry.name.toLowerCase().includes(lowerPattern)) {
|
|
3444
|
+
results.push(fullPath);
|
|
3445
|
+
}
|
|
3446
|
+
if (entry.isDirectory()) {
|
|
3447
|
+
await walk(fullPath, depth + 1);
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
} catch {
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
await walk(resolved, 0);
|
|
3454
|
+
json(res, 200, { directory: resolved, pattern, matches: results });
|
|
3455
|
+
}
|
|
3456
|
+
async function handleWriteCronies(req, res) {
|
|
3457
|
+
const chunks = [];
|
|
3458
|
+
for await (const chunk of req) {
|
|
3459
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
3460
|
+
}
|
|
3461
|
+
let body;
|
|
3462
|
+
try {
|
|
3463
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
3464
|
+
} catch {
|
|
3465
|
+
return json(res, 400, { error: "Invalid JSON body" });
|
|
3466
|
+
}
|
|
3467
|
+
if (!body.path || !body.content) {
|
|
3468
|
+
return json(res, 400, { error: "path and content required" });
|
|
3469
|
+
}
|
|
3470
|
+
if (!isAbsolute(body.path)) {
|
|
3471
|
+
return json(res, 400, { error: "Absolute path required" });
|
|
3472
|
+
}
|
|
3473
|
+
const filename = basename(body.path);
|
|
3474
|
+
if (filename !== "CRONIES.md") {
|
|
3475
|
+
return json(res, 403, { error: "Only CRONIES.md files can be written via this endpoint" });
|
|
3476
|
+
}
|
|
3477
|
+
try {
|
|
3478
|
+
await writeFile(resolve3(body.path), body.content, "utf-8");
|
|
3479
|
+
json(res, 200, { ok: true, path: resolve3(body.path) });
|
|
3480
|
+
} catch (err) {
|
|
3481
|
+
json(res, 500, { error: `Cannot write: ${err instanceof Error ? err.message : String(err)}` });
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
function createDaemonServer(options) {
|
|
3485
|
+
const { port, apiToken, logger } = options;
|
|
3486
|
+
const server = createServer(async (req, res) => {
|
|
3487
|
+
const remoteAddr = req.socket.remoteAddress ?? "";
|
|
3488
|
+
const isLocalhost = ["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(remoteAddr);
|
|
3489
|
+
const authHeader = req.headers.authorization;
|
|
3490
|
+
const hasValidToken = apiToken && authHeader === `Bearer ${apiToken}`;
|
|
3491
|
+
if (!isLocalhost && !hasValidToken) {
|
|
3492
|
+
return json(res, 401, { error: "Unauthorized" });
|
|
3493
|
+
}
|
|
3494
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
3495
|
+
const path = url.pathname;
|
|
3496
|
+
try {
|
|
3497
|
+
if (req.method === "GET" && path === "/api/fs/read") {
|
|
3498
|
+
await handleReadFile(url, res);
|
|
3499
|
+
} else if (req.method === "GET" && path === "/api/fs/list") {
|
|
3500
|
+
await handleListDirectory(url, res);
|
|
3501
|
+
} else if (req.method === "GET" && path === "/api/fs/search") {
|
|
3502
|
+
await handleSearchFiles(url, res);
|
|
3503
|
+
} else if (req.method === "POST" && path === "/api/fs/write-cronies") {
|
|
3504
|
+
await handleWriteCronies(req, res);
|
|
3505
|
+
} else if (req.method === "GET" && path === "/status") {
|
|
3506
|
+
json(res, 200, { status: "ok", pid: process.pid });
|
|
3507
|
+
} else {
|
|
3508
|
+
json(res, 404, { error: "Not found" });
|
|
3509
|
+
}
|
|
3510
|
+
} catch (err) {
|
|
3511
|
+
logger.error("Server error", { path, error: String(err) });
|
|
3512
|
+
json(res, 500, { error: "Internal server error" });
|
|
3513
|
+
}
|
|
3514
|
+
});
|
|
3515
|
+
server.listen(port, "127.0.0.1", () => {
|
|
3516
|
+
logger.info("Daemon HTTP server started", { port, host: "127.0.0.1" });
|
|
3517
|
+
});
|
|
3518
|
+
return server;
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
// src/sync/rest-client.ts
|
|
3522
|
+
var RestClient = class {
|
|
3523
|
+
options;
|
|
3524
|
+
pollTimer = null;
|
|
3525
|
+
running = false;
|
|
3526
|
+
pollCount = 0;
|
|
3527
|
+
// Evaluate idle outcomes every 10 polls (~5 min at 30s poll interval)
|
|
3528
|
+
evalEveryNPolls = 10;
|
|
3529
|
+
/** Called when the poll returns one or more jobs */
|
|
3530
|
+
onJobsReceived;
|
|
3531
|
+
/** Called when a running job has been cancelled/paused from the dashboard */
|
|
3532
|
+
onJobCancelled;
|
|
3533
|
+
/** Called on any polling or network error */
|
|
3534
|
+
onError;
|
|
3535
|
+
constructor(options) {
|
|
3536
|
+
this.options = options;
|
|
3537
|
+
}
|
|
3538
|
+
// -----------------------------------------------------------------------
|
|
3539
|
+
// Lifecycle
|
|
3540
|
+
// -----------------------------------------------------------------------
|
|
3541
|
+
/** Register this daemon with the cloud, then start polling for jobs. */
|
|
3542
|
+
start() {
|
|
3543
|
+
this.running = true;
|
|
3544
|
+
void this.register();
|
|
3545
|
+
void this.poll();
|
|
3546
|
+
this.pollTimer = setInterval(() => void this.poll(), this.options.pollIntervalMs);
|
|
3547
|
+
}
|
|
3548
|
+
/** Stop polling. */
|
|
3549
|
+
stop() {
|
|
3550
|
+
this.running = false;
|
|
3551
|
+
if (this.pollTimer) {
|
|
3552
|
+
clearInterval(this.pollTimer);
|
|
3553
|
+
this.pollTimer = null;
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
isConnected() {
|
|
3557
|
+
return this.running;
|
|
3558
|
+
}
|
|
3559
|
+
// -----------------------------------------------------------------------
|
|
3560
|
+
// Registration & Heartbeat
|
|
3561
|
+
// -----------------------------------------------------------------------
|
|
3562
|
+
/** Register this daemon with the cloud on startup. */
|
|
3563
|
+
async register() {
|
|
3564
|
+
const os = await import("os");
|
|
3565
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
3566
|
+
const agentHelp = {};
|
|
3567
|
+
try {
|
|
3568
|
+
agentHelp["claude-code"] = execSync2("claude --help", {
|
|
3569
|
+
timeout: 5e3,
|
|
3570
|
+
encoding: "utf-8"
|
|
3571
|
+
}).trim();
|
|
3572
|
+
} catch {
|
|
3573
|
+
}
|
|
3574
|
+
try {
|
|
3575
|
+
agentHelp["codex-cli"] = execSync2("codex --help", {
|
|
3576
|
+
timeout: 5e3,
|
|
3577
|
+
encoding: "utf-8"
|
|
3578
|
+
}).trim();
|
|
3579
|
+
} catch {
|
|
3580
|
+
}
|
|
3581
|
+
try {
|
|
3582
|
+
agentHelp["gemini-cli"] = execSync2("gemini --help", {
|
|
3583
|
+
timeout: 5e3,
|
|
3584
|
+
encoding: "utf-8"
|
|
3585
|
+
}).trim();
|
|
3586
|
+
} catch {
|
|
3587
|
+
}
|
|
3588
|
+
try {
|
|
3589
|
+
const cursorCmd = process.platform === "win32" ? 'wsl bash -ic "agent --help"' : "agent --help";
|
|
3590
|
+
agentHelp["cursor-agent"] = execSync2(cursorCmd, {
|
|
3591
|
+
timeout: 5e3,
|
|
3592
|
+
encoding: "utf-8",
|
|
3593
|
+
shell: process.platform === "win32" ? "cmd.exe" : "/bin/sh"
|
|
3594
|
+
}).trim();
|
|
3595
|
+
} catch {
|
|
3596
|
+
}
|
|
3597
|
+
try {
|
|
3598
|
+
await this.post("/api/daemon/register", {
|
|
3599
|
+
daemonId: this.options.daemonId,
|
|
3600
|
+
hostname: os.hostname(),
|
|
3601
|
+
port: this.options.port,
|
|
3602
|
+
daemonVersion: "0.1.0",
|
|
3603
|
+
capabilities: ["claude-code", "codex-cli", "gemini-cli", "cursor-agent"],
|
|
3604
|
+
agentHelp
|
|
3605
|
+
});
|
|
3606
|
+
} catch {
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
/** Send heartbeat to keep daemon status online. Called on each poll. */
|
|
3610
|
+
async heartbeat() {
|
|
3611
|
+
try {
|
|
3612
|
+
await this.patch("/api/daemon/register", {
|
|
3613
|
+
daemonId: this.options.daemonId
|
|
3614
|
+
});
|
|
3615
|
+
} catch {
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
// -----------------------------------------------------------------------
|
|
3619
|
+
// Polling
|
|
3620
|
+
// -----------------------------------------------------------------------
|
|
3621
|
+
async poll() {
|
|
3622
|
+
if (!this.running) return;
|
|
3623
|
+
void this.heartbeat();
|
|
3624
|
+
try {
|
|
3625
|
+
const response = await fetch(`${this.options.baseUrl}/api/daemon/jobs`, {
|
|
3626
|
+
headers: { Authorization: `Bearer ${this.options.apiKey}` }
|
|
3627
|
+
});
|
|
3628
|
+
if (!response.ok) {
|
|
3629
|
+
this.onError?.(new Error(`Poll failed: ${response.status}`));
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
const data = await response.json();
|
|
3633
|
+
if (data.jobs && data.jobs.length > 0) {
|
|
3634
|
+
this.onJobsReceived?.(data.jobs);
|
|
3635
|
+
}
|
|
3636
|
+
} catch (error) {
|
|
3637
|
+
this.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
3638
|
+
}
|
|
3639
|
+
this.pollCount++;
|
|
3640
|
+
if (this.pollCount % this.evalEveryNPolls === 0) {
|
|
3641
|
+
void this.evaluateIdleOutcomes();
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
/** Evaluate all active outcomes that have no running sessions. */
|
|
3645
|
+
async evaluateIdleOutcomes() {
|
|
3646
|
+
try {
|
|
3647
|
+
const response = await fetch(`${this.options.baseUrl}/api/supervisor/evaluate`, {
|
|
3648
|
+
method: "POST",
|
|
3649
|
+
headers: {
|
|
3650
|
+
Authorization: `Bearer ${this.options.apiKey}`,
|
|
3651
|
+
"Content-Type": "application/json"
|
|
3652
|
+
},
|
|
3653
|
+
body: JSON.stringify({ idleOnly: true })
|
|
3654
|
+
});
|
|
3655
|
+
if (response.ok) {
|
|
3656
|
+
const data = await response.json();
|
|
3657
|
+
const evals = data.evaluations ?? [];
|
|
3658
|
+
for (const e of evals) {
|
|
3659
|
+
if (e.action !== "no_action") {
|
|
3660
|
+
console.log(`[cronies] Heartbeat: ${e.action} for outcome ${e.outcomeId} \u2014 ${e.reasoning.slice(0, 100)}`);
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
} catch {
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
/** Check if any jobs the daemon is running have been cancelled/paused from the dashboard */
|
|
3668
|
+
async checkForCancelledJobs(runningJobIds) {
|
|
3669
|
+
if (runningJobIds.length === 0) return;
|
|
3670
|
+
for (const jobId of runningJobIds) {
|
|
3671
|
+
try {
|
|
3672
|
+
const response = await fetch(`${this.options.baseUrl}/api/daemon/jobs/${jobId}`, {
|
|
3673
|
+
headers: { Authorization: `Bearer ${this.options.apiKey}` }
|
|
3674
|
+
});
|
|
3675
|
+
if (!response.ok) continue;
|
|
3676
|
+
const data = await response.json();
|
|
3677
|
+
if (data.job && (data.job.status === "paused" || data.job.status === "cancelled" || data.job.status === "completed")) {
|
|
3678
|
+
this.onJobCancelled?.(jobId);
|
|
3679
|
+
}
|
|
3680
|
+
} catch {
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
// -----------------------------------------------------------------------
|
|
3685
|
+
// Reporting helpers
|
|
3686
|
+
// -----------------------------------------------------------------------
|
|
3687
|
+
/** Report a new run starting */
|
|
3688
|
+
async createRun(run) {
|
|
3689
|
+
await this.post("/api/daemon/runs", run);
|
|
3690
|
+
}
|
|
3691
|
+
/** Update a run (status change, completion, etc.) */
|
|
3692
|
+
async updateRun(runId, update) {
|
|
3693
|
+
await this.patch(`/api/daemon/runs/${runId}`, update);
|
|
3694
|
+
}
|
|
3695
|
+
/** Post run events (batch) */
|
|
3696
|
+
async postEvents(events) {
|
|
3697
|
+
await this.post("/api/daemon/events", { events });
|
|
3698
|
+
}
|
|
3699
|
+
/** Report session health snapshot to the cloud */
|
|
3700
|
+
async postHealth(health) {
|
|
3701
|
+
await this.post("/api/daemon/health", health);
|
|
3702
|
+
}
|
|
3703
|
+
/** Update a job's status (e.g. 'running', 'active', 'completed') */
|
|
3704
|
+
async updateJobStatus(jobId, status) {
|
|
3705
|
+
await this.patch(`/api/daemon/jobs/${jobId}`, { status });
|
|
3706
|
+
}
|
|
3707
|
+
/** Reset orphaned running jobs back to active on daemon startup */
|
|
3708
|
+
async resetOrphanedJobs() {
|
|
3709
|
+
try {
|
|
3710
|
+
await this.post("/api/daemon/jobs/reset-orphaned", {
|
|
3711
|
+
daemonId: this.options.daemonId
|
|
3712
|
+
});
|
|
3713
|
+
} catch {
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
/** Ask the supervisor to evaluate an outcome's next steps. Retries once on failure. */
|
|
3717
|
+
async triggerSupervisorEvaluation(outcomeId) {
|
|
3718
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3719
|
+
try {
|
|
3720
|
+
const response = await fetch(`${this.options.baseUrl}/api/supervisor/evaluate`, {
|
|
3721
|
+
method: "POST",
|
|
3722
|
+
headers: {
|
|
3723
|
+
Authorization: `Bearer ${this.options.apiKey}`,
|
|
3724
|
+
"Content-Type": "application/json"
|
|
3725
|
+
},
|
|
3726
|
+
body: JSON.stringify({ outcomeId })
|
|
3727
|
+
});
|
|
3728
|
+
if (response.ok) {
|
|
3729
|
+
const data = await response.json();
|
|
3730
|
+
const eval0 = data.evaluations?.[0];
|
|
3731
|
+
if (eval0) {
|
|
3732
|
+
console.log(`[supervisor] Evaluation for ${outcomeId}: ${eval0.action} \u2014 ${eval0.reasoning.slice(0, 100)}`);
|
|
3733
|
+
}
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
console.error(`[supervisor] Evaluation failed: ${response.status}`);
|
|
3737
|
+
} catch (err) {
|
|
3738
|
+
console.error(`[supervisor] Evaluation error (attempt ${attempt + 1}):`, err instanceof Error ? err.message : err);
|
|
3739
|
+
}
|
|
3740
|
+
if (attempt === 0) await new Promise((r) => setTimeout(r, 5e3));
|
|
3741
|
+
}
|
|
3742
|
+
console.error(`[supervisor] Evaluation for ${outcomeId} failed after 2 attempts \u2014 outcome may stall`);
|
|
3743
|
+
}
|
|
3744
|
+
// -----------------------------------------------------------------------
|
|
3745
|
+
// Internal HTTP helpers
|
|
3746
|
+
// -----------------------------------------------------------------------
|
|
3747
|
+
async post(path, body) {
|
|
3748
|
+
try {
|
|
3749
|
+
const response = await fetch(`${this.options.baseUrl}${path}`, {
|
|
3750
|
+
method: "POST",
|
|
3751
|
+
headers: {
|
|
3752
|
+
Authorization: `Bearer ${this.options.apiKey}`,
|
|
3753
|
+
"Content-Type": "application/json"
|
|
3754
|
+
},
|
|
3755
|
+
body: JSON.stringify(body)
|
|
3756
|
+
});
|
|
3757
|
+
if (!response.ok) {
|
|
3758
|
+
const text = await response.text().catch(() => "");
|
|
3759
|
+
console.error(`[REST] POST ${path} failed: ${response.status} ${text}`);
|
|
3760
|
+
this.onError?.(new Error(`POST ${path} failed: ${response.status}`));
|
|
3761
|
+
}
|
|
3762
|
+
} catch (error) {
|
|
3763
|
+
console.error(`[REST] POST ${path} error:`, error);
|
|
3764
|
+
this.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
async patch(path, body) {
|
|
3768
|
+
try {
|
|
3769
|
+
const response = await fetch(`${this.options.baseUrl}${path}`, {
|
|
3770
|
+
method: "PATCH",
|
|
3771
|
+
headers: {
|
|
3772
|
+
Authorization: `Bearer ${this.options.apiKey}`,
|
|
3773
|
+
"Content-Type": "application/json"
|
|
3774
|
+
},
|
|
3775
|
+
body: JSON.stringify(body)
|
|
3776
|
+
});
|
|
3777
|
+
if (!response.ok) {
|
|
3778
|
+
this.onError?.(new Error(`PATCH ${path} failed: ${response.status}`));
|
|
3779
|
+
}
|
|
3780
|
+
} catch (error) {
|
|
3781
|
+
this.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
};
|
|
3785
|
+
|
|
3786
|
+
// src/sessionHealthMonitor.ts
|
|
3787
|
+
var DaemonSessionHealthMonitor = class {
|
|
3788
|
+
store;
|
|
3789
|
+
restClient;
|
|
3790
|
+
runners;
|
|
3791
|
+
logger;
|
|
3792
|
+
timer = null;
|
|
3793
|
+
checkIntervalMs;
|
|
3794
|
+
staleTimeoutMs;
|
|
3795
|
+
constructor(options) {
|
|
3796
|
+
this.store = options.store;
|
|
3797
|
+
this.restClient = options.restClient;
|
|
3798
|
+
this.runners = options.runners;
|
|
3799
|
+
this.logger = options.logger;
|
|
3800
|
+
this.checkIntervalMs = parseInt(
|
|
3801
|
+
process.env.SESSION_HEALTH_CHECK_INTERVAL ?? "",
|
|
3802
|
+
10
|
|
3803
|
+
) || 3e4;
|
|
3804
|
+
this.staleTimeoutMs = parseInt(
|
|
3805
|
+
process.env.SESSION_STALE_TIMEOUT ?? "",
|
|
3806
|
+
10
|
|
3807
|
+
) || 12e4;
|
|
3808
|
+
}
|
|
3809
|
+
/** Start the periodic health check timer. */
|
|
3810
|
+
start() {
|
|
3811
|
+
if (this.timer) return;
|
|
3812
|
+
this.logger.info("Session health monitor started", {
|
|
3813
|
+
checkIntervalMs: this.checkIntervalMs,
|
|
3814
|
+
staleTimeoutMs: this.staleTimeoutMs
|
|
3815
|
+
});
|
|
3816
|
+
this.timer = setInterval(() => this.check(), this.checkIntervalMs);
|
|
3817
|
+
}
|
|
3818
|
+
/** Stop the health check timer for graceful shutdown. */
|
|
3819
|
+
stop() {
|
|
3820
|
+
if (this.timer) {
|
|
3821
|
+
clearInterval(this.timer);
|
|
3822
|
+
this.timer = null;
|
|
3823
|
+
this.logger.info("Session health monitor stopped");
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
/** Run a single health check pass. Exposed for testing. */
|
|
3827
|
+
check() {
|
|
3828
|
+
const trackedPids = this.store.getTrackedPids();
|
|
3829
|
+
const now = Date.now();
|
|
3830
|
+
for (const { pid, jobId, agent } of trackedPids) {
|
|
3831
|
+
const alive = this.isProcessAlive(pid);
|
|
3832
|
+
if (alive) continue;
|
|
3833
|
+
const runner = this.runners.get(jobId);
|
|
3834
|
+
if (!runner) {
|
|
3835
|
+
this.store.removePid(pid);
|
|
3836
|
+
continue;
|
|
3837
|
+
}
|
|
3838
|
+
const job = this.store.getJob(jobId);
|
|
3839
|
+
const jobStartedAt = job?.assignedAt ?? 0;
|
|
3840
|
+
const elapsed = now - jobStartedAt;
|
|
3841
|
+
if (elapsed < this.staleTimeoutMs) {
|
|
3842
|
+
continue;
|
|
3843
|
+
}
|
|
3844
|
+
this.logger.warn("[SessionHealthMonitor] Cancelled stale run", {
|
|
3845
|
+
jobId,
|
|
3846
|
+
pid,
|
|
3847
|
+
agent,
|
|
3848
|
+
elapsedMs: elapsed
|
|
3849
|
+
});
|
|
3850
|
+
try {
|
|
3851
|
+
runner.stop();
|
|
3852
|
+
} catch (err) {
|
|
3853
|
+
this.logger.error("Failed to stop stale runner", { jobId, error: String(err) });
|
|
3854
|
+
}
|
|
3855
|
+
this.runners.delete(jobId);
|
|
3856
|
+
this.store.updateJobState(jobId, "cancelled", "{}", "cancelled");
|
|
3857
|
+
this.store.removePid(pid);
|
|
3858
|
+
if (this.restClient) {
|
|
3859
|
+
const runId = this.getRunIdFromRunner(runner);
|
|
3860
|
+
if (runId) {
|
|
3861
|
+
void this.restClient.updateRun(runId, {
|
|
3862
|
+
status: "cancelled",
|
|
3863
|
+
exitReason: "Process died unexpectedly (health monitor)",
|
|
3864
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3865
|
+
});
|
|
3866
|
+
}
|
|
3867
|
+
void this.restClient.updateJobStatus(jobId, "failed");
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
for (const [jobId, runner] of this.runners) {
|
|
3871
|
+
const proc = runner.getProcess();
|
|
3872
|
+
if (proc?.pid) {
|
|
3873
|
+
if (this.isProcessAlive(proc.pid)) continue;
|
|
3874
|
+
} else {
|
|
3875
|
+
}
|
|
3876
|
+
const job = this.store.getJob(jobId);
|
|
3877
|
+
const jobStartedAt = job?.assignedAt ?? 0;
|
|
3878
|
+
const elapsed = now - jobStartedAt;
|
|
3879
|
+
if (elapsed < this.staleTimeoutMs) continue;
|
|
3880
|
+
if (!proc?.pid) {
|
|
3881
|
+
this.logger.warn("[SessionHealthMonitor] Cancelled stale run (no PID)", {
|
|
3882
|
+
jobId,
|
|
3883
|
+
elapsedMs: elapsed
|
|
3884
|
+
});
|
|
3885
|
+
try {
|
|
3886
|
+
runner.stop();
|
|
3887
|
+
} catch (err) {
|
|
3888
|
+
this.logger.error("Failed to stop stale runner", { jobId, error: String(err) });
|
|
3889
|
+
}
|
|
3890
|
+
this.runners.delete(jobId);
|
|
3891
|
+
this.store.updateJobState(jobId, "cancelled", "{}", "cancelled");
|
|
3892
|
+
if (this.restClient) {
|
|
3893
|
+
const runId = this.getRunIdFromRunner(runner);
|
|
3894
|
+
if (runId) {
|
|
3895
|
+
void this.restClient.updateRun(runId, {
|
|
3896
|
+
status: "cancelled",
|
|
3897
|
+
exitReason: "Process died unexpectedly (health monitor)",
|
|
3898
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
void this.restClient.updateJobStatus(jobId, "failed");
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
// -------------------------------------------------------------------------
|
|
3907
|
+
// Helpers
|
|
3908
|
+
// -------------------------------------------------------------------------
|
|
3909
|
+
isProcessAlive(pid) {
|
|
3910
|
+
try {
|
|
3911
|
+
process.kill(pid, 0);
|
|
3912
|
+
return true;
|
|
3913
|
+
} catch {
|
|
3914
|
+
return false;
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
getRunIdFromRunner(runner) {
|
|
3918
|
+
try {
|
|
3919
|
+
return runner.getRunId();
|
|
3920
|
+
} catch {
|
|
3921
|
+
return null;
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
};
|
|
3925
|
+
|
|
3926
|
+
// src/daemon.ts
|
|
3927
|
+
import { ulid as ulid3 } from "ulid";
|
|
3928
|
+
var DEFAULT_POLICY_SNAPSHOT = {
|
|
3929
|
+
version: 0,
|
|
3930
|
+
updatedAt: 0,
|
|
3931
|
+
localRetryThreshold: DEFAULT_POLICY.localRetryThreshold,
|
|
3932
|
+
localRiskThreshold: DEFAULT_POLICY.localRiskThreshold,
|
|
3933
|
+
maxRetries: DEFAULT_POLICY.maxRetries,
|
|
3934
|
+
maxJobDurationMs: DEFAULT_POLICY.maxJobDurationMs,
|
|
3935
|
+
maxAgentTimeMs: DEFAULT_POLICY.maxAgentTimeMs,
|
|
3936
|
+
offlineMaxRetries: DEFAULT_POLICY.offlineMaxRetries,
|
|
3937
|
+
allowedActions: [],
|
|
3938
|
+
deniedActions: [],
|
|
3939
|
+
escalatePatterns: [],
|
|
3940
|
+
allowAgentSwitching: true,
|
|
3941
|
+
preferredAgent: "claude-code",
|
|
3942
|
+
offlineMode: "pause"
|
|
3943
|
+
};
|
|
3944
|
+
function generateDaemonId(store) {
|
|
3945
|
+
const id = randomUUID2();
|
|
3946
|
+
store.setMeta("daemon_id", id);
|
|
3947
|
+
return id;
|
|
3948
|
+
}
|
|
3949
|
+
async function startDaemon(config, logger) {
|
|
3950
|
+
const dataDir = resolveDataDir(config);
|
|
3951
|
+
mkdirSync3(dataDir, { recursive: true });
|
|
3952
|
+
mkdirSync3(join3(dataDir, "logs"), { recursive: true });
|
|
3953
|
+
if (isDaemonRunning(dataDir)) {
|
|
3954
|
+
logger.error("Daemon is already running");
|
|
3955
|
+
process.exit(1);
|
|
3956
|
+
}
|
|
3957
|
+
writePidFile(dataDir);
|
|
3958
|
+
const dbPath = join3(dataDir, "daemon.db");
|
|
3959
|
+
const db = createDatabase(dbPath);
|
|
3960
|
+
const store = new DaemonStore(db);
|
|
3961
|
+
logger.info("Database initialized", { path: dbPath });
|
|
3962
|
+
const orphanPids = store.getTrackedPids();
|
|
3963
|
+
if (orphanPids.length > 0) {
|
|
3964
|
+
logger.info("Cleaning up orphaned processes", { count: orphanPids.length });
|
|
3965
|
+
for (const { pid, jobId, agent } of orphanPids) {
|
|
3966
|
+
try {
|
|
3967
|
+
process.kill(pid, "SIGTERM");
|
|
3968
|
+
logger.info("Killed orphaned process", { pid, jobId, agent });
|
|
3969
|
+
} catch {
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
store.clearAllPids();
|
|
3973
|
+
}
|
|
3974
|
+
const adapterManager = createAdapterManager();
|
|
3975
|
+
const available = await adapterManager.getAvailable();
|
|
3976
|
+
logger.info("Adapters initialized", { available });
|
|
3977
|
+
const cachedPolicy = store.getCachedPolicy();
|
|
3978
|
+
const policy = new LocalPolicyEngine(cachedPolicy ?? DEFAULT_POLICY_SNAPSHOT);
|
|
3979
|
+
logger.info("Policy engine initialized", { version: cachedPolicy?.version ?? "default" });
|
|
3980
|
+
const syncQueue = new SyncQueue(store);
|
|
3981
|
+
const reconciler = new SyncReconciler(store);
|
|
3982
|
+
const activeJobs = store.getActiveJobs();
|
|
3983
|
+
const runners = /* @__PURE__ */ new Map();
|
|
3984
|
+
if (activeJobs.length > 0) {
|
|
3985
|
+
logger.info("Cleaning up orphaned jobs from previous run", { count: activeJobs.length });
|
|
3986
|
+
for (const job of activeJobs) {
|
|
3987
|
+
store.updateJobState(job.jobId, "failed", "{}", "failed");
|
|
3988
|
+
logger.info("Marked orphaned job as failed", { jobId: job.jobId });
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
const daemonId = store.getMeta("daemon_id") ?? generateDaemonId(store);
|
|
3992
|
+
let syncClient = null;
|
|
3993
|
+
let cancelCheckInterval = null;
|
|
3994
|
+
let restClient = null;
|
|
3995
|
+
const deps = {
|
|
3996
|
+
store,
|
|
3997
|
+
reconciler,
|
|
3998
|
+
policy,
|
|
3999
|
+
runners,
|
|
4000
|
+
adapterManager,
|
|
4001
|
+
logger,
|
|
4002
|
+
syncQueue,
|
|
4003
|
+
syncClient: null,
|
|
4004
|
+
// will be set after SyncClient is created
|
|
4005
|
+
restClient: null,
|
|
4006
|
+
// will be set after RestClient is created
|
|
4007
|
+
daemonId
|
|
4008
|
+
};
|
|
4009
|
+
if (config.cloud.token) {
|
|
4010
|
+
restClient = new RestClient({
|
|
4011
|
+
baseUrl: config.cloud.url,
|
|
4012
|
+
apiKey: config.cloud.token,
|
|
4013
|
+
daemonId,
|
|
4014
|
+
port: config.daemon.port,
|
|
4015
|
+
pollIntervalMs: 3e4
|
|
4016
|
+
});
|
|
4017
|
+
restClient.onJobsReceived = (jobs) => {
|
|
4018
|
+
for (const job of jobs) {
|
|
4019
|
+
if (!runners.has(job.id)) {
|
|
4020
|
+
const adapterName = job.preferredAgent ?? "claude-code";
|
|
4021
|
+
const adapter = adapterManager.get(adapterName);
|
|
4022
|
+
if (!adapter) {
|
|
4023
|
+
logger.error("No adapter available for REST job", { jobId: job.id, agent: adapterName });
|
|
4024
|
+
continue;
|
|
4025
|
+
}
|
|
4026
|
+
const jobPolicy = new LocalPolicyEngine(cachedPolicy ?? DEFAULT_POLICY_SNAPSHOT);
|
|
4027
|
+
const runId = ulid3();
|
|
4028
|
+
const runner = new JobRunner(
|
|
4029
|
+
{
|
|
4030
|
+
jobId: job.id,
|
|
4031
|
+
outcomeId: job.outcomeId,
|
|
4032
|
+
runId,
|
|
4033
|
+
daemonId,
|
|
4034
|
+
adapter,
|
|
4035
|
+
policy: jobPolicy,
|
|
4036
|
+
prompt: job.prompt,
|
|
4037
|
+
workingDirectory: job.workingDirectory
|
|
4038
|
+
},
|
|
4039
|
+
{
|
|
4040
|
+
onCheckpoint: (checkpoint) => {
|
|
4041
|
+
store.saveCheckpoint({ ...checkpoint, jobId: job.id });
|
|
4042
|
+
logger.debug("Checkpoint saved", { jobId: job.id, checkpointId: checkpoint.id });
|
|
4043
|
+
},
|
|
4044
|
+
onRunEvent: (event) => {
|
|
4045
|
+
store.appendEvent({ ...event, jobId: job.id });
|
|
4046
|
+
void restClient.postEvents([event]);
|
|
4047
|
+
},
|
|
4048
|
+
onEscalation: (request) => {
|
|
4049
|
+
logger.info("Escalation raised (REST)", { jobId: job.id, type: request.escalationType });
|
|
4050
|
+
},
|
|
4051
|
+
onSpawned: (pid) => {
|
|
4052
|
+
store.trackPid(pid, job.id, adapterName);
|
|
4053
|
+
logger.debug("Tracked PID", { pid, jobId: job.id });
|
|
4054
|
+
},
|
|
4055
|
+
onComplete: (success) => {
|
|
4056
|
+
logger.info("Job completed (REST)", { jobId: job.id, success });
|
|
4057
|
+
store.updateJobState(job.id, "completed", "{}", success ? "completed" : "failed");
|
|
4058
|
+
const proc = runner.getProcess();
|
|
4059
|
+
if (proc?.pid) store.removePid(proc.pid);
|
|
4060
|
+
runners.delete(job.id);
|
|
4061
|
+
const nextJobStatus = job.scheduleType === "cron" ? "active" : job.scheduleType === "event" ? "paused" : "completed";
|
|
4062
|
+
void restClient.updateJobStatus(job.id, nextJobStatus);
|
|
4063
|
+
void restClient.updateRun(runId, {
|
|
4064
|
+
status: success ? "completed" : "failed",
|
|
4065
|
+
exitReason: success ? "completed" : "failed",
|
|
4066
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4067
|
+
summary: runner.getSessionSummary()
|
|
4068
|
+
});
|
|
4069
|
+
void restClient.triggerSupervisorEvaluation(job.outcomeId);
|
|
4070
|
+
},
|
|
4071
|
+
onStateChange: (state) => {
|
|
4072
|
+
logger.debug("Job state change (REST)", { jobId: job.id, state });
|
|
4073
|
+
},
|
|
4074
|
+
onHealthChange: (health) => {
|
|
4075
|
+
logger.debug("Session health (REST)", { jobId: job.id, status: health.status, summary: health.summary });
|
|
4076
|
+
void restClient.postHealth(health);
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
);
|
|
4080
|
+
void restClient.updateJobStatus(job.id, "running");
|
|
4081
|
+
void restClient.createRun({
|
|
4082
|
+
runId,
|
|
4083
|
+
jobId: job.id,
|
|
4084
|
+
daemonId,
|
|
4085
|
+
agentUsed: adapterName
|
|
4086
|
+
});
|
|
4087
|
+
runners.set(job.id, runner);
|
|
4088
|
+
void runner.start();
|
|
4089
|
+
logger.info("Job runner started (REST)", { jobId: job.id, agent: adapterName });
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
};
|
|
4093
|
+
restClient.onJobCancelled = (jobId) => {
|
|
4094
|
+
const runner = runners.get(jobId);
|
|
4095
|
+
if (runner) {
|
|
4096
|
+
logger.info("Job cancelled from dashboard \u2014 stopping", { jobId });
|
|
4097
|
+
runner.stop();
|
|
4098
|
+
const proc = runner.getProcess();
|
|
4099
|
+
if (proc?.pid) store.removePid(proc.pid);
|
|
4100
|
+
runners.delete(jobId);
|
|
4101
|
+
}
|
|
4102
|
+
};
|
|
4103
|
+
restClient.onError = (error) => {
|
|
4104
|
+
logger.error("REST client error", { error: error.message });
|
|
4105
|
+
};
|
|
4106
|
+
deps.restClient = restClient;
|
|
4107
|
+
restClient.start();
|
|
4108
|
+
cancelCheckInterval = setInterval(() => {
|
|
4109
|
+
if (runners.size > 0) {
|
|
4110
|
+
void restClient.checkForCancelledJobs([...runners.keys()]);
|
|
4111
|
+
}
|
|
4112
|
+
}, 3e4);
|
|
4113
|
+
void restClient.resetOrphanedJobs();
|
|
4114
|
+
logger.info("Connected to cloud via REST polling", { url: config.cloud.url });
|
|
4115
|
+
} else {
|
|
4116
|
+
logger.warn("No cloud token configured. Running in offline mode. Set cloud.token in config.");
|
|
4117
|
+
}
|
|
4118
|
+
const sessionMonitor = new DaemonSessionHealthMonitor({
|
|
4119
|
+
store,
|
|
4120
|
+
restClient,
|
|
4121
|
+
runners,
|
|
4122
|
+
logger
|
|
4123
|
+
});
|
|
4124
|
+
sessionMonitor.start();
|
|
4125
|
+
const httpServer = createDaemonServer({
|
|
4126
|
+
port: config.daemon.port,
|
|
4127
|
+
apiToken: config.cloud.token,
|
|
4128
|
+
logger
|
|
4129
|
+
});
|
|
4130
|
+
logger.info("Cronies daemon started", {
|
|
4131
|
+
pid: process.pid,
|
|
4132
|
+
dataDir,
|
|
4133
|
+
adapters: available,
|
|
4134
|
+
activeJobs: activeJobs.length,
|
|
4135
|
+
cloudConnected: !!syncClient,
|
|
4136
|
+
restPolling: !!restClient,
|
|
4137
|
+
httpPort: config.daemon.port
|
|
4138
|
+
});
|
|
4139
|
+
const shutdown = async (signal) => {
|
|
4140
|
+
logger.info("Shutting down...", { signal });
|
|
4141
|
+
for (const [jobId, runner] of runners) {
|
|
4142
|
+
try {
|
|
4143
|
+
logger.info("Checkpointing job", { jobId });
|
|
4144
|
+
runner.stop();
|
|
4145
|
+
} catch (err) {
|
|
4146
|
+
logger.error("Failed to stop runner", { jobId, error: String(err) });
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
runners.clear();
|
|
4150
|
+
sessionMonitor.stop();
|
|
4151
|
+
if (cancelCheckInterval) clearInterval(cancelCheckInterval);
|
|
4152
|
+
if (restClient) {
|
|
4153
|
+
restClient.stop();
|
|
4154
|
+
}
|
|
4155
|
+
store.clearAllPids();
|
|
4156
|
+
httpServer.close();
|
|
4157
|
+
store.close();
|
|
4158
|
+
removePidFile(dataDir);
|
|
4159
|
+
logger.info("Daemon stopped");
|
|
4160
|
+
process.exit(0);
|
|
4161
|
+
};
|
|
4162
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
4163
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
4164
|
+
await new Promise(() => {
|
|
4165
|
+
});
|
|
4166
|
+
}
|
|
4167
|
+
|
|
4168
|
+
// src/logger.ts
|
|
4169
|
+
var LEVEL_ORDER = {
|
|
4170
|
+
debug: 0,
|
|
4171
|
+
info: 1,
|
|
4172
|
+
warn: 2,
|
|
4173
|
+
error: 3
|
|
4174
|
+
};
|
|
4175
|
+
var LEVEL_LABEL = {
|
|
4176
|
+
debug: "DEBUG",
|
|
4177
|
+
info: "INFO ",
|
|
4178
|
+
warn: "WARN ",
|
|
4179
|
+
error: "ERROR"
|
|
4180
|
+
};
|
|
4181
|
+
function formatLine(level, msg, data) {
|
|
4182
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4183
|
+
const suffix = data && Object.keys(data).length > 0 ? ` ${JSON.stringify(data)}` : "";
|
|
4184
|
+
return `[${timestamp}] [${LEVEL_LABEL[level]}] ${msg}${suffix}`;
|
|
4185
|
+
}
|
|
4186
|
+
function createLogger(level) {
|
|
4187
|
+
const threshold = LEVEL_ORDER[level];
|
|
4188
|
+
function log(msgLevel, msg, data) {
|
|
4189
|
+
if (LEVEL_ORDER[msgLevel] >= threshold) {
|
|
4190
|
+
process.stderr.write(formatLine(msgLevel, msg, data) + "\n");
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
return {
|
|
4194
|
+
debug: (msg, data) => log("debug", msg, data),
|
|
4195
|
+
info: (msg, data) => log("info", msg, data),
|
|
4196
|
+
warn: (msg, data) => log("warn", msg, data),
|
|
4197
|
+
error: (msg, data) => log("error", msg, data)
|
|
4198
|
+
};
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4201
|
+
export {
|
|
4202
|
+
DaemonConfigSchema,
|
|
4203
|
+
loadConfig,
|
|
4204
|
+
resolveDataDir,
|
|
4205
|
+
getDefaultConfigPath,
|
|
4206
|
+
writeConfigToken,
|
|
4207
|
+
writePidFile,
|
|
4208
|
+
readPidFile,
|
|
4209
|
+
removePidFile,
|
|
4210
|
+
isDaemonRunning,
|
|
4211
|
+
createDatabase,
|
|
4212
|
+
DaemonStore,
|
|
4213
|
+
ClaudeCodeAdapter,
|
|
4214
|
+
CodexCliAdapter,
|
|
4215
|
+
GeminiCliAdapter,
|
|
4216
|
+
CursorAgentAdapter,
|
|
4217
|
+
createAdapterManager,
|
|
4218
|
+
LocalPolicyEngine,
|
|
4219
|
+
PROTOCOL_VERSION,
|
|
4220
|
+
createEnvelope,
|
|
4221
|
+
encodeMessage,
|
|
4222
|
+
decodeMessage,
|
|
4223
|
+
SyncQueue,
|
|
4224
|
+
SyncReconciler,
|
|
4225
|
+
SessionHealthMonitor,
|
|
4226
|
+
JobRunner,
|
|
4227
|
+
startDaemon,
|
|
4228
|
+
createLogger
|
|
4229
|
+
};
|