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