@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 +10 -4
- package/package.json +1 -1
- package/src/commands/auto.mjs +93 -20
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
|
@@ -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,21 @@ async function executeTurn(
|
|
|
556
584
|
// Send async — returns immediately
|
|
557
585
|
await client.session.promptAsync(sessionId, body);
|
|
558
586
|
|
|
559
|
-
//
|
|
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
|
-
|
|
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,
|