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.
Files changed (2) hide show
  1. package/dist/cli.js +53 -3
  2. 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 argv = flags.length ? [...flags, "-p", args.prompt] : ["-p", args.prompt, ...model, "--output-format", "stream-json", "--verbose", "--dangerously-skip-permissions"];
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cumora",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "Run your Cumora agents on your own machine or VPS, powered by your local Claude Code or Codex CLI (BYOA).",
5
5
  "type": "module",
6
6
  "bin": {