@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 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
- private readonly filePath;
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
- * Returns empty string if no memory file exists yet.
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
- events: QueuedEvent[];
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 stream = sdk.query({ prompt, options: queryOptions });
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.md";
2466
- var MAX_SIZE_KB = 10;
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 writeFile4(path11.join(dir, MEMORY_FILE), content, "utf-8");
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
- if (opts.events.length > 0) {
2543
- const eventDescriptions = opts.events.map(formatEvent);
2544
- sections.push(`## Pending events (${opts.events.length})
2545
- ${eventDescriptions.join("\n\n")}`);
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).slice(0, 2e3)}
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 events = this.eventQueue.drain();
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
- events,
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: events.length,
2686
- triggeredBy: events.map((e) => e.kind)
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: ["Bash", "Read"],
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.slice(0, 500)
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.slice(0, 500), { heartbeatId });
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.slice(0, 500), { heartbeatId });
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 appendFile5 } from "fs/promises";
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 appendFile5(this.eventsPath, `${JSON.stringify(event)}
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: rename2 } = await import("fs/promises");
2988
- await rename2(tempLock, lockPath);
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 = () => {