@xqli02/mneme 0.1.9 → 0.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xqli02/mneme",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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 ────────────────────────────────────────────────────────
@@ -396,6 +396,7 @@ function createEventDisplay(client) {
396
396
  // Track incremental text and tool display state
397
397
  const printedTextLengths = new Map();
398
398
  const displayedToolStates = new Map();
399
+ const deltaParts = new Set(); // parts that received delta events
399
400
 
400
401
  async function start() {
401
402
  running = true;
@@ -430,32 +431,61 @@ function createEventDisplay(client) {
430
431
  const props = event.properties || {};
431
432
 
432
433
  switch (type) {
434
+ // ── Incremental text deltas (streaming) ──
435
+ case "message.part.delta": {
436
+ const partId = props.partID || props.partId;
437
+ if (partId) deltaParts.add(partId);
438
+ if (props.field === "text" && props.delta) {
439
+ process.stdout.write(props.delta);
440
+ lastOutputTime = Date.now();
441
+ }
442
+ break;
443
+ }
444
+
445
+ // ── Part snapshots (full text at end, tool states) ──
433
446
  case "message.part.updated": {
434
447
  if (!props.part) break;
435
448
  const part = props.part;
436
449
  const partId = part.id || `${props.messageID}-${props.index}`;
437
450
 
438
- if (part.type === "text" && part.text) {
439
- const prev = printedTextLengths.get(partId) || 0;
440
- const newText = part.text.slice(prev);
441
- if (newText) {
442
- process.stdout.write(newText);
443
- printedTextLengths.set(partId, part.text.length);
444
- lastOutputTime = Date.now();
445
- }
446
- } else if (
451
+ if (
447
452
  part.type === "tool-invocation" ||
448
453
  part.type === "tool-result"
449
454
  ) {
450
455
  displayToolPart(part, partId);
451
456
  lastOutputTime = Date.now();
452
457
  }
458
+ // For text parts: we rely on message.part.delta for streaming,
459
+ // so only use updated as a fallback if we missed the deltas
460
+ if (part.type === "text" && part.text) {
461
+ const prev = printedTextLengths.get(partId) || 0;
462
+ if (prev === 0 && !deltaParts.has(partId)) {
463
+ // We never saw deltas for this part — print the full text
464
+ process.stdout.write(part.text);
465
+ lastOutputTime = Date.now();
466
+ }
467
+ printedTextLengths.set(partId, part.text.length);
468
+ }
469
+ break;
470
+ }
471
+
472
+ // ── Session status (busy → idle/completed) ──
473
+ case "session.status": {
474
+ const status = props.status?.type || props.status;
475
+ if (status && status !== "busy" && status !== "pending") {
476
+ if (turnResolve) {
477
+ turnResolve(status);
478
+ turnResolve = null;
479
+ }
480
+ }
453
481
  break;
454
482
  }
455
483
 
484
+ // ── Session updated (metadata; also check for status) ──
456
485
  case "session.updated": {
457
- const status = props.session?.status || props.status;
458
- if (status && status !== "running" && status !== "pending") {
486
+ const info = props.info || props.session || {};
487
+ const status = info.status?.type || info.status;
488
+ if (status && status !== "busy" && status !== "running" && status !== "pending") {
459
489
  if (turnResolve) {
460
490
  turnResolve(status);
461
491
  turnResolve = null;
@@ -464,6 +494,17 @@ function createEventDisplay(client) {
464
494
  break;
465
495
  }
466
496
 
497
+ // ── Message-level finish detection ──
498
+ case "message.updated": {
499
+ const info = props.info || {};
500
+ if (info.finish && info.finish !== "pending") {
501
+ // Model finished generating — mark as turn end candidate
502
+ // (session.status should follow, but use this as backup)
503
+ lastOutputTime = Date.now();
504
+ }
505
+ break;
506
+ }
507
+
467
508
  default:
468
509
  break;
469
510
  }
@@ -533,6 +574,7 @@ function createEventDisplay(client) {
533
574
  currentRole = role;
534
575
  printedTextLengths.clear();
535
576
  displayedToolStates.clear();
577
+ deltaParts.clear();
536
578
  }
537
579
 
538
580
  function stop() {
@@ -584,6 +626,28 @@ async function executeTurn(
584
626
  // Send async — returns immediately
585
627
  await client.session.promptAsync(sessionId, body);
586
628
 
629
+ // Quick check: if model is invalid, the session may error out almost
630
+ // instantly but promptAsync still returns 204. Poll once after a short
631
+ // delay to catch this before entering the long wait loop.
632
+ await sleep(2000);
633
+ try {
634
+ const sessions = await client.session.list();
635
+ const s = sessions?.find?.((ss) => ss.id === sessionId);
636
+ if (s && s.status && s.status !== "running" && s.status !== "pending") {
637
+ // Session already finished — likely an immediate error
638
+ const msgs = await client.session.messages(sessionId);
639
+ const lastMsg = msgs?.[msgs.length - 1];
640
+ const errInfo = lastMsg?.info?.error;
641
+ if (errInfo) {
642
+ const errMsg = errInfo.data?.message || errInfo.name || "unknown";
643
+ log.fail(`Model error: ${errMsg}`);
644
+ return { status: "error", aborted: true, quit: false };
645
+ }
646
+ }
647
+ } catch {
648
+ // Ignore probe failures — fall through to normal wait
649
+ }
650
+
587
651
  const HEARTBEAT_INTERVAL = 15_000; // print elapsed every 15s of silence
588
652
  const SILENCE_WARN = 30_000; // warn after 30s of no output
589
653
  const SILENCE_ABORT = 120_000; // auto-abort after 120s of no output
@@ -684,30 +748,49 @@ async function supervisorLoop(client, opts, inputQueue) {
684
748
  const plannerModel = parseModelSpec(opts.planner);
685
749
  const executorModel = parseModelSpec(opts.executor);
686
750
 
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
-
705
- // Create session
751
+ // Create session first (needed for model probing)
706
752
  log.info("Creating session...");
707
753
  const session = await client.session.create({ title: "mneme auto" });
708
754
  const sessionId = session.id;
709
755
  log.ok(`Session: ${sessionId}`);
710
756
 
757
+ // ── Validate models by sending a real test prompt ──
758
+ // opencode models lists theoretical models, but the provider may reject them
759
+ // at runtime (e.g. Copilot plan doesn't include gpt-5.x). Only a real API
760
+ // call reveals this — sync prompt returns the error, async silently fails.
761
+ log.info("Validating models (API probe)...");
762
+ const probeModels = [
763
+ { label: "Planner", spec: opts.planner, parsed: plannerModel },
764
+ { label: "Executor", spec: opts.executor, parsed: executorModel },
765
+ ];
766
+ // Deduplicate if both use the same model
767
+ const seen = new Set();
768
+ for (const m of probeModels) {
769
+ if (seen.has(m.spec)) continue;
770
+ seen.add(m.spec);
771
+ try {
772
+ const result = await client.session.prompt(sessionId, {
773
+ parts: [{ type: "text", text: "Say OK" }],
774
+ model: m.parsed,
775
+ });
776
+ // Check if the response contains an error
777
+ const err = result?.info?.error;
778
+ if (err) {
779
+ const msg = err.data?.message || err.name || "unknown error";
780
+ log.fail(`${m.label} model "${m.spec}" rejected by provider: ${msg}`);
781
+ console.log(color.dim(" Tip: run 'opencode models' to see listed models, but not all may be available on your plan."));
782
+ throw new Error(`${m.label} model unavailable: ${msg}`);
783
+ }
784
+ log.ok(`${m.label} model verified: ${m.spec}`);
785
+ } catch (probeErr) {
786
+ if (probeErr.message.includes("unavailable") || probeErr.message.includes("rejected")) {
787
+ throw probeErr; // re-throw our own errors
788
+ }
789
+ // API call itself failed — might be a transient issue
790
+ log.warn(`${m.label} model probe inconclusive: ${probeErr.message}`);
791
+ }
792
+ }
793
+
711
794
  // Start SSE event display
712
795
  const eventDisplay = createEventDisplay(client);
713
796
  eventDisplay.start().catch(() => {});
@@ -65,7 +65,17 @@ export function createClient(baseUrl) {
65
65
  } else if (line.startsWith("data:")) {
66
66
  const data = line.slice(5).trim();
67
67
  try {
68
- currentEvent.properties = JSON.parse(data);
68
+ const parsed = JSON.parse(data);
69
+ // opencode sends all fields in data JSON: {type, properties, ...}
70
+ // Merge into currentEvent, but prefer explicit event: line if present
71
+ if (parsed && typeof parsed === "object") {
72
+ if (!currentEvent.type && parsed.type) {
73
+ currentEvent.type = parsed.type;
74
+ }
75
+ currentEvent.properties = parsed.properties || parsed;
76
+ } else {
77
+ currentEvent.properties = parsed;
78
+ }
69
79
  } catch {
70
80
  currentEvent.properties = data;
71
81
  }