@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 +10 -4
- package/package.json +1 -1
- package/src/commands/auto.mjs +137 -23
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
|
|
175
|
-
mneme down
|
|
176
|
-
mneme ps
|
|
177
|
-
mneme
|
|
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
package/src/commands/auto.mjs
CHANGED
|
@@ -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
|
+
* - 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-
|
|
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
|
|
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 {
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|