@xqli02/mneme 0.1.8 → 0.1.10

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.10",
4
4
  "description": "Three-layer memory architecture for AI coding agents (Ledger + Beads + OpenCode)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,7 +2,7 @@
2
2
  * mneme auto — Dual-agent autonomous supervisor loop.
3
3
  *
4
4
  * Uses two agents in the same opencode session:
5
- * - Planner (default: gpt-5.2): analyzes goal, breaks down tasks, reviews results
5
+ * - Planner (default: gpt-4.1): analyzes goal, breaks down tasks, reviews results
6
6
  * - Executor (default: claude-opus-4.6): writes code, runs commands, implements changes
7
7
  *
8
8
  * The planner and executor alternate turns via per-message model switching.
@@ -35,7 +35,7 @@ import { color, log, run, has } from "../utils.mjs";
35
35
 
36
36
  // ── Default models ──────────────────────────────────────────────────────────
37
37
 
38
- const DEFAULT_PLANNER = "github-copilot/gpt-5.2";
38
+ const DEFAULT_PLANNER = "github-copilot/gpt-4.1";
39
39
  const DEFAULT_EXECUTOR = "github-copilot/claude-opus-4.6";
40
40
 
41
41
  // ── Argument parsing ────────────────────────────────────────────────────────
@@ -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,43 @@ 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
+ // Quick check: if model is invalid, the session may error out almost
588
+ // instantly but promptAsync still returns 204. Poll once after a short
589
+ // delay to catch this before entering the long wait loop.
590
+ await sleep(2000);
591
+ try {
592
+ const sessions = await client.session.list();
593
+ const s = sessions?.find?.((ss) => ss.id === sessionId);
594
+ if (s && s.status && s.status !== "running" && s.status !== "pending") {
595
+ // Session already finished — likely an immediate error
596
+ const msgs = await client.session.messages(sessionId);
597
+ const lastMsg = msgs?.[msgs.length - 1];
598
+ const errInfo = lastMsg?.info?.error;
599
+ if (errInfo) {
600
+ const errMsg = errInfo.data?.message || errInfo.name || "unknown";
601
+ log.fail(`Model error: ${errMsg}`);
602
+ return { status: "error", aborted: true, quit: false };
603
+ }
604
+ }
605
+ } catch {
606
+ // Ignore probe failures — fall through to normal wait
607
+ }
608
+
609
+ const HEARTBEAT_INTERVAL = 15_000; // print elapsed every 15s of silence
610
+ const SILENCE_WARN = 30_000; // warn after 30s of no output
611
+ const SILENCE_ABORT = 120_000; // auto-abort after 120s of no output
612
+ const turnStartTime = Date.now();
613
+
614
+ // Race: SSE turn completion vs user commands vs silence timeout
560
615
  return new Promise((resolve) => {
561
616
  let resolved = false;
617
+ let warnedSilence = false;
562
618
 
563
619
  const done = (result) => {
564
620
  if (resolved) return;
565
621
  resolved = true;
566
622
  clearInterval(pollId);
567
- clearTimeout(safetyId);
623
+ clearInterval(heartbeatId);
568
624
  resolve(result);
569
625
  };
570
626
 
@@ -573,6 +629,41 @@ async function executeTurn(
573
629
  done({ status, aborted: false, quit: false });
574
630
  });
575
631
 
632
+ // Heartbeat: show elapsed time when no output is flowing
633
+ const heartbeatId = setInterval(() => {
634
+ if (resolved) return;
635
+ const now = Date.now();
636
+ const elapsed = Math.round((now - turnStartTime) / 1000);
637
+ const lastOut = eventDisplay.lastOutputTime;
638
+ const silenceMs = lastOut > 0 ? now - lastOut : now - turnStartTime;
639
+
640
+ // Silence auto-abort
641
+ if (silenceMs >= SILENCE_ABORT) {
642
+ console.log(
643
+ color.dim(`\n ⏱ ${elapsed}s elapsed — no output for ${Math.round(silenceMs / 1000)}s, auto-aborting`),
644
+ );
645
+ client.session.abort(sessionId).catch(() => {});
646
+ done({ status: "aborted", aborted: true, quit: false });
647
+ return;
648
+ }
649
+
650
+ // Silence warning
651
+ if (silenceMs >= SILENCE_WARN && !warnedSilence) {
652
+ warnedSilence = true;
653
+ console.log(
654
+ color.dim(`\n ⏱ ${elapsed}s elapsed — no output for ${Math.round(silenceMs / 1000)}s (will abort at ${SILENCE_ABORT / 1000}s)`),
655
+ );
656
+ return;
657
+ }
658
+
659
+ // Regular heartbeat (only when silent for >HEARTBEAT_INTERVAL)
660
+ if (silenceMs >= HEARTBEAT_INTERVAL) {
661
+ process.stdout.write(
662
+ color.dim(` [${elapsed}s] `),
663
+ );
664
+ }
665
+ }, HEARTBEAT_INTERVAL);
666
+
576
667
  // Poll input queue for /abort, /quit
577
668
  const pollId = setInterval(() => {
578
669
  if (!inputQueue.hasMessages()) return;
@@ -597,22 +688,6 @@ async function executeTurn(
597
688
  }
598
689
  }
599
690
  }, 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
691
  });
