@xqli02/mneme 0.1.8 → 0.1.9

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/bin/mneme.mjs CHANGED
@@ -60,6 +60,7 @@ const MNEME_COMMANDS = new Set([
60
60
  "up",
61
61
  "down",
62
62
  "ps",
63
+ "restart",
63
64
  "version",
64
65
  "--version",
65
66
  "-v",
@@ -142,6 +143,11 @@ switch (command) {
142
143
  await server(["status", ...args.slice(1)]);
143
144
  break;
144
145
  }
146
+ case "restart": {
147
+ const { server } = await import("../src/commands/server.mjs");
148
+ await server(["restart", ...args.slice(1)]);
149
+ break;
150
+ }
145
151
  case "version":
146
152
  case "--version":
147
153
  case "-v":
@@ -171,10 +177,10 @@ Usage:
171
177
  mneme compact Pre-compaction persistence check
172
178
 
173
179
  ${bold("Servers (dolt + opencode):")}
174
- mneme up [TARGET] Start server(s) (TARGET: dolt|opencode|all)
175
- mneme down [TARGET] Stop server(s)
176
- mneme ps [TARGET] Show server status
177
- mneme server restart [TARGET] Restart server(s)
180
+ mneme up [TARGET] Start server(s) (TARGET: dolt|opencode|all)
181
+ mneme down [TARGET] Stop server(s)
182
+ mneme ps [TARGET] Show server status
183
+ mneme restart [TARGET] Restart server(s)
178
184
 
179
185
  ${bold("Task management (beads):")}
180
186
  mneme ready Show tasks with no blockers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xqli02/mneme",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Three-layer memory architecture for AI coding agents (Ledger + Beads + OpenCode)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -382,11 +382,16 @@ function createInputQueue() {
382
382
  /**
383
383
  * Subscribe to SSE events and display agent output in real-time.
384
384
  * Returns an object with methods to control display and detect turn completion.
385
+ *
386
+ * Also tracks `lastOutputTime` so callers can detect stalls.
385
387
  */
386
388
  function createEventDisplay(client) {
387
389
  let running = false;
390
+ let connected = false;
388
391
  let turnResolve = null;
389
392
  let currentRole = null; // "planner" | "executor" — for display prefixing
393
+ let lastOutputTime = 0; // Date.now() of last SSE output
394
+ let hasReceivedAny = false; // true once any event arrives
390
395
 
391
396
  // Track incremental text and tool display state
392
397
  const printedTextLengths = new Map();
@@ -396,15 +401,26 @@ function createEventDisplay(client) {
396
401
  running = true;
397
402
  try {
398
403
  const iterator = await client.events.subscribe();
404
+ connected = true;
405
+ hasReceivedAny = false;
406
+ log.ok("SSE event stream connected");
399
407
  for await (const event of iterator) {
400
408
  if (!running) break;
409
+ if (!hasReceivedAny) hasReceivedAny = true;
401
410
  handleEvent(event);
402
411
  }
403
412
  } catch (err) {
413
+ connected = false;
404
414
  if (running) {
405
415
  console.error(
406
- color.dim(`\n [events] Stream ended: ${err.message}`),
416
+ color.dim(`\n [events] Stream error: ${err.message}`),
407
417
  );
418
+ // Try to reconnect after a brief delay
419
+ await sleep(2000);
420
+ if (running) {
421
+ log.info("Reconnecting SSE...");
422
+ start().catch(() => {});
423
+ }
408
424
  }
409
425
  }
410
426
  }
@@ -425,12 +441,14 @@ function createEventDisplay(client) {
425
441
  if (newText) {
426
442
  process.stdout.write(newText);
427
443
  printedTextLengths.set(partId, part.text.length);
444
+ lastOutputTime = Date.now();
428
445
  }
429
446
  } else if (
430
447
  part.type === "tool-invocation" ||
431
448
  part.type === "tool-result"
432
449
  ) {
433
450
  displayToolPart(part, partId);
451
+ lastOutputTime = Date.now();
434
452
  }
435
453
  break;
436
454
  }
@@ -525,7 +543,15 @@ function createEventDisplay(client) {
525
543
  }
526
544
  }
527
545
 
528
- return { start, stop, waitForTurnEnd, resetTurn };
546
+ return {
547
+ start,
548
+ stop,
549
+ waitForTurnEnd,
550
+ resetTurn,
551
+ get lastOutputTime() { return lastOutputTime; },
552
+ get connected() { return connected; },
553
+ get hasReceivedAny() { return hasReceivedAny; },
554
+ };
529
555
  }
530
556
 
531
557
  // ── Turn execution ──────────────────────────────────────────────────────────
