@neotx/core 0.1.0-alpha.21 → 0.1.0-alpha.22
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/dist/index.d.ts +65 -4
- package/dist/index.js +560 -202
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -60,8 +60,10 @@ ${issues}`);
|
|
|
60
60
|
const promptPath = path.resolve(path.dirname(filePath), config.prompt);
|
|
61
61
|
try {
|
|
62
62
|
config.prompt = await readFile(promptPath, "utf-8");
|
|
63
|
-
} catch {
|
|
64
|
-
throw new Error(
|
|
63
|
+
} catch (err) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Prompt file not found: ${promptPath} (referenced in ${filePath}). Error: ${err instanceof Error ? err.message : String(err)}`
|
|
66
|
+
);
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
return config;
|
|
@@ -198,7 +200,10 @@ var AgentRegistry = class {
|
|
|
198
200
|
let customConfigs;
|
|
199
201
|
try {
|
|
200
202
|
customConfigs = await loadAgentsFromDir(this.customDir);
|
|
201
|
-
} catch {
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.debug(
|
|
205
|
+
`[registry] Custom agents dir not found: ${err instanceof Error ? err.message : String(err)}`
|
|
206
|
+
);
|
|
202
207
|
customConfigs = [];
|
|
203
208
|
}
|
|
204
209
|
for (const config of customConfigs) {
|
|
@@ -505,6 +510,15 @@ var sessionsConfigSchema = z2.object({
|
|
|
505
510
|
maxDurationMs: z2.number().default(36e5),
|
|
506
511
|
dir: z2.string().default("/tmp/neo-sessions")
|
|
507
512
|
}).default({ initTimeoutMs: 12e4, maxDurationMs: 36e5, dir: "/tmp/neo-sessions" });
|
|
513
|
+
var journalConfigSchema = z2.object({
|
|
514
|
+
maxCostJournalSizeBytes: z2.number().default(100 * 1024 * 1024),
|
|
515
|
+
// 100MB
|
|
516
|
+
maxEventJournalSizeBytes: z2.number().default(500 * 1024 * 1024)
|
|
517
|
+
// 500MB
|
|
518
|
+
}).default({
|
|
519
|
+
maxCostJournalSizeBytes: 100 * 1024 * 1024,
|
|
520
|
+
maxEventJournalSizeBytes: 500 * 1024 * 1024
|
|
521
|
+
});
|
|
508
522
|
var supervisorConfigSchema = z2.object({
|
|
509
523
|
port: z2.number().default(7777),
|
|
510
524
|
secret: z2.string().optional(),
|
|
@@ -544,6 +558,7 @@ var globalConfigSchema = z2.object({
|
|
|
544
558
|
budget: budgetConfigSchema,
|
|
545
559
|
recovery: recoveryConfigSchema,
|
|
546
560
|
sessions: sessionsConfigSchema,
|
|
561
|
+
journal: journalConfigSchema.optional(),
|
|
547
562
|
webhooks: z2.array(
|
|
548
563
|
z2.object({
|
|
549
564
|
url: z2.string().url(),
|
|
@@ -598,6 +613,7 @@ var defaultConfig = {
|
|
|
598
613
|
budget: budgetConfigSchema.parse(void 0),
|
|
599
614
|
recovery: recoveryConfigSchema.parse(void 0),
|
|
600
615
|
sessions: sessionsConfigSchema.parse(void 0),
|
|
616
|
+
journal: journalConfigSchema.parse(void 0),
|
|
601
617
|
webhooks: [],
|
|
602
618
|
supervisor: {
|
|
603
619
|
port: 7777,
|
|
@@ -727,6 +743,7 @@ var ConfigStore = class {
|
|
|
727
743
|
}
|
|
728
744
|
const parsed = neoConfigSchema.safeParse(raw);
|
|
729
745
|
if (!parsed.success) {
|
|
746
|
+
console.warn(`[neo] Failed to parse config at ${globalPath}:`, parsed.error.message);
|
|
730
747
|
return null;
|
|
731
748
|
}
|
|
732
749
|
return parsed.data;
|
|
@@ -745,6 +762,7 @@ var ConfigStore = class {
|
|
|
745
762
|
}
|
|
746
763
|
const parsed = repoOverrideConfigSchema.safeParse(raw);
|
|
747
764
|
if (!parsed.success) {
|
|
765
|
+
console.warn(`[neo] Failed to parse config at ${repoConfigPath}:`, parsed.error.message);
|
|
748
766
|
return null;
|
|
749
767
|
}
|
|
750
768
|
return parsed.data;
|
|
@@ -844,6 +862,9 @@ var ConfigWatcher = class extends EventEmitter {
|
|
|
844
862
|
this.debounceTimer = null;
|
|
845
863
|
this.reloadConfig();
|
|
846
864
|
}, this.debounceMs);
|
|
865
|
+
if (typeof this.debounceTimer === "object" && "unref" in this.debounceTimer) {
|
|
866
|
+
this.debounceTimer.unref();
|
|
867
|
+
}
|
|
847
868
|
}
|
|
848
869
|
/**
|
|
849
870
|
* Reloads the config and emits 'change' event.
|
|
@@ -857,6 +878,51 @@ var ConfigWatcher = class extends EventEmitter {
|
|
|
857
878
|
}
|
|
858
879
|
};
|
|
859
880
|
|
|
881
|
+
// src/config/parser.ts
|
|
882
|
+
var NEO_CONFIG_KEYS = {
|
|
883
|
+
"": /* @__PURE__ */ new Set([
|
|
884
|
+
"repos",
|
|
885
|
+
"concurrency",
|
|
886
|
+
"budget",
|
|
887
|
+
"recovery",
|
|
888
|
+
"sessions",
|
|
889
|
+
"webhooks",
|
|
890
|
+
"supervisor",
|
|
891
|
+
"memory",
|
|
892
|
+
"mcpServers",
|
|
893
|
+
"claudeCodePath",
|
|
894
|
+
"idempotency"
|
|
895
|
+
]),
|
|
896
|
+
concurrency: /* @__PURE__ */ new Set(["maxSessions", "maxPerRepo", "queueMax"]),
|
|
897
|
+
budget: /* @__PURE__ */ new Set(["dailyCapUsd", "alertThresholdPct"]),
|
|
898
|
+
recovery: /* @__PURE__ */ new Set(["maxRetries", "backoffBaseMs"]),
|
|
899
|
+
sessions: /* @__PURE__ */ new Set(["initTimeoutMs", "maxDurationMs", "dir"]),
|
|
900
|
+
supervisor: /* @__PURE__ */ new Set([
|
|
901
|
+
"port",
|
|
902
|
+
"secret",
|
|
903
|
+
"heartbeatTimeoutMs",
|
|
904
|
+
"maxConsecutiveFailures",
|
|
905
|
+
"maxEventsPerSec",
|
|
906
|
+
"dailyCapUsd",
|
|
907
|
+
"consolidationIntervalMs",
|
|
908
|
+
"compactionIntervalMs",
|
|
909
|
+
"eventTimeoutMs",
|
|
910
|
+
"instructions",
|
|
911
|
+
"idleSkipMax",
|
|
912
|
+
"activeWorkSkipMax",
|
|
913
|
+
"autoDecide"
|
|
914
|
+
]),
|
|
915
|
+
memory: /* @__PURE__ */ new Set(["embeddings"]),
|
|
916
|
+
idempotency: /* @__PURE__ */ new Set(["enabled", "key", "ttlMs"])
|
|
917
|
+
};
|
|
918
|
+
var REPO_OVERRIDE_KEYS = {
|
|
919
|
+
"": /* @__PURE__ */ new Set(["concurrency", "budget", "recovery", "sessions"]),
|
|
920
|
+
concurrency: NEO_CONFIG_KEYS.concurrency,
|
|
921
|
+
budget: NEO_CONFIG_KEYS.budget,
|
|
922
|
+
recovery: NEO_CONFIG_KEYS.recovery,
|
|
923
|
+
sessions: NEO_CONFIG_KEYS.sessions
|
|
924
|
+
};
|
|
925
|
+
|
|
860
926
|
// src/config.ts
|
|
861
927
|
var DEFAULT_GLOBAL_CONFIG = {
|
|
862
928
|
repos: [],
|
|
@@ -888,8 +954,10 @@ async function loadConfig(configPath) {
|
|
|
888
954
|
let raw;
|
|
889
955
|
try {
|
|
890
956
|
raw = await readFile2(configPath, "utf-8");
|
|
891
|
-
} catch {
|
|
892
|
-
throw new Error(
|
|
957
|
+
} catch (err) {
|
|
958
|
+
throw new Error(
|
|
959
|
+
`Config file not found: ${configPath}. Run 'neo init' to get started. (${err instanceof Error ? err.message : String(err)})`
|
|
960
|
+
);
|
|
893
961
|
}
|
|
894
962
|
const parsed = parseYamlFile(raw, configPath);
|
|
895
963
|
const result = neoConfigSchema.safeParse(parsed);
|
|
@@ -944,7 +1012,9 @@ async function listReposFromGlobalConfig() {
|
|
|
944
1012
|
}
|
|
945
1013
|
|
|
946
1014
|
// src/cost/journal.ts
|
|
947
|
-
import {
|
|
1015
|
+
import { createReadStream } from "fs";
|
|
1016
|
+
import { appendFile, stat } from "fs/promises";
|
|
1017
|
+
import { createInterface } from "readline";
|
|
948
1018
|
|
|
949
1019
|
// src/shared/date.ts
|
|
950
1020
|
import path5 from "path";
|
|
@@ -968,12 +1038,25 @@ async function ensureDir(dirPath, cache) {
|
|
|
968
1038
|
}
|
|
969
1039
|
|
|
970
1040
|
// src/cost/journal.ts
|
|
1041
|
+
var JournalFileSizeError = class extends Error {
|
|
1042
|
+
constructor(filePath, fileSizeBytes, maxSizeBytes) {
|
|
1043
|
+
super(
|
|
1044
|
+
`Journal file exceeds maximum size: ${filePath} (${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB > ${(maxSizeBytes / 1024 / 1024).toFixed(2)}MB)`
|
|
1045
|
+
);
|
|
1046
|
+
this.filePath = filePath;
|
|
1047
|
+
this.fileSizeBytes = fileSizeBytes;
|
|
1048
|
+
this.maxSizeBytes = maxSizeBytes;
|
|
1049
|
+
this.name = "JournalFileSizeError";
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
971
1052
|
var CostJournal = class {
|
|
972
1053
|
dir;
|
|
973
1054
|
dirCache = /* @__PURE__ */ new Set();
|
|
974
1055
|
dayCache = null;
|
|
1056
|
+
maxFileSizeBytes;
|
|
975
1057
|
constructor(options) {
|
|
976
1058
|
this.dir = options.dir;
|
|
1059
|
+
this.maxFileSizeBytes = options.maxFileSizeBytes ?? 100 * 1024 * 1024;
|
|
977
1060
|
}
|
|
978
1061
|
async append(entry) {
|
|
979
1062
|
await ensureDir(this.dir, this.dirCache);
|
|
@@ -991,8 +1074,13 @@ var CostJournal = class {
|
|
|
991
1074
|
const file = fileForDate(d, "cost", this.dir);
|
|
992
1075
|
let total = 0;
|
|
993
1076
|
try {
|
|
994
|
-
const
|
|
995
|
-
|
|
1077
|
+
const stats = await stat(file);
|
|
1078
|
+
if (stats.size > this.maxFileSizeBytes) {
|
|
1079
|
+
throw new JournalFileSizeError(file, stats.size, this.maxFileSizeBytes);
|
|
1080
|
+
}
|
|
1081
|
+
const stream = createReadStream(file, { encoding: "utf-8" });
|
|
1082
|
+
const rl = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
1083
|
+
for await (const line of rl) {
|
|
996
1084
|
if (!line.trim()) continue;
|
|
997
1085
|
const entry = JSON.parse(line);
|
|
998
1086
|
if (toDateKey(new Date(entry.timestamp)) === dayKey) {
|
|
@@ -1038,7 +1126,11 @@ var NeoEventEmitter = class {
|
|
|
1038
1126
|
if (eventType !== "error") {
|
|
1039
1127
|
try {
|
|
1040
1128
|
this.emitter.emit("error", error);
|
|
1041
|
-
} catch {
|
|
1129
|
+
} catch (nestedErr) {
|
|
1130
|
+
console.error(
|
|
1131
|
+
"[emitter] Error handler threw:",
|
|
1132
|
+
nestedErr instanceof Error ? nestedErr.message : String(nestedErr)
|
|
1133
|
+
);
|
|
1042
1134
|
}
|
|
1043
1135
|
}
|
|
1044
1136
|
}
|
|
@@ -1046,16 +1138,37 @@ var NeoEventEmitter = class {
|
|
|
1046
1138
|
};
|
|
1047
1139
|
|
|
1048
1140
|
// src/events/journal.ts
|
|
1049
|
-
import { appendFile as appendFile2 } from "fs/promises";
|
|
1141
|
+
import { appendFile as appendFile2, stat as stat2 } from "fs/promises";
|
|
1142
|
+
var JournalFileSizeError2 = class extends Error {
|
|
1143
|
+
constructor(filePath, fileSizeBytes, maxSizeBytes) {
|
|
1144
|
+
super(
|
|
1145
|
+
`Journal file exceeds maximum size: ${filePath} (${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB > ${(maxSizeBytes / 1024 / 1024).toFixed(2)}MB)`
|
|
1146
|
+
);
|
|
1147
|
+
this.filePath = filePath;
|
|
1148
|
+
this.fileSizeBytes = fileSizeBytes;
|
|
1149
|
+
this.maxSizeBytes = maxSizeBytes;
|
|
1150
|
+
this.name = "JournalFileSizeError";
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1050
1153
|
var EventJournal = class {
|
|
1051
1154
|
dir;
|
|
1052
1155
|
dirCache = /* @__PURE__ */ new Set();
|
|
1156
|
+
maxFileSizeBytes;
|
|
1053
1157
|
constructor(options) {
|
|
1054
1158
|
this.dir = options.dir;
|
|
1159
|
+
this.maxFileSizeBytes = options.maxFileSizeBytes ?? 500 * 1024 * 1024;
|
|
1055
1160
|
}
|
|
1056
1161
|
async append(event) {
|
|
1057
1162
|
await ensureDir(this.dir, this.dirCache);
|
|
1058
1163
|
const file = fileForDate(new Date(event.timestamp), "events", this.dir);
|
|
1164
|
+
try {
|
|
1165
|
+
const stats = await stat2(file);
|
|
1166
|
+
if (stats.size > this.maxFileSizeBytes) {
|
|
1167
|
+
throw new JournalFileSizeError2(file, stats.size, this.maxFileSizeBytes);
|
|
1168
|
+
}
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
if (error.code !== "ENOENT") throw error;
|
|
1171
|
+
}
|
|
1059
1172
|
await appendFile2(file, `${JSON.stringify(event)}
|
|
1060
1173
|
`, "utf-8");
|
|
1061
1174
|
}
|
|
@@ -1127,7 +1240,10 @@ async function sendWithRetry(url, headers, body, timeoutMs) {
|
|
|
1127
1240
|
signal: AbortSignal.timeout(timeoutMs)
|
|
1128
1241
|
});
|
|
1129
1242
|
if (res.ok) return;
|
|
1130
|
-
} catch {
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
console.debug(
|
|
1245
|
+
`[webhook] Network error on attempt ${attempt}: ${err instanceof Error ? err.message : String(err)}`
|
|
1246
|
+
);
|
|
1131
1247
|
}
|
|
1132
1248
|
if (attempt < RETRY_MAX_ATTEMPTS) {
|
|
1133
1249
|
const delay = RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
|
|
@@ -1163,14 +1279,38 @@ import { dirname, resolve } from "path";
|
|
|
1163
1279
|
import { promisify } from "util";
|
|
1164
1280
|
var execFileAsync = promisify(execFile);
|
|
1165
1281
|
var GIT_TIMEOUT = 6e4;
|
|
1282
|
+
function validateGitRef(refName, paramName) {
|
|
1283
|
+
if (!refName || typeof refName !== "string") {
|
|
1284
|
+
throw new Error(`${paramName} must be a non-empty string`);
|
|
1285
|
+
}
|
|
1286
|
+
if (refName.includes("..")) {
|
|
1287
|
+
throw new Error(`${paramName} contains invalid pattern '..' (directory traversal)`);
|
|
1288
|
+
}
|
|
1289
|
+
if (refName.startsWith("-")) {
|
|
1290
|
+
throw new Error(`${paramName} cannot start with '-' (option injection)`);
|
|
1291
|
+
}
|
|
1292
|
+
const validRefPattern = /^[a-zA-Z0-9/_+.-]+$/;
|
|
1293
|
+
if (!validRefPattern.test(refName)) {
|
|
1294
|
+
throw new Error(
|
|
1295
|
+
`${paramName} contains invalid characters. Only alphanumeric, dash, underscore, slash, plus, and dot are allowed. Got: ${refName}`
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1166
1299
|
async function createSessionClone(options) {
|
|
1300
|
+
validateGitRef(options.branch, "branch");
|
|
1301
|
+
validateGitRef(options.baseBranch, "baseBranch");
|
|
1167
1302
|
const repoPath = resolve(options.repoPath);
|
|
1168
1303
|
const sessionDir = resolve(options.sessionDir);
|
|
1169
1304
|
await mkdir3(dirname(sessionDir), { recursive: true });
|
|
1170
1305
|
const remoteUrl = await execFileAsync("git", ["config", "--get", "remote.origin.url"], {
|
|
1171
1306
|
cwd: repoPath,
|
|
1172
1307
|
timeout: GIT_TIMEOUT
|
|
1173
|
-
}).then(({ stdout }) => stdout.trim()).catch(() =>
|
|
1308
|
+
}).then(({ stdout }) => stdout.trim()).catch((err) => {
|
|
1309
|
+
console.debug(
|
|
1310
|
+
`[neo] No remote.origin.url for ${repoPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1311
|
+
);
|
|
1312
|
+
return "";
|
|
1313
|
+
});
|
|
1174
1314
|
const cloneSource = remoteUrl || repoPath;
|
|
1175
1315
|
await execFileAsync("git", ["clone", "--branch", options.baseBranch, cloneSource, sessionDir], {
|
|
1176
1316
|
timeout: GIT_TIMEOUT
|
|
@@ -1180,7 +1320,12 @@ async function createSessionClone(options) {
|
|
|
1180
1320
|
"git",
|
|
1181
1321
|
["ls-remote", "--heads", "origin", options.branch],
|
|
1182
1322
|
{ cwd: sessionDir, timeout: GIT_TIMEOUT }
|
|
1183
|
-
).then(({ stdout }) => stdout.trim().length > 0).catch(() =>
|
|
1323
|
+
).then(({ stdout }) => stdout.trim().length > 0).catch((err) => {
|
|
1324
|
+
console.debug(
|
|
1325
|
+
`[neo] ls-remote failed for branch ${options.branch}: ${err instanceof Error ? err.message : String(err)}`
|
|
1326
|
+
);
|
|
1327
|
+
return false;
|
|
1328
|
+
});
|
|
1184
1329
|
if (branchExists) {
|
|
1185
1330
|
await execFileAsync("git", ["fetch", "origin", options.branch], {
|
|
1186
1331
|
cwd: sessionDir,
|
|
@@ -1234,14 +1379,20 @@ async function listSessionClones(sessionsBaseDir) {
|
|
|
1234
1379
|
);
|
|
1235
1380
|
const url = originUrl.trim();
|
|
1236
1381
|
if (url) repoPath = resolve(clonePath, url);
|
|
1237
|
-
} catch {
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
console.debug(
|
|
1384
|
+
`[neo] Failed to get origin URL for ${clonePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1385
|
+
);
|
|
1238
1386
|
}
|
|
1239
1387
|
clones.push({
|
|
1240
1388
|
path: clonePath,
|
|
1241
1389
|
branch: branchOut.trim(),
|
|
1242
1390
|
repoPath
|
|
1243
1391
|
});
|
|
1244
|
-
} catch {
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
console.debug(
|
|
1394
|
+
`[neo] Skipping ${clonePath}, not a valid git repo: ${err instanceof Error ? err.message : String(err)}`
|
|
1395
|
+
);
|
|
1245
1396
|
}
|
|
1246
1397
|
}
|
|
1247
1398
|
return clones;
|
|
@@ -1261,15 +1412,21 @@ async function git(repoPath, args) {
|
|
|
1261
1412
|
return stdout.trim();
|
|
1262
1413
|
}
|
|
1263
1414
|
async function createBranch(repoPath, branch, baseBranch) {
|
|
1415
|
+
validateGitRef(branch, "branch");
|
|
1416
|
+
validateGitRef(baseBranch, "baseBranch");
|
|
1264
1417
|
await git(repoPath, ["branch", branch, baseBranch]);
|
|
1265
1418
|
}
|
|
1266
1419
|
async function pushBranch(repoPath, branch, remote) {
|
|
1420
|
+
validateGitRef(branch, "branch");
|
|
1421
|
+
validateGitRef(remote, "remote");
|
|
1267
1422
|
await git(repoPath, ["push", remote, branch]);
|
|
1268
1423
|
}
|
|
1269
1424
|
async function fetchRemote(repoPath, remote) {
|
|
1425
|
+
validateGitRef(remote, "remote");
|
|
1270
1426
|
await git(repoPath, ["fetch", remote]);
|
|
1271
1427
|
}
|
|
1272
1428
|
async function deleteBranch(repoPath, branch) {
|
|
1429
|
+
validateGitRef(branch, "branch");
|
|
1273
1430
|
await git(repoPath, ["branch", "-D", branch]);
|
|
1274
1431
|
}
|
|
1275
1432
|
async function getCurrentBranch(repoPath) {
|
|
@@ -1282,6 +1439,8 @@ function getBranchName(config, runId, branch) {
|
|
|
1282
1439
|
return `${prefix}/run-${sanitized}`;
|
|
1283
1440
|
}
|
|
1284
1441
|
async function pushSessionBranch(sessionPath, branch, remote) {
|
|
1442
|
+
validateGitRef(branch, "branch");
|
|
1443
|
+
validateGitRef(remote, "remote");
|
|
1285
1444
|
await git(sessionPath, ["push", "-u", remote, branch]);
|
|
1286
1445
|
}
|
|
1287
1446
|
|
|
@@ -1343,15 +1502,21 @@ function auditLog(options) {
|
|
|
1343
1502
|
await appendFile3(filePath, lines.join(""), "utf-8");
|
|
1344
1503
|
buffers.delete(sessionId);
|
|
1345
1504
|
}
|
|
1505
|
+
function stopTimer() {
|
|
1506
|
+
if (flushTimer !== void 0) {
|
|
1507
|
+
clearInterval(flushTimer);
|
|
1508
|
+
flushTimer = void 0;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1346
1511
|
return {
|
|
1347
1512
|
name: "audit-log",
|
|
1348
1513
|
on: "PostToolUse",
|
|
1349
1514
|
async flush() {
|
|
1515
|
+
stopTimer();
|
|
1350
1516
|
await flushAll();
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
}
|
|
1517
|
+
},
|
|
1518
|
+
cleanup() {
|
|
1519
|
+
stopTimer();
|
|
1355
1520
|
},
|
|
1356
1521
|
async handler(event, context) {
|
|
1357
1522
|
const entry = {
|
|
@@ -1506,12 +1671,12 @@ function loopDetection(options) {
|
|
|
1506
1671
|
// src/orchestrator.ts
|
|
1507
1672
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1508
1673
|
import { existsSync as existsSync6 } from "fs";
|
|
1509
|
-
import { mkdir as mkdir6, readFile as
|
|
1674
|
+
import { mkdir as mkdir6, readFile as readFile5 } from "fs/promises";
|
|
1510
1675
|
import path10 from "path";
|
|
1511
1676
|
|
|
1512
1677
|
// src/orchestrator/run-store.ts
|
|
1513
1678
|
import { existsSync as existsSync4 } from "fs";
|
|
1514
|
-
import { mkdir as mkdir5, readdir as readdir3, readFile as
|
|
1679
|
+
import { mkdir as mkdir5, readdir as readdir3, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
1515
1680
|
import path7 from "path";
|
|
1516
1681
|
|
|
1517
1682
|
// src/shared/process.ts
|
|
@@ -1597,7 +1762,7 @@ var RunStore = class {
|
|
|
1597
1762
|
* If so, update its status to "failed" and return it.
|
|
1598
1763
|
*/
|
|
1599
1764
|
async recoverRunIfOrphaned(filePath) {
|
|
1600
|
-
const content = await
|
|
1765
|
+
const content = await readFile3(filePath, "utf-8");
|
|
1601
1766
|
const run = JSON.parse(content);
|
|
1602
1767
|
if (run.status !== "running") return null;
|
|
1603
1768
|
if (run.pid && run.pid === process.pid) return null;
|
|
@@ -1612,13 +1777,13 @@ var RunStore = class {
|
|
|
1612
1777
|
};
|
|
1613
1778
|
|
|
1614
1779
|
// src/orchestrator/prompt-builder.ts
|
|
1615
|
-
import { readFile as
|
|
1780
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1616
1781
|
import path8 from "path";
|
|
1617
1782
|
var INSTRUCTIONS_PATH = ".neo/INSTRUCTIONS.md";
|
|
1618
1783
|
async function loadRepoInstructions(repoPath) {
|
|
1619
1784
|
const filePath = path8.join(repoPath, INSTRUCTIONS_PATH);
|
|
1620
1785
|
try {
|
|
1621
|
-
return await
|
|
1786
|
+
return await readFile4(filePath, "utf-8");
|
|
1622
1787
|
} catch {
|
|
1623
1788
|
return void 0;
|
|
1624
1789
|
}
|
|
@@ -2684,6 +2849,7 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2684
2849
|
return {
|
|
2685
2850
|
paused: this._paused,
|
|
2686
2851
|
activeSessions: [...this._activeSessions.values()],
|
|
2852
|
+
activeRunCount: this.activeRunCount,
|
|
2687
2853
|
queueDepth: this.semaphore.queueDepth(),
|
|
2688
2854
|
costToday: this._costToday,
|
|
2689
2855
|
budgetCapUsd: this.config.budget.dailyCapUsd,
|
|
@@ -2694,6 +2860,15 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2694
2860
|
get activeSessions() {
|
|
2695
2861
|
return [...this._activeSessions.values()];
|
|
2696
2862
|
}
|
|
2863
|
+
get activeRunCount() {
|
|
2864
|
+
let count = 0;
|
|
2865
|
+
for (const session of this._activeSessions.values()) {
|
|
2866
|
+
if (session.status === "running") {
|
|
2867
|
+
count++;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
return count;
|
|
2871
|
+
}
|
|
2697
2872
|
// ─── Lifecycle ─────────────────────────────────────────
|
|
2698
2873
|
async start() {
|
|
2699
2874
|
this._startedAt = Date.now();
|
|
@@ -2865,7 +3040,10 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2865
3040
|
outcome: "failure",
|
|
2866
3041
|
runId
|
|
2867
3042
|
});
|
|
2868
|
-
} catch {
|
|
3043
|
+
} catch (err) {
|
|
3044
|
+
console.debug(
|
|
3045
|
+
`[orchestrator] Failed to write failure episode to memory: ${err instanceof Error ? err.message : String(err)}`
|
|
3046
|
+
);
|
|
2869
3047
|
}
|
|
2870
3048
|
return failResult;
|
|
2871
3049
|
} finally {
|
|
@@ -2895,14 +3073,17 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2895
3073
|
const branch = ctx.input.branch;
|
|
2896
3074
|
const remote = ctx.repoConfig.pushRemote ?? "origin";
|
|
2897
3075
|
try {
|
|
2898
|
-
await pushSessionBranch(sessionPath, branch, remote).catch(() => {
|
|
3076
|
+
await pushSessionBranch(sessionPath, branch, remote).catch((err) => {
|
|
3077
|
+
console.debug("[neo] Push failed:", err);
|
|
2899
3078
|
});
|
|
2900
|
-
} catch {
|
|
3079
|
+
} catch (err) {
|
|
3080
|
+
console.debug("[neo] Finalization error:", err);
|
|
2901
3081
|
}
|
|
2902
3082
|
}
|
|
2903
3083
|
try {
|
|
2904
3084
|
await removeSessionClone(sessionPath);
|
|
2905
|
-
} catch {
|
|
3085
|
+
} catch (err) {
|
|
3086
|
+
console.debug("[neo] Session cleanup failed:", err);
|
|
2906
3087
|
}
|
|
2907
3088
|
}
|
|
2908
3089
|
async runAgentSession(ctx, sessionPath) {
|
|
@@ -2979,7 +3160,10 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2979
3160
|
outcome: isSuccess ? "success" : "failure",
|
|
2980
3161
|
runId
|
|
2981
3162
|
});
|
|
2982
|
-
} catch {
|
|
3163
|
+
} catch (err) {
|
|
3164
|
+
console.debug(
|
|
3165
|
+
`[orchestrator] Failed to write completion episode to memory: ${err instanceof Error ? err.message : String(err)}`
|
|
3166
|
+
);
|
|
2983
3167
|
}
|
|
2984
3168
|
return result;
|
|
2985
3169
|
}
|
|
@@ -3046,7 +3230,10 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
3046
3230
|
if (memories.length === 0) return void 0;
|
|
3047
3231
|
store.markAccessed(memories.map((m) => m.id));
|
|
3048
3232
|
return formatMemoriesForPrompt(memories);
|
|
3049
|
-
} catch {
|
|
3233
|
+
} catch (err) {
|
|
3234
|
+
console.debug(
|
|
3235
|
+
`[orchestrator] Failed to load memories: ${err instanceof Error ? err.message : String(err)}`
|
|
3236
|
+
);
|
|
3050
3237
|
return void 0;
|
|
3051
3238
|
}
|
|
3052
3239
|
}
|
|
@@ -3197,17 +3384,17 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
3197
3384
|
// ─── Private: Supervisor discovery ─────────────────────
|
|
3198
3385
|
/** Discover running supervisor daemons and return webhook configs for their endpoints. */
|
|
3199
3386
|
async discoverSupervisorWebhooks() {
|
|
3200
|
-
const { readdir:
|
|
3387
|
+
const { readdir: readdir7 } = await import("fs/promises");
|
|
3201
3388
|
const supervisorsDir = getSupervisorsDir();
|
|
3202
3389
|
if (!existsSync6(supervisorsDir)) return [];
|
|
3203
3390
|
const webhooks = [];
|
|
3204
3391
|
try {
|
|
3205
|
-
const entries = await
|
|
3392
|
+
const entries = await readdir7(supervisorsDir, { withFileTypes: true });
|
|
3206
3393
|
for (const entry of entries) {
|
|
3207
3394
|
if (!entry.isDirectory()) continue;
|
|
3208
3395
|
try {
|
|
3209
3396
|
const statePath = path10.join(supervisorsDir, entry.name, "state.json");
|
|
3210
|
-
const raw = await
|
|
3397
|
+
const raw = await readFile5(statePath, "utf-8");
|
|
3211
3398
|
const state = JSON.parse(raw);
|
|
3212
3399
|
if (state.status !== "running" || !state.port) continue;
|
|
3213
3400
|
if (state.pid && !isProcessAlive(state.pid)) continue;
|
|
@@ -3217,10 +3404,16 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
3217
3404
|
secret: this.config.supervisor.secret,
|
|
3218
3405
|
timeoutMs: 5e3
|
|
3219
3406
|
});
|
|
3220
|
-
} catch {
|
|
3407
|
+
} catch (err) {
|
|
3408
|
+
console.debug(
|
|
3409
|
+
`[orchestrator] Failed to load supervisor webhook config: ${err instanceof Error ? err.message : String(err)}`
|
|
3410
|
+
);
|
|
3221
3411
|
}
|
|
3222
3412
|
}
|
|
3223
|
-
} catch {
|
|
3413
|
+
} catch (err) {
|
|
3414
|
+
console.debug(
|
|
3415
|
+
`[orchestrator] Failed to read supervisors directory: ${err instanceof Error ? err.message : String(err)}`
|
|
3416
|
+
);
|
|
3224
3417
|
}
|
|
3225
3418
|
return webhooks;
|
|
3226
3419
|
}
|
|
@@ -3263,7 +3456,7 @@ import { z as z5 } from "zod";
|
|
|
3263
3456
|
|
|
3264
3457
|
// src/supervisor/decisions.ts
|
|
3265
3458
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3266
|
-
import { appendFile as appendFile4, readFile as
|
|
3459
|
+
import { appendFile as appendFile4, readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
3267
3460
|
import path11 from "path";
|
|
3268
3461
|
import { z as z4 } from "zod";
|
|
3269
3462
|
var decisionOptionSchema = z4.object({
|
|
@@ -3279,21 +3472,41 @@ var decisionSchema = z4.object({
|
|
|
3279
3472
|
type: z4.string().default("generic"),
|
|
3280
3473
|
source: z4.string(),
|
|
3281
3474
|
metadata: z4.record(z4.string(), z4.unknown()).optional(),
|
|
3282
|
-
createdAt: z4.string(),
|
|
3283
|
-
expiresAt: z4.string().optional(),
|
|
3475
|
+
createdAt: z4.coerce.string(),
|
|
3476
|
+
expiresAt: z4.coerce.string().optional(),
|
|
3284
3477
|
defaultAnswer: z4.string().optional(),
|
|
3285
|
-
answeredAt: z4.string().optional(),
|
|
3478
|
+
answeredAt: z4.coerce.string().optional(),
|
|
3286
3479
|
answer: z4.string().optional(),
|
|
3287
|
-
expiredAt: z4.string().optional()
|
|
3480
|
+
expiredAt: z4.coerce.string().optional()
|
|
3288
3481
|
});
|
|
3289
3482
|
var DecisionStore = class {
|
|
3290
3483
|
filePath;
|
|
3291
3484
|
dir;
|
|
3292
3485
|
dirCache = /* @__PURE__ */ new Set();
|
|
3486
|
+
/** Promise-based mutex to serialize write operations */
|
|
3487
|
+
writeLock = Promise.resolve();
|
|
3293
3488
|
constructor(filePath) {
|
|
3294
3489
|
this.filePath = filePath;
|
|
3295
3490
|
this.dir = path11.dirname(filePath);
|
|
3296
3491
|
}
|
|
3492
|
+
/**
|
|
3493
|
+
* Acquire the write lock and execute a callback.
|
|
3494
|
+
* Serializes all write operations to prevent race conditions.
|
|
3495
|
+
*/
|
|
3496
|
+
async withWriteLock(fn) {
|
|
3497
|
+
const release = this.writeLock;
|
|
3498
|
+
let releaseLock = () => {
|
|
3499
|
+
};
|
|
3500
|
+
this.writeLock = new Promise((r) => {
|
|
3501
|
+
releaseLock = r;
|
|
3502
|
+
});
|
|
3503
|
+
try {
|
|
3504
|
+
await release;
|
|
3505
|
+
return await fn();
|
|
3506
|
+
} finally {
|
|
3507
|
+
releaseLock();
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3297
3510
|
/**
|
|
3298
3511
|
* Create a new decision and persist it.
|
|
3299
3512
|
* @returns The generated decision ID
|
|
@@ -3313,19 +3526,22 @@ var DecisionStore = class {
|
|
|
3313
3526
|
/**
|
|
3314
3527
|
* Answer a decision by ID.
|
|
3315
3528
|
* Reads all entries, updates the matching one, and rewrites the file.
|
|
3529
|
+
* Uses a mutex to serialize concurrent calls and prevent race conditions.
|
|
3316
3530
|
*/
|
|
3317
3531
|
async answer(id, answer) {
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3532
|
+
await this.withWriteLock(async () => {
|
|
3533
|
+
const decisions = await this.readAll();
|
|
3534
|
+
const decision = decisions.find((d) => d.id === id);
|
|
3535
|
+
if (!decision) {
|
|
3536
|
+
throw new Error(`Decision not found: ${id}`);
|
|
3537
|
+
}
|
|
3538
|
+
if (decision.answer !== void 0) {
|
|
3539
|
+
throw new Error(`Decision already answered: ${id}`);
|
|
3540
|
+
}
|
|
3541
|
+
decision.answer = answer;
|
|
3542
|
+
decision.answeredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3543
|
+
await this.writeAll(decisions);
|
|
3544
|
+
});
|
|
3329
3545
|
}
|
|
3330
3546
|
/**
|
|
3331
3547
|
* Get all pending decisions (unanswered, not expired, not timed out).
|
|
@@ -3362,33 +3578,36 @@ var DecisionStore = class {
|
|
|
3362
3578
|
/**
|
|
3363
3579
|
* Auto-answer expired decisions with their defaultAnswer.
|
|
3364
3580
|
* Decisions without defaultAnswer are marked as expired (expiredAt).
|
|
3581
|
+
* Uses a mutex to serialize concurrent calls and prevent race conditions.
|
|
3365
3582
|
* @returns The decisions that were auto-answered or marked expired
|
|
3366
3583
|
*/
|
|
3367
3584
|
async expire() {
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
if (decision.
|
|
3374
|
-
decision.
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3585
|
+
return this.withWriteLock(async () => {
|
|
3586
|
+
const decisions = await this.readAll();
|
|
3587
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3588
|
+
const expired = [];
|
|
3589
|
+
for (const decision of decisions) {
|
|
3590
|
+
if (decision.answer === void 0 && decision.expiredAt === void 0 && decision.expiresAt && decision.expiresAt < now) {
|
|
3591
|
+
if (decision.defaultAnswer !== void 0) {
|
|
3592
|
+
decision.answer = decision.defaultAnswer;
|
|
3593
|
+
decision.answeredAt = now;
|
|
3594
|
+
} else {
|
|
3595
|
+
decision.expiredAt = now;
|
|
3596
|
+
}
|
|
3597
|
+
expired.push(decision);
|
|
3378
3598
|
}
|
|
3379
|
-
expired.push(decision);
|
|
3380
3599
|
}
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3600
|
+
if (expired.length > 0) {
|
|
3601
|
+
await this.writeAll(decisions);
|
|
3602
|
+
}
|
|
3603
|
+
return expired;
|
|
3604
|
+
});
|
|
3386
3605
|
}
|
|
3387
3606
|
// ─── Private helpers ─────────────────────────────────────
|
|
3388
3607
|
async readAll() {
|
|
3389
3608
|
let content;
|
|
3390
3609
|
try {
|
|
3391
|
-
content = await
|
|
3610
|
+
content = await readFile6(this.filePath, "utf-8");
|
|
3392
3611
|
} catch (error) {
|
|
3393
3612
|
if (error.code === "ENOENT") {
|
|
3394
3613
|
return [];
|
|
@@ -3514,7 +3733,7 @@ var activityQueryOptionsSchema = z5.object({
|
|
|
3514
3733
|
|
|
3515
3734
|
// src/supervisor/activity-log.ts
|
|
3516
3735
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
3517
|
-
import { appendFile as appendFile5, readFile as
|
|
3736
|
+
import { appendFile as appendFile5, readFile as readFile7, rename, stat as stat3 } from "fs/promises";
|
|
3518
3737
|
import path12 from "path";
|
|
3519
3738
|
var ACTIVITY_FILE = "activity.jsonl";
|
|
3520
3739
|
var MAX_SIZE_BYTES = 10 * 1024 * 1024;
|
|
@@ -3553,8 +3772,11 @@ var ActivityLog = class {
|
|
|
3553
3772
|
async tail(n) {
|
|
3554
3773
|
let content;
|
|
3555
3774
|
try {
|
|
3556
|
-
content = await
|
|
3557
|
-
} catch {
|
|
3775
|
+
content = await readFile7(this.filePath, "utf-8");
|
|
3776
|
+
} catch (err) {
|
|
3777
|
+
console.debug(
|
|
3778
|
+
`[ActivityLog] Failed to read activity log: ${err instanceof Error ? err.message : String(err)}`
|
|
3779
|
+
);
|
|
3558
3780
|
return [];
|
|
3559
3781
|
}
|
|
3560
3782
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
@@ -3563,14 +3785,17 @@ var ActivityLog = class {
|
|
|
3563
3785
|
for (const line of lastLines) {
|
|
3564
3786
|
try {
|
|
3565
3787
|
entries.push(JSON.parse(line));
|
|
3566
|
-
} catch {
|
|
3788
|
+
} catch (err) {
|
|
3789
|
+
console.debug(
|
|
3790
|
+
`[ActivityLog] Skipping malformed line: ${err instanceof Error ? err.message : String(err)}`
|
|
3791
|
+
);
|
|
3567
3792
|
}
|
|
3568
3793
|
}
|
|
3569
3794
|
return entries;
|
|
3570
3795
|
}
|
|
3571
3796
|
async checkRotation() {
|
|
3572
3797
|
try {
|
|
3573
|
-
const stats = await
|
|
3798
|
+
const stats = await stat3(this.filePath);
|
|
3574
3799
|
if (stats.size > MAX_SIZE_BYTES) {
|
|
3575
3800
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3576
3801
|
const rotatedPath = path12.join(this.dir, `activity-${timestamp}.jsonl`);
|
|
@@ -3584,13 +3809,13 @@ var ActivityLog = class {
|
|
|
3584
3809
|
// src/supervisor/daemon.ts
|
|
3585
3810
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
3586
3811
|
import { existsSync as existsSync8 } from "fs";
|
|
3587
|
-
import { mkdir as mkdir7, readFile as
|
|
3812
|
+
import { mkdir as mkdir7, readFile as readFile11, rm as rm2, writeFile as writeFile7 } from "fs/promises";
|
|
3588
3813
|
import { homedir as homedir5 } from "os";
|
|
3589
3814
|
import path15 from "path";
|
|
3590
3815
|
|
|
3591
3816
|
// src/supervisor/event-queue.ts
|
|
3592
3817
|
import { watch as watch2 } from "fs";
|
|
3593
|
-
import { readFile as
|
|
3818
|
+
import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
3594
3819
|
var EventQueue = class {
|
|
3595
3820
|
queue = [];
|
|
3596
3821
|
seenIds = /* @__PURE__ */ new Set();
|
|
@@ -3743,7 +3968,7 @@ var EventQueue = class {
|
|
|
3743
3968
|
async readNewLines(filePath, kind) {
|
|
3744
3969
|
let content;
|
|
3745
3970
|
try {
|
|
3746
|
-
content = await
|
|
3971
|
+
content = await readFile8(filePath, "utf-8");
|
|
3747
3972
|
} catch (_err) {
|
|
3748
3973
|
return;
|
|
3749
3974
|
}
|
|
@@ -3768,7 +3993,7 @@ var EventQueue = class {
|
|
|
3768
3993
|
async replayFile(filePath, kind) {
|
|
3769
3994
|
let content;
|
|
3770
3995
|
try {
|
|
3771
|
-
content = await
|
|
3996
|
+
content = await readFile8(filePath, "utf-8");
|
|
3772
3997
|
} catch (_err) {
|
|
3773
3998
|
return;
|
|
3774
3999
|
}
|
|
@@ -3804,7 +4029,7 @@ var EventQueue = class {
|
|
|
3804
4029
|
}
|
|
3805
4030
|
async markInFile(filePath, matchTimestamp, processedAt) {
|
|
3806
4031
|
try {
|
|
3807
|
-
const content = await
|
|
4032
|
+
const content = await readFile8(filePath, "utf-8");
|
|
3808
4033
|
const lines = content.split("\n");
|
|
3809
4034
|
let changed = false;
|
|
3810
4035
|
const updated = lines.map((line) => {
|
|
@@ -3832,7 +4057,7 @@ var EventQueue = class {
|
|
|
3832
4057
|
// src/supervisor/heartbeat.ts
|
|
3833
4058
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
3834
4059
|
import { existsSync as existsSync7 } from "fs";
|
|
3835
|
-
import { readdir as readdir4, readFile as
|
|
4060
|
+
import { readdir as readdir4, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
|
|
3836
4061
|
import { homedir as homedir4 } from "os";
|
|
3837
4062
|
import path14 from "path";
|
|
3838
4063
|
|
|
@@ -3888,7 +4113,7 @@ var IdleDetector = class {
|
|
|
3888
4113
|
};
|
|
3889
4114
|
|
|
3890
4115
|
// src/supervisor/log-buffer.ts
|
|
3891
|
-
import { appendFile as appendFile6, readFile as
|
|
4116
|
+
import { appendFile as appendFile6, readFile as readFile9, stat as stat4, writeFile as writeFile5 } from "fs/promises";
|
|
3892
4117
|
import path13 from "path";
|
|
3893
4118
|
var LOG_BUFFER_FILE = "log-buffer.jsonl";
|
|
3894
4119
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
@@ -3902,16 +4127,22 @@ function parseLines(content) {
|
|
|
3902
4127
|
for (const line of lines) {
|
|
3903
4128
|
try {
|
|
3904
4129
|
entries.push(JSON.parse(line));
|
|
3905
|
-
} catch {
|
|
4130
|
+
} catch (err) {
|
|
4131
|
+
console.debug(
|
|
4132
|
+
`[log-buffer] Skipping malformed line: ${err instanceof Error ? err.message : String(err)}`
|
|
4133
|
+
);
|
|
3906
4134
|
}
|
|
3907
4135
|
}
|
|
3908
4136
|
return entries;
|
|
3909
4137
|
}
|
|
3910
4138
|
async function readLogBuffer(dir) {
|
|
3911
4139
|
try {
|
|
3912
|
-
const content = await
|
|
4140
|
+
const content = await readFile9(bufferPath(dir), "utf-8");
|
|
3913
4141
|
return parseLines(content);
|
|
3914
|
-
} catch {
|
|
4142
|
+
} catch (err) {
|
|
4143
|
+
console.debug(
|
|
4144
|
+
`[log-buffer] Failed to read buffer: ${err instanceof Error ? err.message : String(err)}`
|
|
4145
|
+
);
|
|
3915
4146
|
return [];
|
|
3916
4147
|
}
|
|
3917
4148
|
}
|
|
@@ -3923,8 +4154,11 @@ async function markConsolidated(dir, ids) {
|
|
|
3923
4154
|
const filePath = bufferPath(dir);
|
|
3924
4155
|
let content;
|
|
3925
4156
|
try {
|
|
3926
|
-
content = await
|
|
3927
|
-
} catch {
|
|
4157
|
+
content = await readFile9(filePath, "utf-8");
|
|
4158
|
+
} catch (err) {
|
|
4159
|
+
console.debug(
|
|
4160
|
+
`[log-buffer] Failed to read for consolidation: ${err instanceof Error ? err.message : String(err)}`
|
|
4161
|
+
);
|
|
3928
4162
|
return;
|
|
3929
4163
|
}
|
|
3930
4164
|
const idSet = new Set(ids);
|
|
@@ -3938,7 +4172,10 @@ async function markConsolidated(dir, ids) {
|
|
|
3938
4172
|
entry.consolidatedAt = now;
|
|
3939
4173
|
}
|
|
3940
4174
|
updated.push(JSON.stringify(entry));
|
|
3941
|
-
} catch {
|
|
4175
|
+
} catch (err) {
|
|
4176
|
+
console.debug(
|
|
4177
|
+
`[log-buffer] Preserving malformed line during consolidation: ${err instanceof Error ? err.message : String(err)}`
|
|
4178
|
+
);
|
|
3942
4179
|
updated.push(line);
|
|
3943
4180
|
}
|
|
3944
4181
|
}
|
|
@@ -3949,8 +4186,11 @@ async function compactLogBuffer(dir) {
|
|
|
3949
4186
|
const filePath = bufferPath(dir);
|
|
3950
4187
|
let content;
|
|
3951
4188
|
try {
|
|
3952
|
-
content = await
|
|
3953
|
-
} catch {
|
|
4189
|
+
content = await readFile9(filePath, "utf-8");
|
|
4190
|
+
} catch (err) {
|
|
4191
|
+
console.debug(
|
|
4192
|
+
`[log-buffer] Failed to read for compaction: ${err instanceof Error ? err.message : String(err)}`
|
|
4193
|
+
);
|
|
3954
4194
|
return;
|
|
3955
4195
|
}
|
|
3956
4196
|
const now = Date.now();
|
|
@@ -3966,7 +4206,10 @@ async function compactLogBuffer(dir) {
|
|
|
3966
4206
|
}
|
|
3967
4207
|
}
|
|
3968
4208
|
kept.push(JSON.stringify(entry));
|
|
3969
|
-
} catch {
|
|
4209
|
+
} catch (err) {
|
|
4210
|
+
console.debug(
|
|
4211
|
+
`[log-buffer] Dropping malformed line during compaction: ${err instanceof Error ? err.message : String(err)}`
|
|
4212
|
+
);
|
|
3970
4213
|
}
|
|
3971
4214
|
}
|
|
3972
4215
|
let result = `${kept.join("\n")}
|
|
@@ -3982,7 +4225,10 @@ async function appendLogBuffer(dir, entry) {
|
|
|
3982
4225
|
try {
|
|
3983
4226
|
await appendFile6(bufferPath(dir), `${JSON.stringify(entry)}
|
|
3984
4227
|
`, "utf-8");
|
|
3985
|
-
} catch {
|
|
4228
|
+
} catch (err) {
|
|
4229
|
+
console.debug(
|
|
4230
|
+
`[log-buffer] Failed to append entry: ${err instanceof Error ? err.message : String(err)}`
|
|
4231
|
+
);
|
|
3986
4232
|
}
|
|
3987
4233
|
}
|
|
3988
4234
|
|
|
@@ -4000,7 +4246,8 @@ var OPERATING_PRINCIPLES = `### Operating principles
|
|
|
4000
4246
|
- Keep initiative boundaries strict: decisions for initiative A must not be influenced by unrelated state from B.
|
|
4001
4247
|
- Your user-visible channel is \`neo log\` only; produce concise tool calls (not reasoning/explanations) and avoid wasted tokens.
|
|
4002
4248
|
- You may inspect repositories available via \`neo repos\`, read-only to launch agents.
|
|
4003
|
-
- Task hygiene is non-negotiable: update task outcomes EVERY heartbeat. A task without a current outcome is a blind spot
|
|
4249
|
+
- Task hygiene is non-negotiable: update task outcomes EVERY heartbeat. A task without a current outcome is a blind spot.
|
|
4250
|
+
- **No duplicate dispatches**: before dispatching a \`developer\` for any finding, ALWAYS check for open or recently merged PRs on the same topic: \`gh pr list --repo <repo> --search "<keywords>" --state open\` and \`--state merged --limit 5\`. If a similar PR exists \u2192 skip and log with \`neo log discovery\`. Dispatching duplicate agents wastes budget and pollutes the PR list.`;
|
|
4004
4251
|
var COMMANDS = `### Dispatching agents
|
|
4005
4252
|
\`\`\`bash
|
|
4006
4253
|
neo run <agent> --prompt "..." --repo <path> --branch <name> [--priority critical|high|medium|low] [--meta '<json>']
|
|
@@ -4216,7 +4463,7 @@ ${lines}
|
|
|
4216
4463
|
}
|
|
4217
4464
|
return "<focus>\n(empty \u2014 use neo memory write --type focus to set working context)\n</focus>";
|
|
4218
4465
|
}
|
|
4219
|
-
function buildPendingDecisionsSection(decisions
|
|
4466
|
+
function buildPendingDecisionsSection(decisions) {
|
|
4220
4467
|
if (!decisions || decisions.length === 0) {
|
|
4221
4468
|
return "";
|
|
4222
4469
|
}
|
|
@@ -4235,7 +4482,7 @@ function buildPendingDecisionsSection(decisions, autoDecide = false) {
|
|
|
4235
4482
|
lines.push(` Context: ${d.context}`);
|
|
4236
4483
|
}
|
|
4237
4484
|
}
|
|
4238
|
-
const instruction =
|
|
4485
|
+
const instruction = `You are in **autoDecide** mode \u2014 answer each pending decision yourself based on available context, project knowledge, and best engineering judgment.
|
|
4239
4486
|
|
|
4240
4487
|
\`\`\`bash
|
|
4241
4488
|
neo decision answer <decision_id> <answer>
|
|
@@ -4243,10 +4490,7 @@ neo decision answer <decision_id> <answer>
|
|
|
4243
4490
|
|
|
4244
4491
|
For each decision: analyze the options, consider the project context and risk, then answer decisively. Prefer safe, incremental choices when uncertain. Log your reasoning before answering.
|
|
4245
4492
|
|
|
4246
|
-
**Merge authority:** In autoDecide mode you MAY merge branches when the PR is ready (CI green, reviews approved). Use \`gh pr merge\` with the appropriate merge strategy
|
|
4247
|
-
\`\`\`bash
|
|
4248
|
-
neo event emit decision:answer --data '{"id":"<decision_id>","answer":"<option_key>"}'
|
|
4249
|
-
\`\`\``;
|
|
4493
|
+
**Merge authority:** In autoDecide mode you MAY merge branches when the PR is ready (CI green, reviews approved). Use \`gh pr merge\` with the appropriate merge strategy.`;
|
|
4250
4494
|
return `Pending decisions (${decisions.length}):
|
|
4251
4495
|
${lines.join("\n")}
|
|
4252
4496
|
|
|
@@ -4278,7 +4522,7 @@ ${opts.activeRuns.map((r) => `- ${r}`).join("\n")}`);
|
|
|
4278
4522
|
if (recentActions) {
|
|
4279
4523
|
parts.push(recentActions);
|
|
4280
4524
|
}
|
|
4281
|
-
const pendingDecisions = buildPendingDecisionsSection(opts.pendingDecisions
|
|
4525
|
+
const pendingDecisions = buildPendingDecisionsSection(opts.pendingDecisions);
|
|
4282
4526
|
if (pendingDecisions) {
|
|
4283
4527
|
parts.push(pendingDecisions);
|
|
4284
4528
|
}
|
|
@@ -4589,7 +4833,7 @@ function buildIdlePrompt(opts) {
|
|
|
4589
4833
|
const budgetLine = `Budget: $${opts.budgetStatus.todayUsd.toFixed(2)} / $${opts.budgetStatus.capUsd.toFixed(2)} (${opts.budgetStatus.remainingPct.toFixed(0)}% remaining)`;
|
|
4590
4834
|
const hasRepos = opts.repos.length > 0;
|
|
4591
4835
|
const hasBudget = opts.budgetStatus.remainingPct > 10;
|
|
4592
|
-
const hasPendingDecisions = (opts.pendingDecisions?.length ?? 0) > 0;
|
|
4836
|
+
const hasPendingDecisions = opts.hasPendingDecisions ?? (opts.pendingDecisions?.length ?? 0) > 0;
|
|
4593
4837
|
if (!hasRepos || !hasBudget) {
|
|
4594
4838
|
return `${buildRoleSection(opts.heartbeatCount)}
|
|
4595
4839
|
|
|
@@ -4604,7 +4848,7 @@ Nothing to do. Run \`neo log discovery "idle"\` and yield. Do not produce any ot
|
|
|
4604
4848
|
}
|
|
4605
4849
|
const repoList = opts.repos.map((r) => `- ${r.path} (branch: ${r.defaultBranch})`).join("\n");
|
|
4606
4850
|
if (hasPendingDecisions) {
|
|
4607
|
-
const pendingSection = buildPendingDecisionsSection(opts.pendingDecisions
|
|
4851
|
+
const pendingSection = buildPendingDecisionsSection(opts.pendingDecisions);
|
|
4608
4852
|
if (opts.autoDecide) {
|
|
4609
4853
|
return `${buildRoleSection(opts.heartbeatCount)}
|
|
4610
4854
|
|
|
@@ -4653,29 +4897,8 @@ Repositories:
|
|
|
4653
4897
|
${repoList}
|
|
4654
4898
|
</context>
|
|
4655
4899
|
|
|
4656
|
-
<reference>
|
|
4657
|
-
${getCommandsSection(opts.heartbeatCount)}
|
|
4658
|
-
</reference>
|
|
4659
|
-
|
|
4660
4900
|
<directive>
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
The scout explores the codebase and surfaces bugs, improvements, security issues, and tech debt. It creates decisions (via \`neo decision create\`) for each critical or high-impact finding, so the user can choose what to act on.
|
|
4664
|
-
|
|
4665
|
-
**Rules:**
|
|
4666
|
-
- Pick the repo that was least recently scouted (check your memory for previous scout runs).
|
|
4667
|
-
- Only ONE scout at a time \u2014 never dispatch multiple scouts in parallel.
|
|
4668
|
-
- Use \`--branch main\` (or the repo's default branch) \u2014 scouts are read-only.
|
|
4669
|
-
- Log your decision before dispatching.
|
|
4670
|
-
|
|
4671
|
-
**Example:**
|
|
4672
|
-
\`\`\`bash
|
|
4673
|
-
neo log decision "Idle \u2014 dispatching scout on <repo>"
|
|
4674
|
-
neo run scout --prompt "Explore this repository. Surface bugs, improvements, security issues, and tech debt. Create decisions for critical and high-impact findings." \\
|
|
4675
|
-
--repo <path> \\
|
|
4676
|
-
--branch <default-branch> \\
|
|
4677
|
-
--meta '{"stage":"scout","label":"scout-<repo-name>"}'
|
|
4678
|
-
\`\`\`
|
|
4901
|
+
Nothing to do. Run \`neo log discovery "idle"\` and yield. Do not produce any other output.
|
|
4679
4902
|
</directive>`;
|
|
4680
4903
|
}
|
|
4681
4904
|
function buildStandardPrompt(opts) {
|
|
@@ -4944,7 +5167,8 @@ var HeartbeatLoop = class {
|
|
|
4944
5167
|
if (this.configStore) {
|
|
4945
5168
|
this.config = this.configStore.getAll();
|
|
4946
5169
|
}
|
|
4947
|
-
this.activityLog.log("event", "Configuration reloaded (hot-reload)").catch(() => {
|
|
5170
|
+
this.activityLog.log("event", "Configuration reloaded (hot-reload)").catch((err) => {
|
|
5171
|
+
console.debug("[neo] Config reload log failed:", err);
|
|
4948
5172
|
});
|
|
4949
5173
|
this.eventQueue.interrupt();
|
|
4950
5174
|
}
|
|
@@ -4955,21 +5179,14 @@ var HeartbeatLoop = class {
|
|
|
4955
5179
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4956
5180
|
const budgetCheck = await this.checkBudgetExceeded(state, today);
|
|
4957
5181
|
if (budgetCheck.exceeded) return;
|
|
4958
|
-
const
|
|
4959
|
-
const
|
|
4960
|
-
const activeRuns = await this.getActiveRuns();
|
|
4961
|
-
const decisionStore = this.getDecisionStore();
|
|
4962
|
-
await this.processDecisionAnswers(rawEvents, decisionStore);
|
|
4963
|
-
const expiredDecisions = await decisionStore.expire();
|
|
4964
|
-
const hasExpiredDecisions = expiredDecisions.length > 0;
|
|
4965
|
-
const pendingDecisions = this.config.supervisor.autoDecide ? await decisionStore.pending() : [];
|
|
4966
|
-
const answeredDecisions = this.config.supervisor.autoDecide ? await decisionStore.answered(state?.lastHeartbeat) : [];
|
|
5182
|
+
const eventCtx = await this.gatherEventContext();
|
|
5183
|
+
const { pendingDecisions, answeredDecisions, hasExpiredDecisions, hasPendingDecisions } = await this.processDecisions(eventCtx.rawEvents, state?.lastHeartbeat);
|
|
4967
5184
|
const unconsolidatedEntries = await readUnconsolidated(this.supervisorDir);
|
|
4968
5185
|
const hasPendingConsolidation = unconsolidatedEntries.length > 0;
|
|
4969
5186
|
const skipResult = await this.handleSkipLogic({
|
|
4970
5187
|
state,
|
|
4971
|
-
totalEventCount,
|
|
4972
|
-
activeRuns,
|
|
5188
|
+
totalEventCount: eventCtx.totalEventCount,
|
|
5189
|
+
activeRuns: eventCtx.activeRuns,
|
|
4973
5190
|
hasPendingConsolidation,
|
|
4974
5191
|
hasExpiredDecisions
|
|
4975
5192
|
});
|
|
@@ -4979,27 +5196,31 @@ var HeartbeatLoop = class {
|
|
|
4979
5196
|
}
|
|
4980
5197
|
const modeResult = await this.determineHeartbeatMode(state);
|
|
4981
5198
|
const { prompt, modeLabel } = await this.buildHeartbeatModePrompt({
|
|
4982
|
-
grouped,
|
|
5199
|
+
grouped: eventCtx.grouped,
|
|
4983
5200
|
todayCost: budgetCheck.todayCost,
|
|
4984
5201
|
heartbeatCount: modeResult.heartbeatCount,
|
|
4985
5202
|
unconsolidated: modeResult.unconsolidated,
|
|
4986
5203
|
isCompaction: modeResult.isCompaction,
|
|
4987
5204
|
isConsolidation: modeResult.isConsolidation,
|
|
4988
|
-
activeRuns,
|
|
5205
|
+
activeRuns: eventCtx.activeRuns,
|
|
4989
5206
|
pendingDecisions,
|
|
4990
5207
|
answeredDecisions,
|
|
5208
|
+
hasPendingDecisions,
|
|
4991
5209
|
lastHeartbeat: state?.lastHeartbeat,
|
|
4992
|
-
lastConsolidationTimestamp: modeResult.lastConsolidationTs
|
|
5210
|
+
lastConsolidationTimestamp: modeResult.lastConsolidationTs,
|
|
5211
|
+
memories: eventCtx.memories,
|
|
5212
|
+
recentActions: eventCtx.recentActions,
|
|
5213
|
+
mcpServerNames: eventCtx.mcpServerNames
|
|
4993
5214
|
});
|
|
4994
5215
|
await this.activityLog.log(
|
|
4995
5216
|
"heartbeat",
|
|
4996
5217
|
`Heartbeat #${modeResult.heartbeatCount} starting (${modeLabel})`,
|
|
4997
5218
|
{
|
|
4998
5219
|
heartbeatId,
|
|
4999
|
-
eventCount: totalEventCount,
|
|
5000
|
-
messages: grouped.messages.length,
|
|
5001
|
-
webhooks: grouped.webhooks.length,
|
|
5002
|
-
runCompletions: grouped.runCompletions.length,
|
|
5220
|
+
eventCount: eventCtx.totalEventCount,
|
|
5221
|
+
messages: eventCtx.grouped.messages.length,
|
|
5222
|
+
webhooks: eventCtx.grouped.webhooks.length,
|
|
5223
|
+
runCompletions: eventCtx.grouped.runCompletions.length,
|
|
5003
5224
|
isConsolidation: modeResult.isConsolidation
|
|
5004
5225
|
}
|
|
5005
5226
|
);
|
|
@@ -5011,17 +5232,11 @@ var HeartbeatLoop = class {
|
|
|
5011
5232
|
{ heartbeatId }
|
|
5012
5233
|
);
|
|
5013
5234
|
}
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
const allIds = modeResult.unconsolidated.map((e) => e.id);
|
|
5020
|
-
if (allIds.length > 0) {
|
|
5021
|
-
await markConsolidated(this.supervisorDir, allIds);
|
|
5022
|
-
}
|
|
5023
|
-
await compactLogBuffer(this.supervisorDir);
|
|
5024
|
-
}
|
|
5235
|
+
await this.handlePostSdkProcessing({
|
|
5236
|
+
rawEvents: eventCtx.rawEvents,
|
|
5237
|
+
isConsolidation: modeResult.isConsolidation,
|
|
5238
|
+
unconsolidated: modeResult.unconsolidated
|
|
5239
|
+
});
|
|
5025
5240
|
const durationMs = Date.now() - startTime;
|
|
5026
5241
|
const { stateUpdate } = this.buildStateUpdate({
|
|
5027
5242
|
state,
|
|
@@ -5044,13 +5259,92 @@ var HeartbeatLoop = class {
|
|
|
5044
5259
|
isConsolidation: modeResult.isConsolidation
|
|
5045
5260
|
}
|
|
5046
5261
|
);
|
|
5262
|
+
await this.emitCompletionEvents({
|
|
5263
|
+
heartbeatCount: modeResult.heartbeatCount,
|
|
5264
|
+
activeRuns: eventCtx.activeRuns,
|
|
5265
|
+
todayCost: budgetCheck.todayCost,
|
|
5266
|
+
costUsd,
|
|
5267
|
+
rawEvents: eventCtx.rawEvents
|
|
5268
|
+
});
|
|
5269
|
+
}
|
|
5270
|
+
/**
|
|
5271
|
+
* Check if supervisor daily budget is exceeded.
|
|
5272
|
+
*/
|
|
5273
|
+
async checkBudgetExceeded(state, today) {
|
|
5274
|
+
const todayCost = state?.costResetDate === today ? state.todayCostUsd ?? 0 : 0;
|
|
5275
|
+
if (todayCost >= this.config.supervisor.dailyCapUsd) {
|
|
5276
|
+
await this.activityLog.log(
|
|
5277
|
+
"error",
|
|
5278
|
+
`Supervisor daily budget exceeded ($${todayCost.toFixed(2)} / $${this.config.supervisor.dailyCapUsd}). Skipping heartbeat.`
|
|
5279
|
+
);
|
|
5280
|
+
await this.sleep(this.config.supervisor.eventTimeoutMs);
|
|
5281
|
+
return { todayCost, exceeded: true };
|
|
5282
|
+
}
|
|
5283
|
+
return { todayCost, exceeded: false };
|
|
5284
|
+
}
|
|
5285
|
+
/**
|
|
5286
|
+
* Process decision answers from inbox and expire old decisions.
|
|
5287
|
+
* Returns pending, answered, and expiry status for prompt context.
|
|
5288
|
+
*/
|
|
5289
|
+
async processDecisions(rawEvents, lastHeartbeat) {
|
|
5290
|
+
const decisionStore = this.getDecisionStore();
|
|
5291
|
+
await this.processDecisionAnswers(rawEvents, decisionStore);
|
|
5292
|
+
const expiredDecisions = await decisionStore.expire();
|
|
5293
|
+
const hasExpiredDecisions = expiredDecisions.length > 0;
|
|
5294
|
+
const allPending = await decisionStore.pending();
|
|
5295
|
+
const hasPendingDecisions = allPending.length > 0;
|
|
5296
|
+
const pendingDecisions = this.config.supervisor.autoDecide ? allPending : [];
|
|
5297
|
+
const answeredDecisions = this.config.supervisor.autoDecide ? await decisionStore.answered(lastHeartbeat) : [];
|
|
5298
|
+
return { pendingDecisions, answeredDecisions, hasExpiredDecisions, hasPendingDecisions };
|
|
5299
|
+
}
|
|
5300
|
+
/**
|
|
5301
|
+
* Gather event context: drain queue, fetch active runs, memories, and recent actions.
|
|
5302
|
+
*/
|
|
5303
|
+
async gatherEventContext() {
|
|
5304
|
+
const { grouped, rawEvents } = this.eventQueue.drainAndGroup();
|
|
5305
|
+
const totalEventCount = grouped.messages.length + grouped.webhooks.length + grouped.runCompletions.length;
|
|
5306
|
+
const activeRuns = await this.getActiveRuns();
|
|
5307
|
+
const mcpServerNames = this.config.mcpServers ? Object.keys(this.config.mcpServers) : [];
|
|
5308
|
+
const store = this.getMemoryStore();
|
|
5309
|
+
const memories = store ? store.query({ limit: 40, sortBy: "relevance" }) : [];
|
|
5310
|
+
const recentActions = await this.activityLog.tail(20);
|
|
5311
|
+
return {
|
|
5312
|
+
grouped,
|
|
5313
|
+
rawEvents,
|
|
5314
|
+
totalEventCount,
|
|
5315
|
+
activeRuns,
|
|
5316
|
+
memories,
|
|
5317
|
+
recentActions,
|
|
5318
|
+
mcpServerNames
|
|
5319
|
+
};
|
|
5320
|
+
}
|
|
5321
|
+
/**
|
|
5322
|
+
* Handle post-SDK processing: mark events as processed, consolidate log buffer.
|
|
5323
|
+
*/
|
|
5324
|
+
async handlePostSdkProcessing(input) {
|
|
5325
|
+
if (input.rawEvents.length > 0) {
|
|
5326
|
+
const inboxPath = path14.join(this.supervisorDir, "inbox.jsonl");
|
|
5327
|
+
await this.eventQueue.markProcessed(inboxPath, this.eventsPath, input.rawEvents);
|
|
5328
|
+
}
|
|
5329
|
+
if (input.isConsolidation) {
|
|
5330
|
+
const allIds = input.unconsolidated.map((e) => e.id);
|
|
5331
|
+
if (allIds.length > 0) {
|
|
5332
|
+
await markConsolidated(this.supervisorDir, allIds);
|
|
5333
|
+
}
|
|
5334
|
+
await compactLogBuffer(this.supervisorDir);
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
5337
|
+
/**
|
|
5338
|
+
* Emit completion webhook events: heartbeat completed and run completed events.
|
|
5339
|
+
*/
|
|
5340
|
+
async emitCompletionEvents(input) {
|
|
5047
5341
|
await this.emitHeartbeatCompleted({
|
|
5048
|
-
heartbeatNumber:
|
|
5049
|
-
runsActive: activeRuns.length,
|
|
5050
|
-
todayUsd:
|
|
5342
|
+
heartbeatNumber: input.heartbeatCount + 1,
|
|
5343
|
+
runsActive: input.activeRuns.length,
|
|
5344
|
+
todayUsd: input.todayCost + input.costUsd,
|
|
5051
5345
|
limitUsd: this.config.supervisor.dailyCapUsd
|
|
5052
5346
|
});
|
|
5053
|
-
for (const event of rawEvents) {
|
|
5347
|
+
for (const event of input.rawEvents) {
|
|
5054
5348
|
if (event.kind === "run_complete") {
|
|
5055
5349
|
const runData = await this.readPersistedRun(event.runId);
|
|
5056
5350
|
const emitOpts = {
|
|
@@ -5066,21 +5360,6 @@ var HeartbeatLoop = class {
|
|
|
5066
5360
|
}
|
|
5067
5361
|
}
|
|
5068
5362
|
}
|
|
5069
|
-
/**
|
|
5070
|
-
* Check if supervisor daily budget is exceeded.
|
|
5071
|
-
*/
|
|
5072
|
-
async checkBudgetExceeded(state, today) {
|
|
5073
|
-
const todayCost = state?.costResetDate === today ? state.todayCostUsd ?? 0 : 0;
|
|
5074
|
-
if (todayCost >= this.config.supervisor.dailyCapUsd) {
|
|
5075
|
-
await this.activityLog.log(
|
|
5076
|
-
"error",
|
|
5077
|
-
`Supervisor daily budget exceeded ($${todayCost.toFixed(2)} / $${this.config.supervisor.dailyCapUsd}). Skipping heartbeat.`
|
|
5078
|
-
);
|
|
5079
|
-
await this.sleep(this.config.supervisor.eventTimeoutMs);
|
|
5080
|
-
return { todayCost, exceeded: true };
|
|
5081
|
-
}
|
|
5082
|
-
return { todayCost, exceeded: false };
|
|
5083
|
-
}
|
|
5084
5363
|
/**
|
|
5085
5364
|
* Handle skip logic for idle and active-work scenarios.
|
|
5086
5365
|
* Uses IdleDetector to make skip decisions based on context.
|
|
@@ -5183,10 +5462,6 @@ var HeartbeatLoop = class {
|
|
|
5183
5462
|
* Build the prompt for the current heartbeat mode.
|
|
5184
5463
|
*/
|
|
5185
5464
|
async buildHeartbeatModePrompt(opts) {
|
|
5186
|
-
const mcpServerNames = this.config.mcpServers ? Object.keys(this.config.mcpServers) : [];
|
|
5187
|
-
const store = this.getMemoryStore();
|
|
5188
|
-
const memories = store ? store.query({ limit: 40, sortBy: "relevance" }) : [];
|
|
5189
|
-
const recentActions = await this.activityLog.tail(20);
|
|
5190
5465
|
const sharedOpts = {
|
|
5191
5466
|
repos: this.config.repos,
|
|
5192
5467
|
grouped: opts.grouped,
|
|
@@ -5197,13 +5472,14 @@ var HeartbeatLoop = class {
|
|
|
5197
5472
|
},
|
|
5198
5473
|
activeRuns: opts.activeRuns,
|
|
5199
5474
|
heartbeatCount: opts.heartbeatCount,
|
|
5200
|
-
mcpServerNames,
|
|
5475
|
+
mcpServerNames: opts.mcpServerNames,
|
|
5201
5476
|
customInstructions: this.customInstructions,
|
|
5202
5477
|
supervisorDir: this.supervisorDir,
|
|
5203
|
-
memories,
|
|
5204
|
-
recentActions,
|
|
5478
|
+
memories: opts.memories,
|
|
5479
|
+
recentActions: opts.recentActions,
|
|
5205
5480
|
pendingDecisions: opts.pendingDecisions,
|
|
5206
5481
|
answeredDecisions: opts.answeredDecisions,
|
|
5482
|
+
hasPendingDecisions: opts.hasPendingDecisions,
|
|
5207
5483
|
autoDecide: this.config.supervisor.autoDecide
|
|
5208
5484
|
};
|
|
5209
5485
|
if (opts.isCompaction) {
|
|
@@ -5313,7 +5589,7 @@ var HeartbeatLoop = class {
|
|
|
5313
5589
|
}
|
|
5314
5590
|
async readState() {
|
|
5315
5591
|
try {
|
|
5316
|
-
const raw = await
|
|
5592
|
+
const raw = await readFile10(this.statePath, "utf-8");
|
|
5317
5593
|
return JSON.parse(raw);
|
|
5318
5594
|
} catch {
|
|
5319
5595
|
return null;
|
|
@@ -5321,7 +5597,7 @@ var HeartbeatLoop = class {
|
|
|
5321
5597
|
}
|
|
5322
5598
|
async updateState(updates) {
|
|
5323
5599
|
try {
|
|
5324
|
-
const raw = await
|
|
5600
|
+
const raw = await readFile10(this.statePath, "utf-8");
|
|
5325
5601
|
const state = JSON.parse(raw);
|
|
5326
5602
|
Object.assign(state, updates);
|
|
5327
5603
|
await writeFile6(this.statePath, JSON.stringify(state, null, 2), "utf-8");
|
|
@@ -5346,7 +5622,7 @@ var HeartbeatLoop = class {
|
|
|
5346
5622
|
for (const f of files) {
|
|
5347
5623
|
if (!f.endsWith(".json")) continue;
|
|
5348
5624
|
try {
|
|
5349
|
-
const raw = await
|
|
5625
|
+
const raw = await readFile10(path14.join(subDir, f), "utf-8");
|
|
5350
5626
|
const run = JSON.parse(raw);
|
|
5351
5627
|
if (isRunActive(run)) {
|
|
5352
5628
|
active.push(
|
|
@@ -5380,7 +5656,7 @@ var HeartbeatLoop = class {
|
|
|
5380
5656
|
}
|
|
5381
5657
|
for (const filePath of candidates) {
|
|
5382
5658
|
try {
|
|
5383
|
-
const content = await
|
|
5659
|
+
const content = await readFile10(filePath, "utf-8");
|
|
5384
5660
|
await this.activityLog.log("event", `Loaded instructions from ${filePath}`);
|
|
5385
5661
|
return content;
|
|
5386
5662
|
} catch {
|
|
@@ -5495,7 +5771,7 @@ var HeartbeatLoop = class {
|
|
|
5495
5771
|
const subDir = path14.join(runsDir, entry.name);
|
|
5496
5772
|
const runPath = path14.join(subDir, `${runId}.json`);
|
|
5497
5773
|
if (existsSync7(runPath)) {
|
|
5498
|
-
const raw = await
|
|
5774
|
+
const raw = await readFile10(runPath, "utf-8");
|
|
5499
5775
|
const run = JSON.parse(raw);
|
|
5500
5776
|
const totalCostUsd = Object.values(run.steps).reduce(
|
|
5501
5777
|
(sum, step) => sum + (step.costUsd ?? 0),
|
|
@@ -5873,9 +6149,12 @@ var SupervisorDaemon = class {
|
|
|
5873
6149
|
async readState() {
|
|
5874
6150
|
const statePath = path15.join(this.dir, "state.json");
|
|
5875
6151
|
try {
|
|
5876
|
-
const raw = await
|
|
6152
|
+
const raw = await readFile11(statePath, "utf-8");
|
|
5877
6153
|
return JSON.parse(raw);
|
|
5878
|
-
} catch {
|
|
6154
|
+
} catch (err) {
|
|
6155
|
+
console.debug(
|
|
6156
|
+
`[SupervisorDaemon] Failed to read state: ${err instanceof Error ? err.message : String(err)}`
|
|
6157
|
+
);
|
|
5879
6158
|
return null;
|
|
5880
6159
|
}
|
|
5881
6160
|
}
|
|
@@ -5885,10 +6164,13 @@ var SupervisorDaemon = class {
|
|
|
5885
6164
|
}
|
|
5886
6165
|
async readLockPid(lockPath) {
|
|
5887
6166
|
try {
|
|
5888
|
-
const raw = await
|
|
6167
|
+
const raw = await readFile11(lockPath, "utf-8");
|
|
5889
6168
|
const pid = Number.parseInt(raw.trim(), 10);
|
|
5890
6169
|
return Number.isNaN(pid) ? null : pid;
|
|
5891
|
-
} catch {
|
|
6170
|
+
} catch (err) {
|
|
6171
|
+
console.debug(
|
|
6172
|
+
`[SupervisorDaemon] Failed to read lock PID from ${lockPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
6173
|
+
);
|
|
5892
6174
|
return null;
|
|
5893
6175
|
}
|
|
5894
6176
|
}
|
|
@@ -5924,8 +6206,8 @@ var SupervisorDaemon = class {
|
|
|
5924
6206
|
};
|
|
5925
6207
|
|
|
5926
6208
|
// src/supervisor/StatusReader.ts
|
|
5927
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
5928
|
-
import { readFile as
|
|
6209
|
+
import { existsSync as existsSync9, readFileSync as readFileSync2 } from "fs";
|
|
6210
|
+
import { readdir as readdir5, readFile as readFile12 } from "fs/promises";
|
|
5929
6211
|
import path16 from "path";
|
|
5930
6212
|
var STATE_FILE = "state.json";
|
|
5931
6213
|
var ACTIVITY_FILE2 = "activity.jsonl";
|
|
@@ -5945,14 +6227,20 @@ var StatusReader = class {
|
|
|
5945
6227
|
async getStatus() {
|
|
5946
6228
|
let raw;
|
|
5947
6229
|
try {
|
|
5948
|
-
raw = await
|
|
5949
|
-
} catch {
|
|
6230
|
+
raw = await readFile12(this.statePath, "utf-8");
|
|
6231
|
+
} catch (err) {
|
|
6232
|
+
console.debug(
|
|
6233
|
+
`[StatusReader] State file not found: ${err instanceof Error ? err.message : String(err)}`
|
|
6234
|
+
);
|
|
5950
6235
|
return null;
|
|
5951
6236
|
}
|
|
5952
6237
|
let parsed;
|
|
5953
6238
|
try {
|
|
5954
6239
|
parsed = JSON.parse(raw);
|
|
5955
|
-
} catch {
|
|
6240
|
+
} catch (err) {
|
|
6241
|
+
console.debug(
|
|
6242
|
+
`[StatusReader] Malformed state JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
6243
|
+
);
|
|
5956
6244
|
return null;
|
|
5957
6245
|
}
|
|
5958
6246
|
const result = supervisorDaemonStateSchema.safeParse(parsed);
|
|
@@ -5966,6 +6254,7 @@ var StatusReader = class {
|
|
|
5966
6254
|
stopped: "idle"
|
|
5967
6255
|
};
|
|
5968
6256
|
const recentActivity = this.queryActivity({ limit: 5 });
|
|
6257
|
+
const activeRunCount = await this.countActiveRuns();
|
|
5969
6258
|
return {
|
|
5970
6259
|
pid: daemon.pid,
|
|
5971
6260
|
sessionId: daemon.sessionId,
|
|
@@ -5975,8 +6264,7 @@ var StatusReader = class {
|
|
|
5975
6264
|
todayCostUsd: daemon.todayCostUsd,
|
|
5976
6265
|
status: statusMap[daemon.status],
|
|
5977
6266
|
lastHeartbeat: daemon.lastHeartbeat ?? daemon.startedAt,
|
|
5978
|
-
activeRunCount
|
|
5979
|
-
// TODO: count active runs from .neo/runs/
|
|
6267
|
+
activeRunCount,
|
|
5980
6268
|
recentActivitySummary: recentActivity.map((e) => `[${e.type}] ${e.summary}`)
|
|
5981
6269
|
};
|
|
5982
6270
|
}
|
|
@@ -5989,7 +6277,10 @@ var StatusReader = class {
|
|
|
5989
6277
|
let content;
|
|
5990
6278
|
try {
|
|
5991
6279
|
content = readFileSync2(this.activityPath, "utf-8");
|
|
5992
|
-
} catch {
|
|
6280
|
+
} catch (err) {
|
|
6281
|
+
console.debug(
|
|
6282
|
+
`[StatusReader] Activity file not found: ${err instanceof Error ? err.message : String(err)}`
|
|
6283
|
+
);
|
|
5993
6284
|
return [];
|
|
5994
6285
|
}
|
|
5995
6286
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
@@ -6001,7 +6292,10 @@ var StatusReader = class {
|
|
|
6001
6292
|
if (result.success) {
|
|
6002
6293
|
entries.push(result.data);
|
|
6003
6294
|
}
|
|
6004
|
-
} catch {
|
|
6295
|
+
} catch (err) {
|
|
6296
|
+
console.debug(
|
|
6297
|
+
`[StatusReader] Skipping malformed activity line: ${err instanceof Error ? err.message : String(err)}`
|
|
6298
|
+
);
|
|
6005
6299
|
}
|
|
6006
6300
|
}
|
|
6007
6301
|
if (type) {
|
|
@@ -6017,16 +6311,80 @@ var StatusReader = class {
|
|
|
6017
6311
|
}
|
|
6018
6312
|
return entries.slice(offset, offset + limit);
|
|
6019
6313
|
}
|
|
6314
|
+
/**
|
|
6315
|
+
* Count runs with status "running" from .neo/runs/.
|
|
6316
|
+
* Fails silently — returns 0 if the runs directory doesn't exist.
|
|
6317
|
+
*/
|
|
6318
|
+
async countActiveRuns() {
|
|
6319
|
+
const runsDir = getRunsDir();
|
|
6320
|
+
if (!existsSync9(runsDir)) return 0;
|
|
6321
|
+
try {
|
|
6322
|
+
const runFiles = await this.collectRunFiles(runsDir);
|
|
6323
|
+
let count = 0;
|
|
6324
|
+
for (const filePath of runFiles) {
|
|
6325
|
+
if (await this.isRunning(filePath)) count++;
|
|
6326
|
+
}
|
|
6327
|
+
return count;
|
|
6328
|
+
} catch (err) {
|
|
6329
|
+
console.debug(
|
|
6330
|
+
`[StatusReader] Failed to count active runs: ${err instanceof Error ? err.message : String(err)}`
|
|
6331
|
+
);
|
|
6332
|
+
return 0;
|
|
6333
|
+
}
|
|
6334
|
+
}
|
|
6335
|
+
/**
|
|
6336
|
+
* Collect all run JSON files from the runs directory tree.
|
|
6337
|
+
* Searches both top-level and repo subdirectories.
|
|
6338
|
+
*/
|
|
6339
|
+
async collectRunFiles(runsDir) {
|
|
6340
|
+
const entries = await readdir5(runsDir, { withFileTypes: true });
|
|
6341
|
+
const jsonFiles = [];
|
|
6342
|
+
for (const entry of entries) {
|
|
6343
|
+
if (entry.isDirectory()) {
|
|
6344
|
+
const subDir = path16.join(runsDir, entry.name);
|
|
6345
|
+
const subFiles = await readdir5(subDir);
|
|
6346
|
+
for (const f of subFiles) {
|
|
6347
|
+
if (this.isRunFile(f)) {
|
|
6348
|
+
jsonFiles.push(path16.join(subDir, f));
|
|
6349
|
+
}
|
|
6350
|
+
}
|
|
6351
|
+
} else if (this.isRunFile(entry.name)) {
|
|
6352
|
+
jsonFiles.push(path16.join(runsDir, entry.name));
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
return jsonFiles;
|
|
6356
|
+
}
|
|
6357
|
+
/**
|
|
6358
|
+
* Check if a filename is a run file (JSON but not dispatch).
|
|
6359
|
+
*/
|
|
6360
|
+
isRunFile(filename) {
|
|
6361
|
+
return filename.endsWith(".json") && !filename.endsWith(".dispatch.json");
|
|
6362
|
+
}
|
|
6363
|
+
/**
|
|
6364
|
+
* Check if a run file represents an active (running) run.
|
|
6365
|
+
*/
|
|
6366
|
+
async isRunning(filePath) {
|
|
6367
|
+
try {
|
|
6368
|
+
const content = await readFile12(filePath, "utf-8");
|
|
6369
|
+
const run = JSON.parse(content);
|
|
6370
|
+
return run.status === "running";
|
|
6371
|
+
} catch (err) {
|
|
6372
|
+
console.debug(
|
|
6373
|
+
`[StatusReader] Failed to read run file ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
6374
|
+
);
|
|
6375
|
+
return false;
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
6020
6378
|
};
|
|
6021
6379
|
|
|
6022
6380
|
// src/supervisor/shutdown.ts
|
|
6023
|
-
import { existsSync as
|
|
6024
|
-
import { readdir as
|
|
6381
|
+
import { existsSync as existsSync10 } from "fs";
|
|
6382
|
+
import { readdir as readdir6, readFile as readFile13, writeFile as writeFile8 } from "fs/promises";
|
|
6025
6383
|
import path17 from "path";
|
|
6026
6384
|
|
|
6027
6385
|
// src/webhook-config.ts
|
|
6028
|
-
import { existsSync as
|
|
6029
|
-
import { mkdir as mkdir8, readFile as
|
|
6386
|
+
import { existsSync as existsSync11 } from "fs";
|
|
6387
|
+
import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile9 } from "fs/promises";
|
|
6030
6388
|
import path18 from "path";
|
|
6031
6389
|
import { z as z8 } from "zod";
|
|
6032
6390
|
var webhookEntrySchema = z8.object({
|
|
@@ -6044,10 +6402,10 @@ function getWebhooksConfigPath() {
|
|
|
6044
6402
|
}
|
|
6045
6403
|
async function loadWebhooksConfig() {
|
|
6046
6404
|
const configPath = getWebhooksConfigPath();
|
|
6047
|
-
if (!
|
|
6405
|
+
if (!existsSync11(configPath)) {
|
|
6048
6406
|
return { webhooks: [] };
|
|
6049
6407
|
}
|
|
6050
|
-
const raw = await
|
|
6408
|
+
const raw = await readFile14(configPath, "utf-8");
|
|
6051
6409
|
const parsed = JSON.parse(raw);
|
|
6052
6410
|
return webhooksConfigSchema.parse(parsed);
|
|
6053
6411
|
}
|