cumora 0.1.48 → 0.1.50
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/dist/cli.js +53 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -98,11 +98,19 @@ function spawnEngine(bin, args, { home, env, onLog, signal }) {
|
|
|
98
98
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
99
99
|
const stderrTail = [];
|
|
100
100
|
const stdoutTail = [];
|
|
101
|
+
let sessionId = null;
|
|
101
102
|
const pump = (stream, buf) => {
|
|
102
103
|
for (const line of buf.toString("utf8").split("\n")) {
|
|
103
104
|
const cleaned = cleanLine(line);
|
|
104
105
|
if (!cleaned) continue;
|
|
105
106
|
pushTail(stream === "stderr" ? stderrTail : stdoutTail, cleaned);
|
|
107
|
+
if (stream === "stdout" && cleaned.startsWith("{") && cleaned.includes('"session_id"')) {
|
|
108
|
+
try {
|
|
109
|
+
const sid = JSON.parse(cleaned).session_id;
|
|
110
|
+
if (typeof sid === "string" && sid) sessionId = sid;
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
106
114
|
onLog(cleaned);
|
|
107
115
|
}
|
|
108
116
|
};
|
|
@@ -117,7 +125,8 @@ function spawnEngine(bin, args, { home, env, onLog, signal }) {
|
|
|
117
125
|
const exitCode = code ?? (signalName ? 128 : 1);
|
|
118
126
|
resolve({
|
|
119
127
|
exitCode,
|
|
120
|
-
error: exitCode === 0 ? void 0 : failurePreview({ exitCode, signalName, stderr: stderrTail, stdout: stdoutTail })
|
|
128
|
+
error: exitCode === 0 ? void 0 : failurePreview({ exitCode, signalName, stderr: stderrTail, stdout: stdoutTail }),
|
|
129
|
+
sessionId
|
|
121
130
|
});
|
|
122
131
|
});
|
|
123
132
|
});
|
|
@@ -180,7 +189,8 @@ var ClaudeAdapter = class {
|
|
|
180
189
|
run(args) {
|
|
181
190
|
const flags = extraArgs("CUMORA_CLAUDE_ARGS");
|
|
182
191
|
const model = args.model ? ["--model", args.model] : [];
|
|
183
|
-
const
|
|
192
|
+
const resume = args.resumeSessionId ? ["--resume", args.resumeSessionId] : [];
|
|
193
|
+
const argv = flags.length ? [...flags, ...resume, "-p", args.prompt] : ["-p", args.prompt, ...resume, ...model, "--output-format", "stream-json", "--verbose", "--dangerously-skip-permissions"];
|
|
184
194
|
const env = {
|
|
185
195
|
...args.env,
|
|
186
196
|
MAX_THINKING_TOKENS: args.env.MAX_THINKING_TOKENS ?? "0"
|
|
@@ -384,6 +394,11 @@ var AgentRunner = class {
|
|
|
384
394
|
pendingRerun = false;
|
|
385
395
|
stopped = false;
|
|
386
396
|
lastWakeConvo = null;
|
|
397
|
+
/** The engine session id to `--resume` next wake, so the agent keeps
|
|
398
|
+
* continuous context (its place in a running task) instead of waking cold
|
|
399
|
+
* on a frozen inbox snapshot. Captured from each run's stream-json output;
|
|
400
|
+
* cleared if a resume fails so the next wake starts a clean session. */
|
|
401
|
+
sessionId = null;
|
|
387
402
|
pollTimer;
|
|
388
403
|
adapter;
|
|
389
404
|
async start() {
|
|
@@ -486,6 +501,31 @@ ${hint}`,
|
|
|
486
501
|
async inboxTriage(token) {
|
|
487
502
|
return runtimeGet(this.cfg.serverUrl, "/inbox-triage", token);
|
|
488
503
|
}
|
|
504
|
+
/** Snapshot the unread inbox as {conversationId → latest unread message id}.
|
|
505
|
+
* Captured BEFORE the engine runs so we can later ack exactly what THIS
|
|
506
|
+
* turn saw (`ackSeen`) without swallowing messages that arrive mid-turn —
|
|
507
|
+
* those keep a higher id, stay unread, and drive the coalesced rerun. */
|
|
508
|
+
async snapshotUnread(token) {
|
|
509
|
+
const inbox = await runtimeGet(this.cfg.serverUrl, "/inbox", token);
|
|
510
|
+
const seen = /* @__PURE__ */ new Map();
|
|
511
|
+
for (const row of inbox?.rows ?? []) {
|
|
512
|
+
if (typeof row.conversation_id === "string" && row.conversation_id && typeof row.id === "string" && row.id) {
|
|
513
|
+
seen.set(row.conversation_id, row.id);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return seen;
|
|
517
|
+
}
|
|
518
|
+
/** Advance this agent's read cursor over the conversations it just saw, so a
|
|
519
|
+
* wake that ends WITHOUT a reply (small-brain said "skip", or the engine
|
|
520
|
+
* read the room and chose silence) does not re-trigger on the same messages
|
|
521
|
+
* every INBOX_POLL_MS. markConversationRead is monotonic, so this never
|
|
522
|
+
* regresses a cursor the engine already advanced further via `cumora reply`. */
|
|
523
|
+
async ackSeen(token, seen) {
|
|
524
|
+
if (seen.size === 0) return;
|
|
525
|
+
await Promise.all([...seen].map(
|
|
526
|
+
([conversationId, upToMessageId]) => runtimeBest(this.cfg.serverUrl, "/conversation/mark-read", token, { conversationId, upToMessageId })
|
|
527
|
+
));
|
|
528
|
+
}
|
|
489
529
|
formatTriageNote(triage) {
|
|
490
530
|
if (!triage) return "";
|
|
491
531
|
const note = typeof triage?.promptNote === "string" ? triage.promptNote.trim() : "";
|
|
@@ -543,10 +583,12 @@ If nothing genuinely needs you, it's fine to do nothing and stop. When finished,
|
|
|
543
583
|
const token = this.token;
|
|
544
584
|
const convo = this.lastWakeConvo;
|
|
545
585
|
this.lastWakeConvo = null;
|
|
586
|
+
const seen = await this.snapshotUnread(token);
|
|
546
587
|
const triage = await this.inboxTriage(token);
|
|
547
588
|
if (triage?.actionable === false) {
|
|
548
589
|
const reason = typeof triage.reason === "string" ? triage.reason : "not relevant";
|
|
549
590
|
console.log(`[computer] ${this.agent.id} skipped by small-brain inbox triage: ${reason}`);
|
|
591
|
+
await this.ackSeen(token, seen);
|
|
550
592
|
await runtimeBest(this.cfg.serverUrl, "/status", token, { status: "avail" });
|
|
551
593
|
continue;
|
|
552
594
|
}
|
|
@@ -577,17 +619,24 @@ If nothing genuinely needs you, it's fine to do nothing and stop. When finished,
|
|
|
577
619
|
this.memoryDigest(),
|
|
578
620
|
Promise.resolve(this.formatTriageNote(triage))
|
|
579
621
|
]);
|
|
622
|
+
const resumeSessionId = this.sessionId;
|
|
580
623
|
const result = await this.adapter.run({
|
|
581
624
|
home: this.home,
|
|
582
625
|
prompt: this.prompt(memoryDigest, triageNote),
|
|
583
626
|
env: this.engineEnv(),
|
|
584
627
|
model: this.agent.model,
|
|
585
628
|
fastModel: this.agent.fastModel,
|
|
629
|
+
resumeSessionId,
|
|
586
630
|
onLog: (line) => console.log(`[${this.agent.id}/${this.adapter.id}] ${line.slice(0, 500)}`),
|
|
587
631
|
signal: controller.signal
|
|
588
632
|
});
|
|
589
633
|
exitCode = result.exitCode;
|
|
590
634
|
if (result.error) engineError = this.visibleEngineError(exitCode, result.error);
|
|
635
|
+
if (result.sessionId) this.sessionId = result.sessionId;
|
|
636
|
+
else if (engineError && resumeSessionId && /resume|session|conversation/i.test(engineError)) {
|
|
637
|
+
console.warn(`[computer] ${this.agent.id} resume failed; starting a fresh session next wake`);
|
|
638
|
+
this.sessionId = null;
|
|
639
|
+
}
|
|
591
640
|
} catch (err) {
|
|
592
641
|
console.error(`[computer] ${this.agent.id} engine spawn failed:`, err instanceof Error ? err.message : err);
|
|
593
642
|
exitCode = 1;
|
|
@@ -615,6 +664,7 @@ If nothing genuinely needs you, it's fine to do nothing and stop. When finished,
|
|
|
615
664
|
error: engineError
|
|
616
665
|
});
|
|
617
666
|
}
|
|
667
|
+
if (!engineError) await this.ackSeen(token, seen);
|
|
618
668
|
await runtimeBest(this.cfg.serverUrl, "/status", token, { status: "avail" });
|
|
619
669
|
} while (this.pendingRerun && !this.stopped);
|
|
620
670
|
} finally {
|
|
@@ -637,7 +687,7 @@ If nothing genuinely needs you, it's fine to do nothing and stop. When finished,
|
|
|
637
687
|
void this.runTurn();
|
|
638
688
|
for await (const evt of parseSseStream(res.body)) {
|
|
639
689
|
if (this.stopped) break;
|
|
640
|
-
if (evt.event === "wake") {
|
|
690
|
+
if (evt.event === "wake" || evt.event === "steer") {
|
|
641
691
|
try {
|
|
642
692
|
const convo = evt.data ? JSON.parse(evt.data).conversationId : null;
|
|
643
693
|
if (convo) this.lastWakeConvo = convo;
|