@neotx/core 0.1.0-alpha.0 → 0.1.0-alpha.2
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 -7
- package/dist/index.js +248 -31
- 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;
|
|
@@ -1002,7 +1009,6 @@ declare function runWithRecovery(options: RecoveryOptions): Promise<SessionResul
|
|
|
1002
1009
|
|
|
1003
1010
|
declare const supervisorDaemonStateSchema: z.ZodObject<{
|
|
1004
1011
|
pid: z.ZodNumber;
|
|
1005
|
-
tmuxSession: z.ZodString;
|
|
1006
1012
|
sessionId: z.ZodString;
|
|
1007
1013
|
port: z.ZodNumber;
|
|
1008
1014
|
cwd: z.ZodString;
|
|
@@ -1012,6 +1018,7 @@ declare const supervisorDaemonStateSchema: z.ZodObject<{
|
|
|
1012
1018
|
totalCostUsd: z.ZodDefault<z.ZodNumber>;
|
|
1013
1019
|
todayCostUsd: z.ZodDefault<z.ZodNumber>;
|
|
1014
1020
|
costResetDate: z.ZodOptional<z.ZodString>;
|
|
1021
|
+
idleSkipCount: z.ZodDefault<z.ZodNumber>;
|
|
1015
1022
|
status: z.ZodDefault<z.ZodEnum<{
|
|
1016
1023
|
running: "running";
|
|
1017
1024
|
draining: "draining";
|
|
@@ -1072,7 +1079,7 @@ type QueuedEvent = {
|
|
|
1072
1079
|
};
|
|
1073
1080
|
|
|
1074
1081
|
declare class ActivityLog {
|
|
1075
|
-
|
|
1082
|
+
readonly filePath: string;
|
|
1076
1083
|
private readonly dir;
|
|
1077
1084
|
constructor(dir: string);
|
|
1078
1085
|
/**
|
|
@@ -1121,6 +1128,16 @@ declare class SupervisorDaemon {
|
|
|
1121
1128
|
interface EventQueueOptions {
|
|
1122
1129
|
maxEventsPerSec: number;
|
|
1123
1130
|
}
|
|
1131
|
+
interface GroupedMessage {
|
|
1132
|
+
text: string;
|
|
1133
|
+
from: string;
|
|
1134
|
+
count: number;
|
|
1135
|
+
}
|
|
1136
|
+
interface GroupedEvents {
|
|
1137
|
+
messages: GroupedMessage[];
|
|
1138
|
+
webhooks: QueuedEvent[];
|
|
1139
|
+
runCompletions: QueuedEvent[];
|
|
1140
|
+
}
|
|
1124
1141
|
/**
|
|
1125
1142
|
* In-memory event queue with deduplication, rate limiting, and file watching.
|
|
1126
1143
|
*
|
|
@@ -1151,6 +1168,11 @@ declare class EventQueue {
|
|
|
1151
1168
|
* Drain all queued events and return them. Clears the queue.
|
|
1152
1169
|
*/
|
|
1153
1170
|
drain(): QueuedEvent[];
|
|
1171
|
+
/**
|
|
1172
|
+
* Drain and group events: deduplicates messages by content,
|
|
1173
|
+
* keeps webhooks and run completions separate.
|
|
1174
|
+
*/
|
|
1175
|
+
drainAndGroup(): GroupedEvents;
|
|
1154
1176
|
size(): number;
|
|
1155
1177
|
/**
|
|
1156
1178
|
* Start watching inbox.jsonl and events.jsonl for new entries.
|
|
@@ -1224,7 +1246,7 @@ declare class HeartbeatLoop {
|
|
|
1224
1246
|
private loadInstructions;
|
|
1225
1247
|
/** Route a single SDK stream message to the appropriate log handler. */
|
|
1226
1248
|
private logStreamMessage;
|
|
1227
|
-
/** Log thinking and plan blocks from assistant content. */
|
|
1249
|
+
/** Log thinking and plan blocks from assistant content — no truncation. */
|
|
1228
1250
|
private logContentBlocks;
|
|
1229
1251
|
/** Log tool use events — distinguish MCP tools from built-in tools. */
|
|
1230
1252
|
private logToolUse;
|
|
@@ -1235,17 +1257,16 @@ declare class HeartbeatLoop {
|
|
|
1235
1257
|
|
|
1236
1258
|
/**
|
|
1237
1259
|
* Load the supervisor memory from disk.
|
|
1238
|
-
*
|
|
1260
|
+
* Migrates from legacy memory.md if needed.
|
|
1239
1261
|
*/
|
|
1240
1262
|
declare function loadMemory(dir: string): Promise<string>;
|
|
1241
1263
|
/**
|
|
1242
1264
|
* Save the supervisor memory to disk (full overwrite).
|
|
1265
|
+
* Automatically compacts if needed.
|
|
1243
1266
|
*/
|
|
1244
1267
|
declare function saveMemory(dir: string, content: string): Promise<void>;
|
|
1245
1268
|
/**
|
|
1246
1269
|
* 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
1270
|
*/
|
|
1250
1271
|
declare function extractMemoryFromResponse(response: string): string | null;
|
|
1251
1272
|
/**
|
|
@@ -1259,8 +1280,9 @@ declare function checkMemorySize(content: string): {
|
|
|
1259
1280
|
interface HeartbeatPromptOptions {
|
|
1260
1281
|
repos: RepoConfig[];
|
|
1261
1282
|
memory: string;
|
|
1283
|
+
knowledge: string;
|
|
1262
1284
|
memorySizeKB: number;
|
|
1263
|
-
|
|
1285
|
+
grouped: GroupedEvents;
|
|
1264
1286
|
budgetStatus: {
|
|
1265
1287
|
todayUsd: number;
|
|
1266
1288
|
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);
|
|
@@ -2131,7 +2164,6 @@ function objectDepth(obj, current = 0) {
|
|
|
2131
2164
|
import { z as z4 } from "zod";
|
|
2132
2165
|
var supervisorDaemonStateSchema = z4.object({
|
|
2133
2166
|
pid: z4.number(),
|
|
2134
|
-
tmuxSession: z4.string(),
|
|
2135
2167
|
sessionId: z4.string(),
|
|
2136
2168
|
port: z4.number(),
|
|
2137
2169
|
cwd: z4.string(),
|
|
@@ -2141,6 +2173,7 @@ var supervisorDaemonStateSchema = z4.object({
|
|
|
2141
2173
|
totalCostUsd: z4.number().default(0),
|
|
2142
2174
|
todayCostUsd: z4.number().default(0),
|
|
2143
2175
|
costResetDate: z4.string().optional(),
|
|
2176
|
+
idleSkipCount: z4.number().default(0),
|
|
2144
2177
|
status: z4.enum(["running", "draining", "stopped"]).default("running")
|
|
2145
2178
|
});
|
|
2146
2179
|
var webhookIncomingEventSchema = z4.object({
|
|
@@ -2302,6 +2335,36 @@ var EventQueue = class {
|
|
|
2302
2335
|
this.queue.length = 0;
|
|
2303
2336
|
return events;
|
|
2304
2337
|
}
|
|
2338
|
+
/**
|
|
2339
|
+
* Drain and group events: deduplicates messages by content,
|
|
2340
|
+
* keeps webhooks and run completions separate.
|
|
2341
|
+
*/
|
|
2342
|
+
drainAndGroup() {
|
|
2343
|
+
const events = this.drain();
|
|
2344
|
+
const messageMap = /* @__PURE__ */ new Map();
|
|
2345
|
+
const webhooks = [];
|
|
2346
|
+
const runCompletions = [];
|
|
2347
|
+
for (const event of events) {
|
|
2348
|
+
if (event.kind === "message") {
|
|
2349
|
+
const key = event.data.text.trim().toLowerCase();
|
|
2350
|
+
const existing = messageMap.get(key);
|
|
2351
|
+
if (existing) {
|
|
2352
|
+
existing.count++;
|
|
2353
|
+
} else {
|
|
2354
|
+
messageMap.set(key, { text: event.data.text, from: event.data.from, count: 1 });
|
|
2355
|
+
}
|
|
2356
|
+
} else if (event.kind === "webhook") {
|
|
2357
|
+
webhooks.push(event);
|
|
2358
|
+
} else {
|
|
2359
|
+
runCompletions.push(event);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
return {
|
|
2363
|
+
messages: [...messageMap.values()],
|
|
2364
|
+
webhooks,
|
|
2365
|
+
runCompletions
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2305
2368
|
size() {
|
|
2306
2369
|
return this.queue.length;
|
|
2307
2370
|
}
|
|
@@ -2458,21 +2521,74 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
2458
2521
|
import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
|
|
2459
2522
|
import { homedir as homedir2 } from "os";
|
|
2460
2523
|
import path12 from "path";
|
|
2524
|
+
import { fileURLToPath } from "url";
|
|
2461
2525
|
|
|
2462
2526
|
// src/supervisor/memory.ts
|
|
2463
|
-
import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
2527
|
+
import { appendFile as appendFile5, readFile as readFile8, rename as rename2, writeFile as writeFile4 } from "fs/promises";
|
|
2464
2528
|
import path11 from "path";
|
|
2465
|
-
var MEMORY_FILE = "memory.
|
|
2466
|
-
var
|
|
2529
|
+
var MEMORY_FILE = "memory.json";
|
|
2530
|
+
var KNOWLEDGE_FILE = "knowledge.json";
|
|
2531
|
+
var ARCHIVE_FILE = "memory-archive.jsonl";
|
|
2532
|
+
var LEGACY_FILE = "memory.md";
|
|
2533
|
+
var MAX_SIZE_KB = 6;
|
|
2534
|
+
var MAX_DECISIONS = 10;
|
|
2535
|
+
function parseStructuredMemory(raw) {
|
|
2536
|
+
if (!raw.trim()) {
|
|
2537
|
+
return emptyMemory();
|
|
2538
|
+
}
|
|
2539
|
+
try {
|
|
2540
|
+
const parsed = JSON.parse(raw);
|
|
2541
|
+
return {
|
|
2542
|
+
activeWork: parsed.activeWork ?? [],
|
|
2543
|
+
blockers: parsed.blockers ?? [],
|
|
2544
|
+
repoNotes: parsed.repoNotes ?? {},
|
|
2545
|
+
recentDecisions: parsed.recentDecisions ?? [],
|
|
2546
|
+
trackerSync: parsed.trackerSync ?? {},
|
|
2547
|
+
notes: parsed.notes ?? ""
|
|
2548
|
+
};
|
|
2549
|
+
} catch {
|
|
2550
|
+
return { ...emptyMemory(), notes: raw };
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
function emptyMemory() {
|
|
2554
|
+
return {
|
|
2555
|
+
activeWork: [],
|
|
2556
|
+
blockers: [],
|
|
2557
|
+
repoNotes: {},
|
|
2558
|
+
recentDecisions: [],
|
|
2559
|
+
trackerSync: {},
|
|
2560
|
+
notes: ""
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
async function loadKnowledge(dir) {
|
|
2564
|
+
try {
|
|
2565
|
+
return await readFile8(path11.join(dir, KNOWLEDGE_FILE), "utf-8");
|
|
2566
|
+
} catch {
|
|
2567
|
+
return "";
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
async function saveKnowledge(dir, content) {
|
|
2571
|
+
await writeFile4(path11.join(dir, KNOWLEDGE_FILE), content, "utf-8");
|
|
2572
|
+
}
|
|
2467
2573
|
async function loadMemory(dir) {
|
|
2468
2574
|
try {
|
|
2469
2575
|
return await readFile8(path11.join(dir, MEMORY_FILE), "utf-8");
|
|
2470
2576
|
} catch {
|
|
2471
|
-
return "";
|
|
2472
2577
|
}
|
|
2578
|
+
try {
|
|
2579
|
+
const legacy = await readFile8(path11.join(dir, LEGACY_FILE), "utf-8");
|
|
2580
|
+
if (legacy.trim()) {
|
|
2581
|
+
await writeFile4(path11.join(dir, MEMORY_FILE), legacy, "utf-8");
|
|
2582
|
+
await rename2(path11.join(dir, LEGACY_FILE), path11.join(dir, `${LEGACY_FILE}.bak`));
|
|
2583
|
+
return legacy;
|
|
2584
|
+
}
|
|
2585
|
+
} catch {
|
|
2586
|
+
}
|
|
2587
|
+
return "";
|
|
2473
2588
|
}
|
|
2474
2589
|
async function saveMemory(dir, content) {
|
|
2475
|
-
await
|
|
2590
|
+
const compacted = await compactMemory(dir, content);
|
|
2591
|
+
await writeFile4(path11.join(dir, MEMORY_FILE), compacted, "utf-8");
|
|
2476
2592
|
}
|
|
2477
2593
|
function extractMemoryFromResponse(response) {
|
|
2478
2594
|
const match = /<memory>([\s\S]*?)<\/memory>/i.exec(response);
|
|
@@ -2487,10 +2603,53 @@ function extractMemoryFromResponse(response) {
|
|
|
2487
2603
|
}
|
|
2488
2604
|
return content;
|
|
2489
2605
|
}
|
|
2606
|
+
function extractKnowledgeFromResponse(response) {
|
|
2607
|
+
const match = /<knowledge>([\s\S]*?)<\/knowledge>/i.exec(response);
|
|
2608
|
+
if (!match?.[1]) return null;
|
|
2609
|
+
return match[1].trim();
|
|
2610
|
+
}
|
|
2490
2611
|
function checkMemorySize(content) {
|
|
2491
2612
|
const sizeKB = Buffer.byteLength(content, "utf-8") / 1024;
|
|
2492
2613
|
return { ok: sizeKB <= MAX_SIZE_KB, sizeKB: Math.round(sizeKB * 10) / 10 };
|
|
2493
2614
|
}
|
|
2615
|
+
async function compactMemory(dir, content) {
|
|
2616
|
+
if (!content.startsWith("{")) return content;
|
|
2617
|
+
let parsed;
|
|
2618
|
+
try {
|
|
2619
|
+
parsed = parseStructuredMemory(content);
|
|
2620
|
+
} catch {
|
|
2621
|
+
return content;
|
|
2622
|
+
}
|
|
2623
|
+
let changed = false;
|
|
2624
|
+
if (parsed.recentDecisions.length > MAX_DECISIONS) {
|
|
2625
|
+
const toArchive = parsed.recentDecisions.slice(0, -MAX_DECISIONS);
|
|
2626
|
+
parsed.recentDecisions = parsed.recentDecisions.slice(-MAX_DECISIONS);
|
|
2627
|
+
changed = true;
|
|
2628
|
+
const archivePath = path11.join(dir, ARCHIVE_FILE);
|
|
2629
|
+
const entry = {
|
|
2630
|
+
type: "decisions_archived",
|
|
2631
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2632
|
+
decisions: toArchive
|
|
2633
|
+
};
|
|
2634
|
+
await appendFile5(archivePath, `${JSON.stringify(entry)}
|
|
2635
|
+
`, "utf-8");
|
|
2636
|
+
}
|
|
2637
|
+
const result = changed ? JSON.stringify(parsed, null, 2) : content;
|
|
2638
|
+
const sizeKB = Buffer.byteLength(result, "utf-8") / 1024;
|
|
2639
|
+
if (sizeKB > MAX_SIZE_KB && parsed.notes.length > 200) {
|
|
2640
|
+
const archivePath = path11.join(dir, ARCHIVE_FILE);
|
|
2641
|
+
const entry = {
|
|
2642
|
+
type: "notes_archived",
|
|
2643
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2644
|
+
notes: parsed.notes
|
|
2645
|
+
};
|
|
2646
|
+
await appendFile5(archivePath, `${JSON.stringify(entry)}
|
|
2647
|
+
`, "utf-8");
|
|
2648
|
+
parsed.notes = "(archived \u2014 see memory-archive.jsonl)";
|
|
2649
|
+
return JSON.stringify(parsed, null, 2);
|
|
2650
|
+
}
|
|
2651
|
+
return result;
|
|
2652
|
+
}
|
|
2494
2653
|
|
|
2495
2654
|
// src/supervisor/prompt-builder.ts
|
|
2496
2655
|
function buildHeartbeatPrompt(opts) {
|
|
@@ -2510,7 +2669,12 @@ Available commands (via bash):
|
|
|
2510
2669
|
neo cost --short [--all] check budget
|
|
2511
2670
|
neo agents list available agents
|
|
2512
2671
|
|
|
2513
|
-
IMPORTANT: Always include a <memory>...</memory> block at the end of your response with your updated memory
|
|
2672
|
+
IMPORTANT: Always include a <memory>...</memory> block at the end of your response with your updated memory.
|
|
2673
|
+
|
|
2674
|
+
## Reporting
|
|
2675
|
+
Use the \`mcp__neo__report_progress\` tool to log your decisions, actions and blockers.
|
|
2676
|
+
Always report what you're doing and why \u2014 these logs are your audit trail.
|
|
2677
|
+
Types: "decision" (what you chose), "action" (what you did), "blocker" (what's stuck), "progress" (status update).`);
|
|
2514
2678
|
if (opts.customInstructions) {
|
|
2515
2679
|
sections.push(`## Custom instructions
|
|
2516
2680
|
${opts.customInstructions}`);
|
|
@@ -2539,15 +2703,33 @@ You can use these tools directly to query external systems.`
|
|
|
2539
2703
|
sections.push(`## Active runs
|
|
2540
2704
|
${opts.activeRuns.map((r) => `- ${r}`).join("\n")}`);
|
|
2541
2705
|
}
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2706
|
+
const { messages, webhooks, runCompletions } = opts.grouped;
|
|
2707
|
+
const totalEvents = messages.length + webhooks.length + runCompletions.length;
|
|
2708
|
+
if (totalEvents > 0) {
|
|
2709
|
+
const parts = [];
|
|
2710
|
+
for (const msg of messages) {
|
|
2711
|
+
const countSuffix = msg.count > 1 ? ` (\xD7${msg.count})` : "";
|
|
2712
|
+
parts.push(`**Message from ${msg.from}${countSuffix}**: ${msg.text}`);
|
|
2713
|
+
}
|
|
2714
|
+
for (const evt of webhooks) {
|
|
2715
|
+
parts.push(formatEvent(evt));
|
|
2716
|
+
}
|
|
2717
|
+
for (const evt of runCompletions) {
|
|
2718
|
+
parts.push(formatEvent(evt));
|
|
2719
|
+
}
|
|
2720
|
+
sections.push(`## Pending events (${totalEvents})
|
|
2721
|
+
${parts.join("\n\n")}`);
|
|
2546
2722
|
} else {
|
|
2547
2723
|
sections.push(
|
|
2548
2724
|
"## Pending events\nNo new events. This is an idle heartbeat \u2014 check on active runs if any, or wait."
|
|
2549
2725
|
);
|
|
2550
2726
|
}
|
|
2727
|
+
if (opts.knowledge) {
|
|
2728
|
+
sections.push(`## Reference knowledge (read-only)
|
|
2729
|
+
${opts.knowledge}
|
|
2730
|
+
|
|
2731
|
+
To update knowledge, output a \`<knowledge>...</knowledge>\` block. Only update when reference data changes (API IDs, workspace config, etc.).`);
|
|
2732
|
+
}
|
|
2551
2733
|
sections.push(buildMemorySection(opts.memory, opts.memorySizeKB));
|
|
2552
2734
|
return sections.join("\n\n---\n\n");
|
|
2553
2735
|
}
|
|
@@ -2582,7 +2764,7 @@ function formatEvent(event) {
|
|
|
2582
2764
|
case "webhook":
|
|
2583
2765
|
return `**Webhook** [${event.data.source ?? "unknown"}] ${event.data.event ?? ""}
|
|
2584
2766
|
\`\`\`json
|
|
2585
|
-
${JSON.stringify(event.data.payload ?? {}, null, 2)
|
|
2767
|
+
${JSON.stringify(event.data.payload ?? {}, null, 2)}
|
|
2586
2768
|
\`\`\``;
|
|
2587
2769
|
case "message":
|
|
2588
2770
|
return `**Message from ${event.data.from}**: ${event.data.text}`;
|
|
@@ -2660,15 +2842,27 @@ var HeartbeatLoop = class {
|
|
|
2660
2842
|
await this.sleep(this.config.supervisor.idleIntervalMs);
|
|
2661
2843
|
return;
|
|
2662
2844
|
}
|
|
2663
|
-
const
|
|
2845
|
+
const grouped = this.eventQueue.drainAndGroup();
|
|
2846
|
+
const totalEventCount = grouped.messages.length + grouped.webhooks.length + grouped.runCompletions.length;
|
|
2847
|
+
const idleSkipCount = state?.idleSkipCount ?? 0;
|
|
2848
|
+
if (totalEventCount === 0 && idleSkipCount < this.config.supervisor.idleSkipMax) {
|
|
2849
|
+
await this.updateState({ idleSkipCount: idleSkipCount + 1 });
|
|
2850
|
+
await this.activityLog.log("heartbeat", `Idle skip #${idleSkipCount + 1} \u2014 no events`);
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
if (idleSkipCount > 0) {
|
|
2854
|
+
await this.updateState({ idleSkipCount: 0 });
|
|
2855
|
+
}
|
|
2664
2856
|
const memory = await loadMemory(this.supervisorDir);
|
|
2857
|
+
const knowledge = await loadKnowledge(this.supervisorDir);
|
|
2665
2858
|
const memoryCheck = checkMemorySize(memory);
|
|
2666
2859
|
const mcpServerNames = this.config.mcpServers ? Object.keys(this.config.mcpServers) : [];
|
|
2667
2860
|
const prompt = buildHeartbeatPrompt({
|
|
2668
2861
|
repos: this.config.repos,
|
|
2669
2862
|
memory,
|
|
2863
|
+
knowledge,
|
|
2670
2864
|
memorySizeKB: memoryCheck.sizeKB,
|
|
2671
|
-
|
|
2865
|
+
grouped,
|
|
2672
2866
|
budgetStatus: {
|
|
2673
2867
|
todayUsd: todayCost,
|
|
2674
2868
|
capUsd: this.config.supervisor.dailyCapUsd,
|
|
@@ -2682,8 +2876,10 @@ var HeartbeatLoop = class {
|
|
|
2682
2876
|
});
|
|
2683
2877
|
await this.activityLog.log("heartbeat", `Heartbeat #${state?.heartbeatCount ?? 0} starting`, {
|
|
2684
2878
|
heartbeatId,
|
|
2685
|
-
eventCount:
|
|
2686
|
-
|
|
2879
|
+
eventCount: totalEventCount,
|
|
2880
|
+
messages: grouped.messages.length,
|
|
2881
|
+
webhooks: grouped.webhooks.length,
|
|
2882
|
+
runCompletions: grouped.runCompletions.length
|
|
2687
2883
|
});
|
|
2688
2884
|
const abortController = new AbortController();
|
|
2689
2885
|
this.activeAbort = abortController;
|
|
@@ -2695,16 +2891,33 @@ var HeartbeatLoop = class {
|
|
|
2695
2891
|
let turnCount = 0;
|
|
2696
2892
|
try {
|
|
2697
2893
|
const sdk = await import("@anthropic-ai/claude-agent-sdk");
|
|
2894
|
+
const allowedTools = ["Bash", "Read", "mcp__neo__*"];
|
|
2895
|
+
if (this.config.mcpServers) {
|
|
2896
|
+
for (const name of Object.keys(this.config.mcpServers)) {
|
|
2897
|
+
allowedTools.push(`mcp__${name}__*`);
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
const mcpInternalPath = path12.join(
|
|
2901
|
+
path12.dirname(fileURLToPath(import.meta.url)),
|
|
2902
|
+
"mcp-internal.js"
|
|
2903
|
+
);
|
|
2904
|
+
const mcpServers = {
|
|
2905
|
+
neo: {
|
|
2906
|
+
type: "stdio",
|
|
2907
|
+
command: "node",
|
|
2908
|
+
args: [mcpInternalPath],
|
|
2909
|
+
env: { NEO_ACTIVITY_PATH: this.activityLog.filePath }
|
|
2910
|
+
},
|
|
2911
|
+
...this.config.mcpServers ?? {}
|
|
2912
|
+
};
|
|
2698
2913
|
const queryOptions = {
|
|
2699
2914
|
cwd: homedir2(),
|
|
2700
2915
|
maxTurns: 50,
|
|
2701
|
-
allowedTools
|
|
2916
|
+
allowedTools,
|
|
2702
2917
|
permissionMode: "bypassPermissions",
|
|
2703
|
-
allowDangerouslySkipPermissions: true
|
|
2918
|
+
allowDangerouslySkipPermissions: true,
|
|
2919
|
+
mcpServers
|
|
2704
2920
|
};
|
|
2705
|
-
if (this.config.mcpServers) {
|
|
2706
|
-
queryOptions.mcpServers = this.config.mcpServers;
|
|
2707
|
-
}
|
|
2708
2921
|
const stream = sdk.query({ prompt, options: queryOptions });
|
|
2709
2922
|
for await (const message of stream) {
|
|
2710
2923
|
if (abortController.signal.aborted) break;
|
|
@@ -2727,6 +2940,10 @@ var HeartbeatLoop = class {
|
|
|
2727
2940
|
if (newMemory) {
|
|
2728
2941
|
await saveMemory(this.supervisorDir, newMemory);
|
|
2729
2942
|
}
|
|
2943
|
+
const newKnowledge = extractKnowledgeFromResponse(output);
|
|
2944
|
+
if (newKnowledge) {
|
|
2945
|
+
await saveKnowledge(this.supervisorDir, newKnowledge);
|
|
2946
|
+
}
|
|
2730
2947
|
const durationMs = Date.now() - startTime;
|
|
2731
2948
|
await this.updateState({
|
|
2732
2949
|
sessionId: this.sessionId,
|
|
@@ -2745,7 +2962,7 @@ var HeartbeatLoop = class {
|
|
|
2745
2962
|
durationMs,
|
|
2746
2963
|
turnCount,
|
|
2747
2964
|
memoryUpdated: !!newMemory,
|
|
2748
|
-
responseSummary: output
|
|
2965
|
+
responseSummary: output
|
|
2749
2966
|
}
|
|
2750
2967
|
);
|
|
2751
2968
|
}
|
|
@@ -2799,16 +3016,16 @@ var HeartbeatLoop = class {
|
|
|
2799
3016
|
await this.logToolResult(msg, heartbeatId);
|
|
2800
3017
|
}
|
|
2801
3018
|
}
|
|
2802
|
-
/** Log thinking and plan blocks from assistant content. */
|
|
3019
|
+
/** Log thinking and plan blocks from assistant content — no truncation. */
|
|
2803
3020
|
async logContentBlocks(msg, heartbeatId) {
|
|
2804
3021
|
const content = msg.message?.content;
|
|
2805
3022
|
if (!content) return;
|
|
2806
3023
|
for (const block of content) {
|
|
2807
3024
|
if (block.type === "thinking" && block.thinking) {
|
|
2808
|
-
await this.activityLog.log("thinking", block.thinking
|
|
3025
|
+
await this.activityLog.log("thinking", block.thinking, { heartbeatId });
|
|
2809
3026
|
}
|
|
2810
3027
|
if (block.type === "text" && block.text) {
|
|
2811
|
-
await this.activityLog.log("plan", block.text
|
|
3028
|
+
await this.activityLog.log("plan", block.text, { heartbeatId });
|
|
2812
3029
|
break;
|
|
2813
3030
|
}
|
|
2814
3031
|
}
|
|
@@ -2841,7 +3058,7 @@ var HeartbeatLoop = class {
|
|
|
2841
3058
|
|
|
2842
3059
|
// src/supervisor/webhook-server.ts
|
|
2843
3060
|
import { timingSafeEqual } from "crypto";
|
|
2844
|
-
import { appendFile as
|
|
3061
|
+
import { appendFile as appendFile6 } from "fs/promises";
|
|
2845
3062
|
import { createServer } from "http";
|
|
2846
3063
|
var MAX_BODY_SIZE = 1024 * 1024;
|
|
2847
3064
|
var WebhookServer = class {
|
|
@@ -2925,7 +3142,7 @@ var WebhookServer = class {
|
|
|
2925
3142
|
payload: parsed.payload ?? parsed,
|
|
2926
3143
|
receivedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2927
3144
|
};
|
|
2928
|
-
await
|
|
3145
|
+
await appendFile6(this.eventsPath, `${JSON.stringify(event)}
|
|
2929
3146
|
`, "utf-8");
|
|
2930
3147
|
this.onEvent(event);
|
|
2931
3148
|
this.sendJson(res, 200, { ok: true, id: event.id });
|
|
@@ -2984,8 +3201,8 @@ var SupervisorDaemon = class {
|
|
|
2984
3201
|
}
|
|
2985
3202
|
const tempLock = `${lockPath}.${process.pid}`;
|
|
2986
3203
|
await writeFile6(tempLock, String(process.pid), "utf-8");
|
|
2987
|
-
const { rename:
|
|
2988
|
-
await
|
|
3204
|
+
const { rename: rename3 } = await import("fs/promises");
|
|
3205
|
+
await rename3(tempLock, lockPath);
|
|
2989
3206
|
const existingState = await this.readState();
|
|
2990
3207
|
if (existingState?.sessionId && existingState.status !== "stopped") {
|
|
2991
3208
|
this.sessionId = existingState.sessionId;
|
|
@@ -3012,7 +3229,6 @@ var SupervisorDaemon = class {
|
|
|
3012
3229
|
await this.webhookServer.start();
|
|
3013
3230
|
await this.writeState({
|
|
3014
3231
|
pid: process.pid,
|
|
3015
|
-
tmuxSession: `neo-${this.name}`,
|
|
3016
3232
|
sessionId: this.sessionId,
|
|
3017
3233
|
port: this.config.supervisor.port,
|
|
3018
3234
|
cwd: homedir3(),
|
|
@@ -3022,6 +3238,7 @@ var SupervisorDaemon = class {
|
|
|
3022
3238
|
totalCostUsd: existingState?.totalCostUsd ?? 0,
|
|
3023
3239
|
todayCostUsd: existingState?.todayCostUsd ?? 0,
|
|
3024
3240
|
costResetDate: existingState?.costResetDate,
|
|
3241
|
+
idleSkipCount: existingState?.idleSkipCount ?? 0,
|
|
3025
3242
|
status: "running"
|
|
3026
3243
|
});
|
|
3027
3244
|
const shutdown = () => {
|