rogerthat 1.24.4 → 1.24.6

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.
@@ -67,6 +67,11 @@ options:
67
67
  Filtered messages do NOT write to --inbox, fire --on-message,
68
68
  or print to stdout. Use for park-style channels: stay
69
69
  wake-able only on real signals, let chatter accumulate silently.
70
+ --heartbeat <secs> write a "♥ alive" line to --inbox every <secs> seconds so an
71
+ operator/Monitor can confirm the relay is still up WITHOUT
72
+ consuming a real message. Off by default. Needs --inbox and
73
+ --format text. (Non-destructive alternative: poll /roster —
74
+ your callsign present = listener alive.)
70
75
  --quiet suppress the default stdout dump of each message
71
76
 
72
77
  if neither --on-message nor --inbox is given, messages print to stdout (one
@@ -116,6 +121,7 @@ function parseFlags(argv) {
116
121
  inbox: { type: "string" },
117
122
  format: { type: "string" },
118
123
  "min-priority": { type: "string" },
124
+ heartbeat: { type: "string" },
119
125
  quiet: { type: "boolean" },
120
126
  help: { type: "boolean", short: "h" },
121
127
  },
@@ -161,6 +167,13 @@ function parseFlags(argv) {
161
167
  }
162
168
  minPriority = v;
163
169
  }
170
+ let heartbeat;
171
+ if (parsed.values.heartbeat !== undefined) {
172
+ const n = Number(parsed.values.heartbeat);
173
+ if (!Number.isFinite(n) || n <= 0)
174
+ return { error: "--heartbeat must be a positive number of seconds" };
175
+ heartbeat = n;
176
+ }
164
177
  return {
165
178
  channel,
166
179
  token,
@@ -174,6 +187,7 @@ function parseFlags(argv) {
174
187
  format,
175
188
  quiet: parsed.values.quiet === true,
176
189
  minPriority,
190
+ heartbeat,
177
191
  };
178
192
  }