@@ -533,6 +559,8 @@ function createEventDisplay(client) {
533
559
  /**
534
560
  * Send a message and wait for the turn to complete.
535
561
  * Handles /abort and /quit from input queue during execution.
562
+ * Prints heartbeat every 15s when no output is flowing.
563
+ * Warns at 30s of silence, auto-aborts at 120s of silence.
536
564
  *
537
565
  * @returns {{ status: string, aborted: boolean, quit: boolean }}
538
566
  */
@@ -556,15 +584,21 @@ async function executeTurn(
556
584
  // Send async — returns immediately
557
585
  await client.session.promptAsync(sessionId, body);
558
586
 
559
- // Race: SSE turn completion vs user commands
587
+ const HEARTBEAT_INTERVAL = 15_000; // print elapsed every 15s of silence
588
+ const SILENCE_WARN = 30_000; // warn after 30s of no output
589
+ const SILENCE_ABORT = 120_000; // auto-abort after 120s of no output
590
+ const turnStartTime = Date.now();
591
+
592
+ // Race: SSE turn completion vs user commands vs silence timeout
560
593
  return new Promise((resolve) => {
561
594
  let resolved = false;
595
+ let warnedSilence = false;
562
596
 
563
597
  const done = (result) => {
564
598
  if (resolved) return;
565
599
  resolved = true;
566
600
  clearInterval(pollId);
567
- clearTimeout(safetyId);
601
+ clearInterval(heartbeatId);
568
602
  resolve(result);
569
603
  };
570
604
 
@@ -573,6 +607,41 @@ async function executeTurn(
573
607
  done({ status, aborted: false, quit: false });
574
608
  });
575
609
 
610
+ // Heartbeat: show elapsed time when no output is flowing
611
+ const heartbeatId = setInterval(() => {
612
+ if (resolved) return;
613
+ const now = Date.now();
614
+ const elapsed = Math.round((now - turnStartTime) / 1000);
615
+ const lastOut = eventDisplay.lastOutputTime;
616
+ const silenceMs = lastOut > 0 ? now - lastOut : now - turnStartTime;
617
+
618
+ // Silence auto-abort
619
+ if (silenceMs >= SILENCE_ABORT) {
620
+ console.log(
621
+ color.dim(`\n ⏱ ${elapsed}s elapsed — no output for ${Math.round(silenceMs / 1000)}s, auto-aborting`),
622
+ );
623
+ client.session.abort(sessionId).catch(() => {});
624
+ done({ status: "aborted", aborted: true, quit: false });
625
+ return;
626
+ }
627
+
628
+ // Silence warning
629
+ if (silenceMs >= SILENCE_WARN && !warnedSilence) {
630
+ warnedSilence = true;
631
+ console.log(
632
+ color.dim(`\n ⏱ ${elapsed}s elapsed — no output for ${Math.round(silenceMs / 1000)}s (will abort at ${SILENCE_ABORT / 1000}s)`),
633
+ );
634
+ return;
635
+ }
636
+
637
+ // Regular heartbeat (only when silent for >HEARTBEAT_INTERVAL)
638
+ if (silenceMs >= HEARTBEAT_INTERVAL) {
639
+ process.stdout.write(
640
+ color.dim(` [${elapsed}s] `),
641
+ );
642
+ }
643
+ }, HEARTBEAT_INTERVAL);
644
+
576
645
  // Poll input queue for /abort, /quit
577
646
  const pollId = setInterval(() => {
578
647
  if (!inputQueue.hasMessages()) return;
@@ -597,22 +666,6 @@ async function executeTurn(
597
666
  }
598
667
  }
599
668
  }, 200);
600
-
601
- // Safety timeout: poll session status after 10 minutes
602
- const safetyId = setTimeout(async () => {
603
- if (resolved) return;
604
- try {
605
- // Try listing sessions and checking status
606
- const sessions = await client.session.list();
607
- const session = sessions?.find?.((s) => s.id === sessionId);
608
- const s = session?.status;
609
- if (s && s !== "running" && s !== "pending") {
610
- done({ status: s, aborted: false, quit: false });
611
- }
612
- } catch {
613
- // ignore
614
- }
615
- }, 600_000);
616
669
  });
617
670
  }
618
671
 
@@ -631,6 +684,24 @@ async function supervisorLoop(client, opts, inputQueue) {
631
684
  const plannerModel = parseModelSpec(opts.planner);
632
685
  const executorModel = parseModelSpec(opts.executor);
633
686
 
687
+ // ── Validate models ──
688
+ log.info("Validating models...");
689
+ const modelsOutput = run("opencode models 2>/dev/null") || "";
690
+ const validateModel = (label, spec) => {
691
+ // Look for the model ID in the output; opencode models lists "provider/model"
692
+ if (modelsOutput && !modelsOutput.includes(spec)) {
693
+ log.fail(`${label} model "${spec}" not found in available models.`);
694
+ console.log(color.dim(" Available models:"));
695
+ for (const line of modelsOutput.split("\n").filter(l => l.trim())) {
696
+ console.log(color.dim(` ${line.trim()}`));
697
+ }
698
+ throw new Error(`Invalid ${label.toLowerCase()} model: ${spec}`);
699
+ }
700
+ };
701
+ validateModel("Planner", opts.planner);
702
+ validateModel("Executor", opts.executor);
703
+ log.ok("Models validated");
704
+
634
705
  // Create session
635
706
  log.info("Creating session...");
636
707
  const session = await client.session.create({ title: "mneme auto" });
@@ -720,6 +791,7 @@ async function supervisorLoop(client, opts, inputQueue) {
720
791
  `\n${color.bold(`── Cycle ${cycle} · Planner`)} ${color.dim(`(${opts.planner})`)} ${color.bold("────────────────────")}`,
721
792
  );
722
793
 
794
+ log.info(`Sending prompt to Planner (${opts.planner})...`);
723
795
  eventDisplay.resetTurn("planner");
724
796
  const plannerResult = await executeTurn(
725
797
  client,
@@ -775,6 +847,7 @@ async function supervisorLoop(client, opts, inputQueue) {
775
847
  );
776
848
 
777
849
  const executorPrompt = buildExecutorPrompt();
850
+ log.info(`Sending prompt to Executor (${opts.executor})...`);
778
851
  eventDisplay.resetTurn("executor");
779
852
  const executorResult = await executeTurn(
780
853
  client,