@neotx/core 0.1.0-alpha.0 → 0.1.0-alpha.1
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 +29 -6
- package/dist/index.js +248 -29
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -132,6 +132,7 @@ declare const globalConfigSchema: z.ZodObject<{
|
|
|
132
132
|
port: z.ZodDefault<z.ZodNumber>;
|
|
133
133
|
secret: z.ZodOptional<z.ZodString>;
|
|
134
134
|
idleIntervalMs: z.ZodDefault<z.ZodNumber>;
|
|
135
|
+
idleSkipMax: z.ZodDefault<z.ZodNumber>;
|
|
135
136
|
heartbeatTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
136
137
|
maxConsecutiveFailures: z.ZodDefault<z.ZodNumber>;
|
|
137
138
|
maxEventsPerSec: z.ZodDefault<z.ZodNumber>;
|
|
@@ -195,6 +196,7 @@ declare const neoConfigSchema: z.ZodObject<{
|
|
|
195
196
|
port: z.ZodDefault<z.ZodNumber>;
|
|
196
197
|
secret: z.ZodOptional<z.ZodString>;
|
|
197
198
|
idleIntervalMs: z.ZodDefault<z.ZodNumber>;
|
|
199
|
+
idleSkipMax: z.ZodDefault<z.ZodNumber>;
|
|
198
200
|
heartbeatTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
199
201
|
maxConsecutiveFailures: z.ZodDefault<z.ZodNumber>;
|
|
200
202
|
maxEventsPerSec: z.ZodDefault<z.ZodNumber>;
|
|
@@ -867,6 +869,11 @@ declare class Orchestrator extends NeoEventEmitter {
|
|
|
867
869
|
private preDispatchChecks;
|
|
868
870
|
private buildDispatchContext;
|
|
869
871
|
private executeStep;
|
|
872
|
+
/**
|
|
873
|
+
* Push the branch, then remove the worktree.
|
|
874
|
+
* Runs in `finally` so it executes on both success and failure.
|
|
875
|
+
*/
|
|
876
|
+
private finalizeWorktree;
|
|
870
877
|
private runAgentSession;
|
|
871
878
|
private finalizeDispatch;
|
|
872
879
|
private emitCostEvents;
|
|
@@ -1012,6 +1019,7 @@ declare const supervisorDaemonStateSchema: z.ZodObject<{
|
|
|
1012
1019
|
totalCostUsd: z.ZodDefault<z.ZodNumber>;
|
|
1013
1020
|
todayCostUsd: z.ZodDefault<z.ZodNumber>;
|
|
1014
1021
|
costResetDate: z.ZodOptional<z.ZodString>;
|
|
1022
|
+
idleSkipCount: z.ZodDefault<z.ZodNumber>;
|
|
1015
1023
|
status: z.ZodDefault<z.ZodEnum<{
|
|
1016
1024
|
running: "running";
|
|
1017
1025
|
draining: "draining";
|
|
@@ -1072,7 +1080,7 @@ type QueuedEvent = {
|
|
|
1072
1080
|
};
|
|
1073
1081
|
|
|
1074
1082
|
declare class ActivityLog {
|
|
1075
|
-
|
|
1083
|
+
readonly filePath: string;
|
|
1076
1084
|
private readonly dir;
|
|
1077
1085
|
constructor(dir: string);
|
|
1078
1086
|
/**
|
|
@@ -1121,6 +1129,16 @@ declare class SupervisorDaemon {
|
|
|
1121
1129
|
interface EventQueueOptions {
|
|
1122
1130
|
maxEventsPerSec: number;
|
|
1123
1131
|
}
|
|
1132
|
+
interface GroupedMessage {
|
|
1133
|
+
text: string;
|
|
1134
|
+
from: string;
|
|
1135
|
+
count: number;
|
|
1136
|
+
}
|
|
1137
|
+
interface GroupedEvents {
|
|
1138
|
+
messages: GroupedMessage[];
|
|
1139
|
+
webhooks: QueuedEvent[];
|
|
1140
|
+
runCompletions: QueuedEvent[];
|
|
1141
|
+
}
|
|
1124
1142
|
/**
|
|
1125
1143
|
* In-memory event queue with deduplication, rate limiting, and file watching.
|
|
1126
1144
|
*
|
|
@@ -1151,6 +1169,11 @@ declare class EventQueue {
|
|
|
1151
1169
|
* Drain all queued events and return them. Clears the queue.
|
|
1152
1170
|
*/
|
|
1153
1171
|
drain(): QueuedEvent[];
|
|
1172
|
+
/**
|
|
1173
|
+
* Drain and group events: deduplicates messages by content,
|
|
1174
|
+
* keeps webhooks and run completions separate.
|
|
1175
|
+
*/
|
|
1176
|
+
drainAndGroup(): GroupedEvents;
|
|
1154
1177
|
size(): number;
|
|
1155
1178
|
/**
|
|
1156
1179
|
* Start watching inbox.jsonl and events.jsonl for new entries.
|
|
@@ -1224,7 +1247,7 @@ declare class HeartbeatLoop {
|
|
|
1224
1247
|
private loadInstructions;
|
|
1225
1248
|
/** Route a single SDK stream message to the appropriate log handler. */
|
|
1226
1249
|
private logStreamMessage;
|
|
1227
|
-
/** Log thinking and plan blocks from assistant content. */
|
|
1250
|
+
/** Log thinking and plan blocks from assistant content — no truncation. */
|
|
1228
1251
|
private logContentBlocks;
|
|
1229
1252
|
/** Log tool use events — distinguish MCP tools from built-in tools. */
|
|
1230
1253
|
private logToolUse;
|
|
@@ -1235,17 +1258,16 @@ declare class HeartbeatLoop {
|
|
|
1235
1258
|
|
|
1236
1259
|
/**
|
|
1237
1260
|
* Load the supervisor memory from disk.
|
|
1238
|
-
*
|
|
1261
|
+
* Migrates from legacy memory.md if needed.
|
|
1239
1262
|
*/
|
|
1240
1263
|
declare function loadMemory(dir: string): Promise<string>;
|
|
1241
1264
|
/**
|
|
1242
1265
|
* Save the supervisor memory to disk (full overwrite).
|
|
1266
|
+
* Automatically compacts if needed.
|
|
1243
1267
|
*/
|
|
1244
1268
|
declare function saveMemory(dir: string, content: string): Promise<void>;
|
|
1245
1269
|
/**
|
|
1246
1270
|
* Extract memory content from Claude's response using <memory>...</memory> tags.
|
|
1247
|
-
* Handles both JSON and markdown content inside the tags.
|
|
1248
|
-
* Returns null if no memory block is found.
|
|
1249
1271
|
*/
|
|
1250
1272
|
declare function extractMemoryFromResponse(response: string): string | null;
|
|
1251
1273
|
/**
|
|
@@ -1259,8 +1281,9 @@ declare function checkMemorySize(content: string): {
|
|
|
1259
1281
|
interface HeartbeatPromptOptions {
|
|
1260
1282
|
repos: RepoConfig[];
|
|
1261
1283
|
memory: string;
|
|
1284
|
+
knowledge: string;
|
|
1262
1285
|
memorySizeKB: number;
|
|
1263
|
-
|
|
1286
|
+
grouped: GroupedEvents;
|
|
1264
1287
|
budgetStatus: {
|
|
1265
1288
|
todayUsd: number;
|
|
1266
1289
|
capUsd: number;
|
package/dist/index.js
CHANGED
|
@@ -510,6 +510,7 @@ var globalConfigSchema = z2.object({
|
|
|
510
510
|
port: z2.number().default(7777),
|
|
511
511
|
secret: z2.string().optional(),
|
|
512
512
|
idleIntervalMs: z2.number().default(6e4),
|
|
513
|
+
idleSkipMax: z2.number().default(20),
|
|
513
514
|
heartbeatTimeoutMs: z2.number().default(3e5),
|
|
514
515
|
maxConsecutiveFailures: z2.number().default(3),
|
|
515
516
|
maxEventsPerSec: z2.number().default(10),
|
|
@@ -518,6 +519,7 @@ var globalConfigSchema = z2.object({
|
|
|
518
519
|
}).default({
|
|
519
520
|
port: 7777,
|
|
520
521
|
idleIntervalMs: 6e4,
|
|
522
|
+
idleSkipMax: 20,
|
|
521
523
|
heartbeatTimeoutMs: 3e5,
|
|
522
524
|
maxConsecutiveFailures: 3,
|
|
523
525
|
maxEventsPerSec: 10,
|
|
@@ -846,6 +848,9 @@ function getBranchName(config, runId) {
|
|
|
846
848
|
const sanitized = runId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
847
849
|
return `${prefix}/run-${sanitized}`;
|
|
848
850
|
}
|
|
851
|
+
async function pushWorktreeBranch(worktreePath, branch, remote) {
|
|
852
|
+
await withGitLock(worktreePath, () => git(worktreePath, ["push", "-u", remote, branch]));
|
|
853
|
+
}
|
|
849
854
|
|
|
850
855
|
// src/isolation/sandbox.ts
|
|
851
856
|
import { resolve as resolve2 } from "path";
|
|
@@ -1261,7 +1266,14 @@ async function runSession(options) {
|
|
|
1261
1266
|
let output = "";
|
|
1262
1267
|
let costUsd = 0;
|
|
1263
1268
|
let turnCount = 0;
|
|
1264
|
-
const
|
|
1269
|
+
const fullPrompt = agent.definition.prompt ? `${agent.definition.prompt}
|
|
1270
|
+
|
|
1271
|
+
---
|
|
1272
|
+
|
|
1273
|
+
## Task
|
|
1274
|
+
|
|
1275
|
+
${prompt}` : prompt;
|
|
1276
|
+
const stream = sdk.query({ prompt: fullPrompt, options: queryOptions });
|
|
1265
1277
|
for await (const message of stream) {
|
|
1266
1278
|
checkAborted(abortController.signal);
|
|
1267
1279
|
const msg = message;
|
|
@@ -1779,6 +1791,9 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
1779
1791
|
attempt: 1
|
|
1780
1792
|
};
|
|
1781
1793
|
} finally {
|
|
1794
|
+
if (worktreePath) {
|
|
1795
|
+
await this.finalizeWorktree(worktreePath, ctx);
|
|
1796
|
+
}
|
|
1782
1797
|
this.semaphore.release(sessionId);
|
|
1783
1798
|
this._activeSessions.delete(sessionId);
|
|
1784
1799
|
this.abortControllers.delete(sessionId);
|
|
@@ -1788,6 +1803,24 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
1788
1803
|
}
|
|
1789
1804
|
}
|
|
1790
1805
|
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Push the branch, then remove the worktree.
|
|
1808
|
+
* Runs in `finally` so it executes on both success and failure.
|
|
1809
|
+
*/
|
|
1810
|
+
async finalizeWorktree(worktreePath, ctx) {
|
|
1811
|
+
const { runId, repoConfig } = ctx;
|
|
1812
|
+
const branch = getBranchName(repoConfig, runId);
|
|
1813
|
+
const remote = repoConfig.pushRemote ?? "origin";
|
|
1814
|
+
try {
|
|
1815
|
+
await pushWorktreeBranch(worktreePath, branch, remote).catch(() => {
|
|
1816
|
+
});
|
|
1817
|
+
} catch {
|
|
1818
|
+
}
|
|
1819
|
+
try {
|
|
1820
|
+
await removeWorktree(worktreePath);
|
|
1821
|
+
} catch {
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1791
1824
|
async runAgentSession(ctx, worktreePath) {
|
|
1792
1825
|
const { input, runId, sessionId, stepName, stepDef, agent, activeSession } = ctx;
|
|
1793
1826
|
const sandboxConfig = buildSandboxConfig(agent, worktreePath);
|
|
@@ -2141,6 +2174,7 @@ var supervisorDaemonStateSchema = z4.object({
|
|
|
2141
2174
|
totalCostUsd: z4.number().default(0),
|
|
2142
2175
|
todayCostUsd: z4.number().default(0),
|
|
2143
2176
|
costResetDate: z4.string().optional(),
|
|
2177
|
+
idleSkipCount: z4.number().default(0),
|
|
2144
2178
|
status: z4.enum(["running", "draining", "stopped"]).default("running")
|
|
2145
2179
|
});
|
|
2146
2180
|
var webhookIncomingEventSchema = z4.object({
|
|
@@ -2302,6 +2336,36 @@ var EventQueue = class {
|
|
|
2302
2336
|
this.queue.length = 0;
|
|
2303
2337
|
return events;
|
|
2304
2338
|
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Drain and group events: deduplicates messages by content,
|
|
2341
|
+
* keeps webhooks and run completions separate.
|
|
2342
|
+
*/
|
|
2343
|
+
drainAndGroup() {
|
|
2344
|
+
const events = this.drain();
|
|
2345
|
+
const messageMap = /* @__PURE__ */ new Map();
|
|
2346
|
+
const webhooks = [];
|
|
2347
|
+
const runCompletions = [];
|
|
2348
|
+
for (const event of events) {
|
|
2349
|
+
if (event.kind === "message") {
|
|
2350
|
+
const key = event.data.text.trim().toLowerCase();
|
|
2351
|
+
const existing = messageMap.get(key);
|
|
2352
|
+
if (existing) {
|
|
2353
|
+
existing.count++;
|
|
2354
|
+
} else {
|
|
2355
|
+
messageMap.set(key, { text: event.data.text, from: event.data.from, count: 1 });
|
|
2356
|
+
}
|
|
2357
|
+
} else if (event.kind === "webhook") {
|
|
2358
|
+
webhooks.push(event);
|
|
2359
|
+
} else {
|
|
2360
|
+
runCompletions.push(event);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
return {
|
|
2364
|
+
messages: [...messageMap.values()],
|
|
2365
|
+
webhooks,
|
|
2366
|
+
runCompletions
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2305
2369
|
size() {
|
|
2306
2370
|
return this.queue.length;
|
|
2307
2371
|
}
|
|
@@ -2458,21 +2522,74 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
2458
2522
|
import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
|
|
2459
2523
|
import { homedir as homedir2 } from "os";
|
|
2460
2524
|
import path12 from "path";
|
|
2525
|
+
import { fileURLToPath } from "url";
|
|
2461
2526
|
|
|
2462
2527
|
// src/supervisor/memory.ts
|
|
2463
|
-
import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
2528
|
+
import { appendFile as appendFile5, readFile as readFile8, rename as rename2, writeFile as writeFile4 } from "fs/promises";
|
|
2464
2529
|
import path11 from "path";
|
|
2465
|
-
var MEMORY_FILE = "memory.
|
|
2466
|
-
var
|
|
2530
|
+
var MEMORY_FILE = "memory.json";
|
|
2531
|
+
var KNOWLEDGE_FILE = "knowledge.json";
|
|
2532
|
+
var ARCHIVE_FILE = "memory-archive.jsonl";
|
|
2533
|
+
var LEGACY_FILE = "memory.md";
|
|
2534
|
+
var MAX_SIZE_KB = 6;
|
|
2535
|
+
var MAX_DECISIONS = 10;
|
|
2536
|
+
function parseStructuredMemory(raw) {
|
|
2537
|
+
if (!raw.trim()) {
|
|
2538
|
+
return emptyMemory();
|
|
2539
|
+
}
|
|
2540
|
+
try {
|
|
2541
|
+
const parsed = JSON.parse(raw);
|
|
2542
|
+
return {
|
|
2543
|
+
activeWork: parsed.activeWork ?? [],
|
|
2544
|
+
blockers: parsed.blockers ?? [],
|
|
2545
|
+
repoNotes: parsed.repoNotes ?? {},
|
|
2546
|
+
recentDecisions: parsed.recentDecisions ?? [],
|
|
2547
|
+
trackerSync: parsed.trackerSync ?? {},
|
|
2548
|
+
notes: parsed.notes ?? ""
|
|
2549
|
+
};
|
|
2550
|
+
} catch {
|
|
2551
|
+
return { ...emptyMemory(), notes: raw };
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
function emptyMemory() {
|
|
2555
|
+
return {
|
|
2556
|
+
activeWork: [],
|
|
2557
|
+
blockers: [],
|
|
2558
|
+
repoNotes: {},
|
|
2559
|
+
recentDecisions: [],
|
|
2560
|
+
trackerSync: {},
|
|
2561
|
+
notes: ""
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
async function loadKnowledge(dir) {
|
|
2565
|
+
try {
|
|
2566
|
+
return await readFile8(path11.join(dir, KNOWLEDGE_FILE), "utf-8");
|
|
2567
|
+
} catch {
|
|
2568
|
+
return "";
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
async function saveKnowledge(dir, content) {
|
|
2572
|
+
await writeFile4(path11.join(dir, KNOWLEDGE_FILE), content, "utf-8");
|
|
2573
|
+
}
|
|
2467
2574
|
async function loadMemory(dir) {
|
|
2468
2575
|
try {
|
|
2469
2576
|
return await readFile8(path11.join(dir, MEMORY_FILE), "utf-8");
|
|
2470
2577
|
} catch {
|
|
2471
|
-
return "";
|
|
2472
2578
|
}
|
|
2579
|
+
try {
|
|
2580
|
+
const legacy = await readFile8(path11.join(dir, LEGACY_FILE), "utf-8");
|
|
2581
|
+
if (legacy.trim()) {
|
|
2582
|
+
await writeFile4(path11.join(dir, MEMORY_FILE), legacy, "utf-8");
|
|
2583
|
+
await rename2(path11.join(dir, LEGACY_FILE), path11.join(dir, `${LEGACY_FILE}.bak`));
|
|
2584
|
+
return legacy;
|
|
2585
|
+
}
|
|
2586
|
+
} catch {
|
|
2587
|
+
}
|
|
2588
|
+
return "";
|
|
2473
2589
|
}
|
|
2474
2590
|
async function saveMemory(dir, content) {
|
|
2475
|
-
await
|
|
2591
|
+
const compacted = await compactMemory(dir, content);
|
|
2592
|
+
await writeFile4(path11.join(dir, MEMORY_FILE), compacted, "utf-8");
|
|
2476
2593
|
}
|
|
2477
2594
|
function extractMemoryFromResponse(response) {
|
|
2478
2595
|
const match = /<memory>([\s\S]*?)<\/memory>/i.exec(response);
|
|
@@ -2487,10 +2604,53 @@ function extractMemoryFromResponse(response) {
|
|
|
2487
2604
|
}
|
|
2488
2605
|
return content;
|
|
2489
2606
|
}
|
|
2607
|
+
function extractKnowledgeFromResponse(response) {
|
|
2608
|
+
const match = /<knowledge>([\s\S]*?)<\/knowledge>/i.exec(response);
|
|
2609
|
+
if (!match?.[1]) return null;
|
|
2610
|
+
return match[1].trim();
|
|
2611
|
+
}
|
|
2490
2612
|
function checkMemorySize(content) {
|
|
2491
2613
|
const sizeKB = Buffer.byteLength(content, "utf-8") / 1024;
|
|
2492
2614
|
return { ok: sizeKB <= MAX_SIZE_KB, sizeKB: Math.round(sizeKB * 10) / 10 };
|
|
2493
2615
|
}
|
|
2616
|
+
async function compactMemory(dir, content) {
|
|
2617
|
+
if (!content.startsWith("{")) return content;
|
|
2618
|
+
let parsed;
|
|
2619
|
+
try {
|
|
2620
|
+
parsed = parseStructuredMemory(content);
|
|
2621
|
+
} catch {
|
|
2622
|
+
return content;
|
|
2623
|
+
}
|
|
2624
|
+
let changed = false;
|
|
2625
|
+
if (parsed.recentDecisions.length > MAX_DECISIONS) {
|
|
2626
|
+
const toArchive = parsed.recentDecisions.slice(0, -MAX_DECISIONS);
|
|
2627
|
+
parsed.recentDecisions = parsed.recentDecisions.slice(-MAX_DECISIONS);
|
|
2628
|
+
changed = true;
|
|
2629
|
+
const archivePath = path11.join(dir, ARCHIVE_FILE);
|
|
2630
|
+
const entry = {
|
|
2631
|
+
type: "decisions_archived",
|
|
2632
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2633
|
+
decisions: toArchive
|
|
2634
|
+
};
|
|
2635
|
+
await appendFile5(archivePath, `${JSON.stringify(entry)}
|
|
2636
|
+
`, "utf-8");
|
|
2637
|
+
}
|
|
2638
|
+
const result = changed ? JSON.stringify(parsed, null, 2) : content;
|
|
2639
|
+
const sizeKB = Buffer.byteLength(result, "utf-8") / 1024;
|
|
2640
|
+
if (sizeKB > MAX_SIZE_KB && parsed.notes.length > 200) {
|
|
2641
|
+
const archivePath = path11.join(dir, ARCHIVE_FILE);
|
|
2642
|
+
const entry = {
|
|
2643
|
+
type: "notes_archived",
|
|
2644
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2645
|
+
notes: parsed.notes
|
|
2646
|
+
};
|
|
2647
|
+
await appendFile5(archivePath, `${JSON.stringify(entry)}
|
|
2648
|
+
`, "utf-8");
|
|
2649
|
+
parsed.notes = "(archived \u2014 see memory-archive.jsonl)";
|
|
2650
|
+
return JSON.stringify(parsed, null, 2);
|
|
2651
|
+
}
|
|
2652
|
+
return result;
|
|
2653
|
+
}
|
|
2494
2654
|
|
|
2495
2655
|
// src/supervisor/prompt-builder.ts
|
|
2496
2656
|
function buildHeartbeatPrompt(opts) {
|
|
@@ -2510,7 +2670,12 @@ Available commands (via bash):
|
|
|
2510
2670
|
neo cost --short [--all] check budget
|
|
2511
2671
|
neo agents list available agents
|
|
2512
2672
|
|
|
2513
|
-
IMPORTANT: Always include a <memory>...</memory> block at the end of your response with your updated memory
|
|
2673
|
+
IMPORTANT: Always include a <memory>...</memory> block at the end of your response with your updated memory.
|
|
2674
|
+
|
|
2675
|
+
## Reporting
|
|
2676
|
+
Use the \`mcp__neo__report_progress\` tool to log your decisions, actions and blockers.
|
|
2677
|
+
Always report what you're doing and why \u2014 these logs are your audit trail.
|
|
2678
|
+
Types: "decision" (what you chose), "action" (what you did), "blocker" (what's stuck), "progress" (status update).`);
|
|
2514
2679
|
if (opts.customInstructions) {
|
|
2515
2680
|
sections.push(`## Custom instructions
|
|
2516
2681
|
${opts.customInstructions}`);
|
|
@@ -2539,15 +2704,33 @@ You can use these tools directly to query external systems.`
|
|
|
2539
2704
|
sections.push(`## Active runs
|
|
2540
2705
|
${opts.activeRuns.map((r) => `- ${r}`).join("\n")}`);
|
|
2541
2706
|
}
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2707
|
+
const { messages, webhooks, runCompletions } = opts.grouped;
|
|
2708
|
+
const totalEvents = messages.length + webhooks.length + runCompletions.length;
|
|
2709
|
+
if (totalEvents > 0) {
|
|
2710
|
+
const parts = [];
|
|
2711
|
+
for (const msg of messages) {
|
|
2712
|
+
const countSuffix = msg.count > 1 ? ` (\xD7${msg.count})` : "";
|
|
2713
|
+
parts.push(`**Message from ${msg.from}${countSuffix}**: ${msg.text}`);
|
|
2714
|
+
}
|
|
2715
|
+
for (const evt of webhooks) {
|
|
2716
|
+
parts.push(formatEvent(evt));
|
|
2717
|
+
}
|
|
2718
|
+
for (const evt of runCompletions) {
|
|
2719
|
+
parts.push(formatEvent(evt));
|
|
2720
|
+
}
|
|
2721
|
+
sections.push(`## Pending events (${totalEvents})
|
|
2722
|
+
${parts.join("\n\n")}`);
|
|
2546
2723
|
} else {
|
|
2547
2724
|
sections.push(
|
|
2548
2725
|
"## Pending events\nNo new events. This is an idle heartbeat \u2014 check on active runs if any, or wait."
|
|
2549
2726
|
);
|
|
2550
2727
|
}
|
|
2728
|
+
if (opts.knowledge) {
|
|
2729
|
+
sections.push(`## Reference knowledge (read-only)
|
|
2730
|
+
${opts.knowledge}
|
|
2731
|
+
|
|
2732
|
+
To update knowledge, output a \`<knowledge>...</knowledge>\` block. Only update when reference data changes (API IDs, workspace config, etc.).`);
|
|
2733
|
+
}
|
|
2551
2734
|
sections.push(buildMemorySection(opts.memory, opts.memorySizeKB));
|
|
2552
2735
|
return sections.join("\n\n---\n\n");
|
|
2553
2736
|
}
|
|
@@ -2582,7 +2765,7 @@ function formatEvent(event) {
|
|
|
2582
2765
|
case "webhook":
|
|
2583
2766
|
return `**Webhook** [${event.data.source ?? "unknown"}] ${event.data.event ?? ""}
|
|
2584
2767
|
\`\`\`json
|
|
2585
|
-
${JSON.stringify(event.data.payload ?? {}, null, 2)
|
|
2768
|
+
${JSON.stringify(event.data.payload ?? {}, null, 2)}
|
|
2586
2769
|
\`\`\``;
|
|
2587
2770
|
case "message":
|
|
2588
2771
|
return `**Message from ${event.data.from}**: ${event.data.text}`;
|
|
@@ -2660,15 +2843,27 @@ var HeartbeatLoop = class {
|
|
|
2660
2843
|
await this.sleep(this.config.supervisor.idleIntervalMs);
|
|
2661
2844
|
return;
|
|
2662
2845
|
}
|
|
2663
|
-
const
|
|
2846
|
+
const grouped = this.eventQueue.drainAndGroup();
|
|
2847
|
+
const totalEventCount = grouped.messages.length + grouped.webhooks.length + grouped.runCompletions.length;
|
|
2848
|
+
const idleSkipCount = state?.idleSkipCount ?? 0;
|
|
2849
|
+
if (totalEventCount === 0 && idleSkipCount < this.config.supervisor.idleSkipMax) {
|
|
2850
|
+
await this.updateState({ idleSkipCount: idleSkipCount + 1 });
|
|
2851
|
+
await this.activityLog.log("heartbeat", `Idle skip #${idleSkipCount + 1} \u2014 no events`);
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
if (idleSkipCount > 0) {
|
|
2855
|
+
await this.updateState({ idleSkipCount: 0 });
|
|
2856
|
+
}
|
|
2664
2857
|
const memory = await loadMemory(this.supervisorDir);
|
|
2858
|
+
const knowledge = await loadKnowledge(this.supervisorDir);
|
|
2665
2859
|
const memoryCheck = checkMemorySize(memory);
|
|
2666
2860
|
const mcpServerNames = this.config.mcpServers ? Object.keys(this.config.mcpServers) : [];
|
|
2667
2861
|
const prompt = buildHeartbeatPrompt({
|
|
2668
2862
|
repos: this.config.repos,
|
|
2669
2863
|
memory,
|
|
2864
|
+
knowledge,
|
|
2670
2865
|
memorySizeKB: memoryCheck.sizeKB,
|
|
2671
|
-
|
|
2866
|
+
grouped,
|
|
2672
2867
|
budgetStatus: {
|
|
2673
2868
|
todayUsd: todayCost,
|
|
2674
2869
|
capUsd: this.config.supervisor.dailyCapUsd,
|
|
@@ -2682,8 +2877,10 @@ var HeartbeatLoop = class {
|
|
|
2682
2877
|
});
|
|
2683
2878
|
await this.activityLog.log("heartbeat", `Heartbeat #${state?.heartbeatCount ?? 0} starting`, {
|
|
2684
2879
|
heartbeatId,
|
|
2685
|
-
eventCount:
|
|
2686
|
-
|
|
2880
|
+
eventCount: totalEventCount,
|
|
2881
|
+
messages: grouped.messages.length,
|
|
2882
|
+
webhooks: grouped.webhooks.length,
|
|
2883
|
+
runCompletions: grouped.runCompletions.length
|
|
2687
2884
|
});
|
|
2688
2885
|
const abortController = new AbortController();
|
|
2689
2886
|
this.activeAbort = abortController;
|
|
@@ -2695,16 +2892,33 @@ var HeartbeatLoop = class {
|
|
|
2695
2892
|
let turnCount = 0;
|
|
2696
2893
|
try {
|
|
2697
2894
|
const sdk = await import("@anthropic-ai/claude-agent-sdk");
|
|
2895
|
+
const allowedTools = ["Bash", "Read", "mcp__neo__*"];
|
|
2896
|
+
if (this.config.mcpServers) {
|
|
2897
|
+
for (const name of Object.keys(this.config.mcpServers)) {
|
|
2898
|
+
allowedTools.push(`mcp__${name}__*`);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
const mcpInternalPath = path12.join(
|
|
2902
|
+
path12.dirname(fileURLToPath(import.meta.url)),
|
|
2903
|
+
"mcp-internal.js"
|
|
2904
|
+
);
|
|
2905
|
+
const mcpServers = {
|
|
2906
|
+
neo: {
|
|
2907
|
+
type: "stdio",
|
|
2908
|
+
command: "node",
|
|
2909
|
+
args: [mcpInternalPath],
|
|
2910
|
+
env: { NEO_ACTIVITY_PATH: this.activityLog.filePath }
|
|
2911
|
+
},
|
|
2912
|
+
...this.config.mcpServers ?? {}
|
|
2913
|
+
};
|
|
2698
2914
|
const queryOptions = {
|
|
2699
2915
|
cwd: homedir2(),
|
|
2700
2916
|
maxTurns: 50,
|
|
2701
|
-
allowedTools
|
|
2917
|
+
allowedTools,
|
|
2702
2918
|
permissionMode: "bypassPermissions",
|
|
2703
|
-
allowDangerouslySkipPermissions: true
|
|
2919
|
+
allowDangerouslySkipPermissions: true,
|
|
2920
|
+
mcpServers
|
|
2704
2921
|
};
|
|
2705
|
-
if (this.config.mcpServers) {
|
|
2706
|
-
queryOptions.mcpServers = this.config.mcpServers;
|
|
2707
|
-
}
|
|
2708
2922
|
const stream = sdk.query({ prompt, options: queryOptions });
|
|
2709
2923
|
for await (const message of stream) {
|
|
2710
2924
|
if (abortController.signal.aborted) break;
|
|
@@ -2727,6 +2941,10 @@ var HeartbeatLoop = class {
|
|
|
2727
2941
|
if (newMemory) {
|
|
2728
2942
|
await saveMemory(this.supervisorDir, newMemory);
|
|
2729
2943
|
}
|
|
2944
|
+
const newKnowledge = extractKnowledgeFromResponse(output);
|
|
2945
|
+
if (newKnowledge) {
|
|
2946
|
+
await saveKnowledge(this.supervisorDir, newKnowledge);
|
|
2947
|
+
}
|
|
2730
2948
|
const durationMs = Date.now() - startTime;
|
|
2731
2949
|
await this.updateState({
|
|
2732
2950
|
sessionId: this.sessionId,
|
|
@@ -2745,7 +2963,7 @@ var HeartbeatLoop = class {
|
|
|
2745
2963
|
durationMs,
|
|
2746
2964
|
turnCount,
|
|
2747
2965
|
memoryUpdated: !!newMemory,
|
|
2748
|
-
responseSummary: output
|
|
2966
|
+
responseSummary: output
|
|
2749
2967
|
}
|
|
2750
2968
|
);
|
|
2751
2969
|
}
|
|
@@ -2799,16 +3017,16 @@ var HeartbeatLoop = class {
|
|
|
2799
3017
|
await this.logToolResult(msg, heartbeatId);
|
|
2800
3018
|
}
|
|
2801
3019
|
}
|
|
2802
|
-
/** Log thinking and plan blocks from assistant content. */
|
|
3020
|
+
/** Log thinking and plan blocks from assistant content — no truncation. */
|
|
2803
3021
|
async logContentBlocks(msg, heartbeatId) {
|
|
2804
3022
|
const content = msg.message?.content;
|
|
2805
3023
|
if (!content) return;
|
|
2806
3024
|
for (const block of content) {
|
|
2807
3025
|
if (block.type === "thinking" && block.thinking) {
|
|
2808
|
-
await this.activityLog.log("thinking", block.thinking
|
|
3026
|
+
await this.activityLog.log("thinking", block.thinking, { heartbeatId });
|
|
2809
3027
|
}
|
|
2810
3028
|
if (block.type === "text" && block.text) {
|
|
2811
|
-
await this.activityLog.log("plan", block.text
|
|
3029
|
+
await this.activityLog.log("plan", block.text, { heartbeatId });
|
|
2812
3030
|
break;
|
|
2813
3031
|
}
|
|
2814
3032
|
}
|
|
@@ -2841,7 +3059,7 @@ var HeartbeatLoop = class {
|
|
|
2841
3059
|
|
|
2842
3060
|
// src/supervisor/webhook-server.ts
|
|
2843
3061
|
import { timingSafeEqual } from "crypto";
|
|
2844
|
-
import { appendFile as
|
|
3062
|
+
import { appendFile as appendFile6 } from "fs/promises";
|
|
2845
3063
|
import { createServer } from "http";
|
|
2846
3064
|
var MAX_BODY_SIZE = 1024 * 1024;
|
|
2847
3065
|
var WebhookServer = class {
|
|
@@ -2925,7 +3143,7 @@ var WebhookServer = class {
|
|
|
2925
3143
|
payload: parsed.payload ?? parsed,
|
|
2926
3144
|
receivedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2927
3145
|
};
|
|
2928
|
-
await
|
|
3146
|
+
await appendFile6(this.eventsPath, `${JSON.stringify(event)}
|
|
2929
3147
|
`, "utf-8");
|
|
2930
3148
|
this.onEvent(event);
|
|
2931
3149
|
this.sendJson(res, 200, { ok: true, id: event.id });
|
|
@@ -2984,8 +3202,8 @@ var SupervisorDaemon = class {
|
|
|
2984
3202
|
}
|
|
2985
3203
|
const tempLock = `${lockPath}.${process.pid}`;
|
|
2986
3204
|
await writeFile6(tempLock, String(process.pid), "utf-8");
|
|
2987
|
-
const { rename:
|
|
2988
|
-
await
|
|
3205
|
+
const { rename: rename3 } = await import("fs/promises");
|
|
3206
|
+
await rename3(tempLock, lockPath);
|
|
2989
3207
|
const existingState = await this.readState();
|
|
2990
3208
|
if (existingState?.sessionId && existingState.status !== "stopped") {
|
|
2991
3209
|
this.sessionId = existingState.sessionId;
|
|
@@ -3022,6 +3240,7 @@ var SupervisorDaemon = class {
|
|
|
3022
3240
|
totalCostUsd: existingState?.totalCostUsd ?? 0,
|
|
3023
3241
|
todayCostUsd: existingState?.todayCostUsd ?? 0,
|
|
3024
3242
|
costResetDate: existingState?.costResetDate,
|
|
3243
|
+
idleSkipCount: existingState?.idleSkipCount ?? 0,
|
|
3025
3244
|
status: "running"
|
|
3026
3245
|
});
|
|
3027
3246
|
const shutdown = () => {
|