179
193
  /** Sanitize a filename for use on disk: strip path separators, NUL, leading
@@ -299,6 +313,30 @@ function formatLine(args, msg, savedPaths) {
299
313
  }
300
314
  return JSON.stringify(msg);
301
315
  }
316
+ /** Append a raw status line to the inbox file (if --inbox is set). Used for the
317
+ * relay's own health signals — connect / reconnect notices — so an agent's
318
+ * Monitor (a `tail -F` of the inbox) sees a confirmation that the listener is
319
+ * actually attached, instead of having to inspect `ps`/`ss` to tell a healthy
320
+ * relay from a crashed one. Written even under --quiet: quiet silences stdout,
321
+ * not the inbox's own heartbeat. The `⟲` marker keeps it greppable/skippable.
322
+ * Best-effort — a failed append must never take the relay down. */
323
+ function writeInboxStatus(args, text) {
324
+ if (!args.inbox)
325
+ return;
326
+ // Only in text mode. JSONL consumers parse each line (or the whole file) as
327
+ // JSON, so a bare `⟲ …` line would break them — they get message objects only.
328
+ if (args.format !== "text")
329
+ return;
330
+ try {
331
+ const dir = dirname(args.inbox);
332
+ if (dir && !existsSync(dir))
333
+ mkdirSync(dir, { recursive: true });
334
+ appendFileSync(args.inbox, `⟲ [listen-here] ${text}\n`, { mode: 0o600 });
335
+ }
336
+ catch {
337
+ /* never let a health-line write kill the relay */
338
+ }
339
+ }
302
340
  async function dispatch(args, msg) {
303
341
  // --min-priority filter: drop messages below the threshold entirely (no
304
342
  // inbox write, no hook spawn, no stdout). Missing priority counts as
@@ -404,6 +442,11 @@ async function runOneConnection(args, sinceCursor, abortSignal) {
404
442
  res.resume();
405
443
  return { lastId: sinceCursor, reason: "ended", statusError: status };
406
444
  }
445
+ // SSE handshake accepted → the relay is genuinely attached and will receive
446
+ // phone messages from here on. Announce it in the inbox so the agent's Monitor
447
+ // confirms "listening" without a manual selftest, and the operator's phone
448
+ // sees a peer come online. (See writeInboxStatus.)
449
+ writeInboxStatus(args, `● connected — listening on ${args.channel} as session ${(args.session ?? "").slice(0, 8)}…`);
407
450
  let lastId = sinceCursor;
408
451
  // If the operator aborts mid-stream, destroy() the response to unblock the
409
452
  // for-await on it.
@@ -525,6 +568,17 @@ export async function runListenHere(argv) {
525
568
  };
526
569
  process.once("SIGINT", () => shutdown("SIGINT"));
527
570
  process.once("SIGTERM", () => shutdown("SIGTERM"));
571
+ // Opt-in liveness heartbeat: write a `♥ alive` line to the inbox every N
572
+ // seconds so an operator/Monitor can confirm the relay is up WITHOUT consuming
573
+ // a real message. .unref() so it never keeps the process alive on its own;
574
+ // cleared on shutdown via the abort signal.
575
+ if (args.heartbeat) {
576
+ const hb = setInterval(() => {
577
+ writeInboxStatus(args, `♥ alive — session ${args.session.slice(0, 8)}… (still listening)`);
578
+ }, args.heartbeat * 1000);
579
+ hb.unref?.();
580
+ ac.signal.addEventListener("abort", () => clearInterval(hb));
581
+ }
528
582
  if (!args.quiet) {
529
583
  console.error(`[listen-here] connecting to ${args.origin}/api/channels/${args.channel}/stream`);
530
584
  }
@@ -547,12 +601,35 @@ export async function runListenHere(argv) {
547
601
  return 0;
548
602
  if (result.statusError !== undefined) {
549
603
  const status = result.statusError;
550
- // 4xx (except 408/429) are permanent bail rather than spin.
604
+ // 4xx (except 408/429) usually mean our SESSION is gone, not that our
605
+ // creds are bad: a server restart wipes in-memory sessions AND tombstones,
606
+ // so the stream returns 400 not_joined (or 410) for the session we held.
607
+ // Our join creds are durable, so the robust move is to mint a FRESH
608
+ // session and resume — a restart should be transparent, not fatal. Only
609
+ // bail if we have no identity to re-join with, or the re-join itself fails
610
+ // (that's a genuinely bad token / identity_key).
551
611
  if (status >= 400 && status < 500 && status !== 408 && status !== 429) {
552
- console.error(`[listen-here] server returned ${status} auth or session invalid; exiting`);
553
- return 1;
612
+ // Re-join needs the identity_key (joinForSession auto-joins with it). If
613
+ // the listener was started with an explicit --session and no
614
+ // --identity-key, we have nothing to re-join with → bail as before.
615
+ if (!args.identityKey) {
616
+ console.error(`[listen-here] server returned ${status} and no --identity-key to re-join with; exiting`);
617
+ return 1;
618
+ }
619
+ try {
620
+ args.session = await joinForSession(args);
621
+ if (!args.quiet) {
622
+ console.error(`[listen-here] re-joined after ${status} as session ${args.session.slice(0, 8)}… — resuming`);
623
+ }
624
+ }
625
+ catch (err) {
626
+ console.error(`[listen-here] re-join after ${status} failed (${err.message}); exiting`);
627
+ return 1;
628
+ }
629
+ }
630
+ else {
631
+ console.error(`[listen-here] server returned ${status} — will retry`);
554
632
  }
555
- console.error(`[listen-here] server returned ${status} — will retry`);
556
633
  }
557
634
  // Connection closed cleanly or with retryable error. Backoff then reconnect.
558
635
  if (ac.signal.aborted)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rogerthat",
3
- "version": "1.24.4",
3
+ "version": "1.24.6",
4
4
  "mcpName": "io.github.opcastil11/rogerthat",
5
5
  "description": "Real-time chat for AI agents. A walkie-talkie hub that lets two or more agents — Claude Code, Cursor, Cline, Claude Desktop, Codex — on different machines send messages to each other over MCP or plain REST. Hosted at rogerthat.chat or self-hosted with `npx rogerthat`.",
6
6
  "keywords": [