617
692
  }
618
693
 
@@ -631,12 +706,49 @@ async function supervisorLoop(client, opts, inputQueue) {
631
706
  const plannerModel = parseModelSpec(opts.planner);
632
707
  const executorModel = parseModelSpec(opts.executor);
633
708
 
634
- // Create session
709
+ // Create session first (needed for model probing)
635
710
  log.info("Creating session...");
636
711
  const session = await client.session.create({ title: "mneme auto" });
637
712
  const sessionId = session.id;
638
713
  log.ok(`Session: ${sessionId}`);
639
714
 
715
+ // ── Validate models by sending a real test prompt ──
716
+ // opencode models lists theoretical models, but the provider may reject them
717
+ // at runtime (e.g. Copilot plan doesn't include gpt-5.x). Only a real API
718
+ // call reveals this — sync prompt returns the error, async silently fails.
719
+ log.info("Validating models (API probe)...");
720
+ const probeModels = [
721
+ { label: "Planner", spec: opts.planner, parsed: plannerModel },
722
+ { label: "Executor", spec: opts.executor, parsed: executorModel },
723
+ ];
724
+ // Deduplicate if both use the same model
725
+ const seen = new Set();
726
+ for (const m of probeModels) {
727
+ if (seen.has(m.spec)) continue;
728
+ seen.add(m.spec);
729
+ try {
730
+ const result = await client.session.prompt(sessionId, {
731
+ parts: [{ type: "text", text: "Say OK" }],
732
+ model: m.parsed,
733
+ });
734
+ // Check if the response contains an error
735
+ const err = result?.info?.error;
736
+ if (err) {
737
+ const msg = err.data?.message || err.name || "unknown error";
738
+ log.fail(`${m.label} model "${m.spec}" rejected by provider: ${msg}`);
739
+ console.log(color.dim(" Tip: run 'opencode models' to see listed models, but not all may be available on your plan."));
740
+ throw new Error(`${m.label} model unavailable: ${msg}`);
741
+ }
742
+ log.ok(`${m.label} model verified: ${m.spec}`);
743
+ } catch (probeErr) {
744
+ if (probeErr.message.includes("unavailable") || probeErr.message.includes("rejected")) {
745
+ throw probeErr; // re-throw our own errors
746
+ }
747
+ // API call itself failed — might be a transient issue
748
+ log.warn(`${m.label} model probe inconclusive: ${probeErr.message}`);
749
+ }
750
+ }
751
+
640
752
  // Start SSE event display
641
753
  const eventDisplay = createEventDisplay(client);
642
754
  eventDisplay.start().catch(() => {});
@@ -720,6 +832,7 @@ async function supervisorLoop(client, opts, inputQueue) {
720
832
  `\n${color.bold(`── Cycle ${cycle} · Planner`)} ${color.dim(`(${opts.planner})`)} ${color.bold("────────────────────")}`,
721
833
  );
722
834
 
835
+ log.info(`Sending prompt to Planner (${opts.planner})...`);
723
836
  eventDisplay.resetTurn("planner");
724
837
  const plannerResult = await executeTurn(
725
838
  client,
@@ -775,6 +888,7 @@ async function supervisorLoop(client, opts, inputQueue) {
775
888
  );
776
889
 
777
890
  const executorPrompt = buildExecutorPrompt();
891
+ log.info(`Sending prompt to Executor (${opts.executor})...`);
778
892
  eventDisplay.resetTurn("executor");
779
893
  const executorResult = await executeTurn(
780
894
  client,