claudemesh-cli 1.34.6 → 1.34.8

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.
@@ -104,7 +104,7 @@ __export(exports_urls, {
104
104
  VERSION: () => VERSION,
105
105
  URLS: () => URLS
106
106
  });
107
- var URLS, VERSION = "1.34.6", env;
107
+ var URLS, VERSION = "1.34.8", env;
108
108
  var init_urls = __esm(() => {
109
109
  URLS = {
110
110
  BROKER: process.env.CLAUDEMESH_BROKER_URL ?? "wss://ic.claudemesh.com/ws",
@@ -4022,7 +4022,9 @@ __export(exports_daemon_route, {
4022
4022
  tryListInboxViaDaemon: () => tryListInboxViaDaemon,
4023
4023
  tryGetStateViaDaemon: () => tryGetStateViaDaemon,
4024
4024
  tryGetSkillViaDaemon: () => tryGetSkillViaDaemon,
4025
- tryForgetViaDaemon: () => tryForgetViaDaemon
4025
+ tryForgetViaDaemon: () => tryForgetViaDaemon,
4026
+ tryFlushInboxViaDaemon: () => tryFlushInboxViaDaemon,
4027
+ tryDeleteInboxRowViaDaemon: () => tryDeleteInboxRowViaDaemon
4026
4028
  });
4027
4029
  function meshQuery(mesh) {
4028
4030
  return mesh ? `?mesh=${encodeURIComponent(mesh)}` : "";
@@ -4050,11 +4052,18 @@ async function tryListPeersViaDaemon(mesh) {
4050
4052
  return null;
4051
4053
  }
4052
4054
  }
4053
- async function tryListInboxViaDaemon(mesh, limit = 100) {
4055
+ async function tryListInboxViaDaemon(mesh, limit = 100, opts = {}) {
4054
4056
  if (!await daemonReachable())
4055
4057
  return null;
4056
4058
  try {
4057
- const path = `/v1/inbox${meshQuery(mesh)}${meshQuery(mesh) ? "&" : "?"}limit=${limit}`;
4059
+ const params = [`limit=${limit}`];
4060
+ if (mesh)
4061
+ params.push(`mesh=${encodeURIComponent(mesh)}`);
4062
+ if (opts.unreadOnly)
4063
+ params.push("unread_only=true");
4064
+ if (opts.markSeen === false)
4065
+ params.push("mark_seen=false");
4066
+ const path = `/v1/inbox?${params.join("&")}`;
4058
4067
  const res = await ipc({ path, timeoutMs: 3000 });
4059
4068
  if (res.status !== 200)
4060
4069
  return null;
@@ -4066,6 +4075,48 @@ async function tryListInboxViaDaemon(mesh, limit = 100) {
4066
4075
  return null;
4067
4076
  }
4068
4077
  }
4078
+ async function tryFlushInboxViaDaemon(args = {}) {
4079
+ if (!await daemonReachable())
4080
+ return null;
4081
+ try {
4082
+ const params = [];
4083
+ if (args.mesh)
4084
+ params.push(`mesh=${encodeURIComponent(args.mesh)}`);
4085
+ if (args.beforeIso)
4086
+ params.push(`before=${encodeURIComponent(args.beforeIso)}`);
4087
+ const path = `/v1/inbox${params.length ? `?${params.join("&")}` : ""}`;
4088
+ const res = await ipc({ path, method: "DELETE", timeoutMs: 3000 });
4089
+ if (res.status !== 200)
4090
+ return null;
4091
+ return typeof res.body.removed === "number" ? res.body.removed : null;
4092
+ } catch (err) {
4093
+ const msg = String(err);
4094
+ if (/ENOENT|ECONNREFUSED|ipc_timeout/.test(msg))
4095
+ return null;
4096
+ return null;
4097
+ }
4098
+ }
4099
+ async function tryDeleteInboxRowViaDaemon(id) {
4100
+ if (!await daemonReachable())
4101
+ return null;
4102
+ try {
4103
+ const res = await ipc({
4104
+ path: `/v1/inbox/${encodeURIComponent(id)}`,
4105
+ method: "DELETE",
4106
+ timeoutMs: 3000
4107
+ });
4108
+ if (res.status === 404)
4109
+ return false;
4110
+ if (res.status !== 200)
4111
+ return null;
4112
+ return (res.body.removed ?? 0) > 0;
4113
+ } catch (err) {
4114
+ const msg = String(err);
4115
+ if (/ENOENT|ECONNREFUSED|ipc_timeout/.test(msg))
4116
+ return null;
4117
+ return null;
4118
+ }
4119
+ }
4069
4120
  async function tryListSkillsViaDaemon(mesh) {
4070
4121
  if (!await daemonReachable())
4071
4122
  return null;
@@ -8239,6 +8290,87 @@ var init_send = __esm(() => {
8239
8290
  init_styles();
8240
8291
  });
8241
8292
 
8293
+ // src/commands/inbox-actions.ts
8294
+ var exports_inbox_actions = {};
8295
+ __export(exports_inbox_actions, {
8296
+ runInboxFlush: () => runInboxFlush,
8297
+ runInboxDelete: () => runInboxDelete
8298
+ });
8299
+ async function runInboxFlush(flags) {
8300
+ const hasFilter = !!(flags.mesh || flags.before);
8301
+ if (!hasFilter && !flags.all) {
8302
+ if (flags.json) {
8303
+ process.stdout.write(JSON.stringify({ ok: false, error: "missing_filter" }) + `
8304
+ `);
8305
+ return;
8306
+ }
8307
+ render.info(dim(`Refusing to flush every row on every mesh.
8308
+ ` + " Re-run with --mesh <slug>, --before <iso-timestamp>, or --all to confirm."));
8309
+ process.exit(1);
8310
+ }
8311
+ const removed = await tryFlushInboxViaDaemon({
8312
+ ...flags.mesh ? { mesh: flags.mesh } : {},
8313
+ ...flags.before ? { beforeIso: flags.before } : {}
8314
+ });
8315
+ if (removed === null) {
8316
+ if (flags.json) {
8317
+ process.stdout.write(JSON.stringify({ ok: false, error: "daemon_unreachable" }) + `
8318
+ `);
8319
+ return;
8320
+ }
8321
+ render.info(dim("Daemon not reachable. Run `claudemesh daemon up` and retry."));
8322
+ process.exit(1);
8323
+ }
8324
+ if (flags.json) {
8325
+ process.stdout.write(JSON.stringify({ ok: true, removed }) + `
8326
+ `);
8327
+ return;
8328
+ }
8329
+ const scope = flags.mesh ? `mesh "${flags.mesh}"` : flags.before ? `older than ${flags.before}` : "all meshes";
8330
+ render.info(`✔ Flushed ${removed} message${removed === 1 ? "" : "s"} from ${scope}.`);
8331
+ }
8332
+ async function runInboxDelete(id, flags) {
8333
+ if (!id) {
8334
+ if (flags.json) {
8335
+ process.stdout.write(JSON.stringify({ ok: false, error: "missing_id" }) + `
8336
+ `);
8337
+ return;
8338
+ }
8339
+ render.info(dim("Usage: claudemesh inbox delete <message-id>"));
8340
+ process.exit(1);
8341
+ }
8342
+ const ok = await tryDeleteInboxRowViaDaemon(id);
8343
+ if (ok === null) {
8344
+ if (flags.json) {
8345
+ process.stdout.write(JSON.stringify({ ok: false, error: "daemon_unreachable" }) + `
8346
+ `);
8347
+ return;
8348
+ }
8349
+ render.info(dim("Daemon not reachable. Run `claudemesh daemon up` and retry."));
8350
+ process.exit(1);
8351
+ }
8352
+ if (!ok) {
8353
+ if (flags.json) {
8354
+ process.stdout.write(JSON.stringify({ ok: false, error: "not_found", id }) + `
8355
+ `);
8356
+ return;
8357
+ }
8358
+ render.info(dim(`No inbox row with id "${id}".`));
8359
+ process.exit(1);
8360
+ }
8361
+ if (flags.json) {
8362
+ process.stdout.write(JSON.stringify({ ok: true, id }) + `
8363
+ `);
8364
+ return;
8365
+ }
8366
+ render.info(`✔ Deleted inbox row ${id}.`);
8367
+ }
8368
+ var init_inbox_actions = __esm(() => {
8369
+ init_daemon_route();
8370
+ init_render();
8371
+ init_styles();
8372
+ });
8373
+
8242
8374
  // src/commands/inbox.ts
8243
8375
  var exports_inbox = {};
8244
8376
  __export(exports_inbox, {
@@ -8255,7 +8387,10 @@ function formatMessage(msg, includeMesh) {
8255
8387
  }
8256
8388
  async function runInbox(flags) {
8257
8389
  const meshSlug = flags.mesh;
8258
- const items = await tryListInboxViaDaemon(meshSlug, flags.limit ?? 100);
8390
+ const items = await tryListInboxViaDaemon(meshSlug, flags.limit ?? 100, {
8391
+ unreadOnly: flags.unread === true,
8392
+ markSeen: true
8393
+ });
8259
8394
  if (items === null) {
8260
8395
  if (flags.json) {
8261
8396
  process.stdout.write(`[]
@@ -8272,10 +8407,12 @@ async function runInbox(flags) {
8272
8407
  }
8273
8408
  if (items.length === 0) {
8274
8409
  const scope = meshSlug ? `mesh "${meshSlug}"` : "any mesh";
8275
- render.info(dim(`No messages on ${scope}.`));
8410
+ const filter = flags.unread ? "unread " : "";
8411
+ render.info(dim(`No ${filter}messages on ${scope}.`));
8276
8412
  return;
8277
8413
  }
8278
- const heading = meshSlug ? `inbox — ${meshSlug} (${items.length} message${items.length === 1 ? "" : "s"})` : `inbox (${items.length} message${items.length === 1 ? "" : "s"})`;
8414
+ const filterTag = flags.unread ? " unread" : "";
8415
+ const heading = meshSlug ? `inbox — ${meshSlug} (${items.length}${filterTag} message${items.length === 1 ? "" : "s"})` : `inbox (${items.length}${filterTag} message${items.length === 1 ? "" : "s"})`;
8279
8416
  render.section(heading);
8280
8417
  for (const msg of items) {
8281
8418
  process.stdout.write(formatMessage(msg, !meshSlug) + `
@@ -9457,6 +9594,11 @@ function migrateInbox(db) {
9457
9594
  CREATE INDEX IF NOT EXISTS inbox_topic ON inbox(topic);
9458
9595
  CREATE INDEX IF NOT EXISTS inbox_sender ON inbox(sender_pubkey);
9459
9596
  `);
9597
+ const cols = db.prepare(`PRAGMA table_info(inbox)`).all();
9598
+ if (!cols.some((c) => c.name === "seen_at")) {
9599
+ db.exec(`ALTER TABLE inbox ADD COLUMN seen_at INTEGER`);
9600
+ db.exec(`CREATE INDEX IF NOT EXISTS inbox_seen_at ON inbox(seen_at)`);
9601
+ }
9460
9602
  }
9461
9603
  function insertIfNew(db, row) {
9462
9604
  const before = db.prepare(`SELECT id FROM inbox WHERE client_message_id = ?`).get(row.client_message_id);
@@ -9491,9 +9633,12 @@ function listInbox(db, p) {
9491
9633
  where.push("mesh = ?");
9492
9634
  args.push(p.mesh);
9493
9635
  }
9636
+ if (p.unreadOnly === true) {
9637
+ where.push("seen_at IS NULL");
9638
+ }
9494
9639
  const sql = `
9495
9640
  SELECT id, client_message_id, broker_message_id, mesh, topic,
9496
- sender_pubkey, sender_name, body, meta, received_at, reply_to_id
9641
+ sender_pubkey, sender_name, body, meta, received_at, reply_to_id, seen_at
9497
9642
  FROM inbox
9498
9643
  ${where.length ? "WHERE " + where.join(" AND ") : ""}
9499
9644
  ORDER BY received_at DESC
@@ -9502,6 +9647,36 @@ function listInbox(db, p) {
9502
9647
  args.push(Math.min(Math.max(p.limit ?? 100, 1), 1000));
9503
9648
  return db.prepare(sql).all(...args);
9504
9649
  }
9650
+ function markInboxSeen(db, ids, now = Date.now()) {
9651
+ if (ids.length === 0)
9652
+ return 0;
9653
+ const placeholders = ids.map(() => "?").join(",");
9654
+ const r = db.prepare(`UPDATE inbox SET seen_at = ? WHERE seen_at IS NULL AND id IN (${placeholders})`).run(now, ...ids);
9655
+ return Number(r.changes);
9656
+ }
9657
+ function pruneInboxBefore(db, cutoffMs) {
9658
+ const r = db.prepare(`DELETE FROM inbox WHERE received_at < ?`).run(cutoffMs);
9659
+ return Number(r.changes);
9660
+ }
9661
+ function deleteInboxRow(db, id) {
9662
+ const r = db.prepare(`DELETE FROM inbox WHERE id = ?`).run(id);
9663
+ return Number(r.changes) > 0;
9664
+ }
9665
+ function flushInbox(db, p) {
9666
+ const where = [];
9667
+ const args = [];
9668
+ if (p.mesh !== undefined) {
9669
+ where.push("mesh = ?");
9670
+ args.push(p.mesh);
9671
+ }
9672
+ if (p.before !== undefined) {
9673
+ where.push("received_at < ?");
9674
+ args.push(p.before);
9675
+ }
9676
+ const sql = `DELETE FROM inbox ${where.length ? "WHERE " + where.join(" AND ") : ""}`;
9677
+ const r = db.prepare(sql).run(...args);
9678
+ return Number(r.changes);
9679
+ }
9505
9680
 
9506
9681
  // src/daemon/events.ts
9507
9682
  class EventBus {
@@ -10243,13 +10418,23 @@ function makeHandler(opts) {
10243
10418
  const limitRaw = url.searchParams.get("limit");
10244
10419
  const limit = limitRaw ? Number.parseInt(limitRaw, 10) : undefined;
10245
10420
  const meshFilter = meshFromCtx(url.searchParams.get("mesh")) ?? undefined;
10421
+ const unreadOnly = url.searchParams.get("unread_only") === "true";
10422
+ const markSeen = url.searchParams.get("mark_seen") !== "false";
10246
10423
  const rows = listInbox(opts.inboxDb, {
10247
10424
  since: Number.isFinite(since) ? since : undefined,
10248
10425
  topic,
10249
10426
  fromPubkey,
10250
10427
  ...meshFilter ? { mesh: meshFilter } : {},
10428
+ unreadOnly,
10251
10429
  limit: Number.isFinite(limit ?? NaN) ? limit : undefined
10252
10430
  });
10431
+ let flippedCount = 0;
10432
+ if (markSeen) {
10433
+ const unreadIds = rows.filter((r) => r.seen_at == null).map((r) => r.id);
10434
+ if (unreadIds.length > 0) {
10435
+ flippedCount = markInboxSeen(opts.inboxDb, unreadIds);
10436
+ }
10437
+ }
10253
10438
  respond(res, 200, {
10254
10439
  items: rows.map((r) => ({
10255
10440
  id: r.id,
@@ -10261,11 +10446,65 @@ function makeHandler(opts) {
10261
10446
  sender_name: r.sender_name,
10262
10447
  body: r.body,
10263
10448
  received_at: new Date(r.received_at).toISOString(),
10264
- reply_to_id: r.reply_to_id
10265
- }))
10449
+ reply_to_id: r.reply_to_id,
10450
+ seen_at: r.seen_at ? new Date(r.seen_at).toISOString() : null
10451
+ })),
10452
+ marked_seen: flippedCount
10266
10453
  });
10267
10454
  return;
10268
10455
  }
10456
+ if (req.method === "POST" && url.pathname === "/v1/inbox/seen") {
10457
+ if (!opts.inboxDb) {
10458
+ respond(res, 503, { error: "inbox not initialised" });
10459
+ return;
10460
+ }
10461
+ try {
10462
+ const body = await readJsonBody(req, 64 * 1024);
10463
+ const ids = Array.isArray(body?.ids) ? body.ids.filter((x) => typeof x === "string") : [];
10464
+ if (ids.length === 0) {
10465
+ respond(res, 400, { error: "missing 'ids' (string[])" });
10466
+ return;
10467
+ }
10468
+ const flipped = markInboxSeen(opts.inboxDb, ids);
10469
+ respond(res, 200, { marked_seen: flipped });
10470
+ } catch (e) {
10471
+ respond(res, 400, { error: String(e) });
10472
+ }
10473
+ return;
10474
+ }
10475
+ if (req.method === "DELETE" && url.pathname === "/v1/inbox") {
10476
+ if (!opts.inboxDb) {
10477
+ respond(res, 503, { error: "inbox not initialised" });
10478
+ return;
10479
+ }
10480
+ const meshFilter = meshFromCtx(url.searchParams.get("mesh")) ?? undefined;
10481
+ const beforeRaw = url.searchParams.get("before");
10482
+ const before = beforeRaw ? Date.parse(beforeRaw) : undefined;
10483
+ const removed = flushInbox(opts.inboxDb, {
10484
+ ...meshFilter ? { mesh: meshFilter } : {},
10485
+ ...Number.isFinite(before) ? { before } : {}
10486
+ });
10487
+ respond(res, 200, { removed });
10488
+ return;
10489
+ }
10490
+ if (req.method === "DELETE" && url.pathname.startsWith("/v1/inbox/")) {
10491
+ if (!opts.inboxDb) {
10492
+ respond(res, 503, { error: "inbox not initialised" });
10493
+ return;
10494
+ }
10495
+ const id = url.pathname.slice("/v1/inbox/".length);
10496
+ if (!id) {
10497
+ respond(res, 400, { error: "missing id" });
10498
+ return;
10499
+ }
10500
+ const ok = deleteInboxRow(opts.inboxDb, id);
10501
+ if (!ok) {
10502
+ respond(res, 404, { error: "not found", id });
10503
+ return;
10504
+ }
10505
+ respond(res, 200, { removed: 1, id });
10506
+ return;
10507
+ }
10269
10508
  if (req.method === "GET" && url.pathname === "/v1/outbox") {
10270
10509
  if (!opts.outboxDb) {
10271
10510
  respond(res, 503, { error: "outbox not initialised" });
@@ -11199,6 +11438,11 @@ class SessionBrokerClient {
11199
11438
  return;
11200
11439
  }
11201
11440
  if (msg.type === "push" || msg.type === "inbound") {
11441
+ const senderPubkey = String(msg.senderPubkey ?? "").toLowerCase();
11442
+ if (senderPubkey && senderPubkey === this.opts.sessionPubkey.toLowerCase()) {
11443
+ this.log("info", "self_echo_dropped", { sender: senderPubkey.slice(0, 12) });
11444
+ return;
11445
+ }
11202
11446
  this.opts.onPush?.(msg);
11203
11447
  return;
11204
11448
  }
@@ -11490,6 +11734,46 @@ function defaultLog4(level, msg, meta) {
11490
11734
  var POLL_INTERVAL_MS = 500, MAX_ATTEMPTS_PER_ROW = 25, BACKOFF_BASE_MS = 500, BACKOFF_CAP_MS = 30000;
11491
11735
  var init_drain = () => {};
11492
11736
 
11737
+ // src/daemon/inbox-pruner.ts
11738
+ function startInboxPruner(opts) {
11739
+ const retentionMs = opts.retentionMs ?? DEFAULT_RETENTION_MS;
11740
+ const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
11741
+ const log2 = opts.log ?? defaultLog5;
11742
+ const tick = () => {
11743
+ try {
11744
+ const cutoff = Date.now() - retentionMs;
11745
+ const removed = pruneInboxBefore(opts.db, cutoff);
11746
+ if (removed > 0) {
11747
+ log2("info", "inbox_prune_completed", {
11748
+ removed,
11749
+ retention_days: Math.round(retentionMs / (24 * 60 * 60 * 1000))
11750
+ });
11751
+ }
11752
+ } catch (e) {
11753
+ log2("warn", "inbox_prune_failed", { err: String(e) });
11754
+ }
11755
+ };
11756
+ tick();
11757
+ const handle = setInterval(tick, intervalMs);
11758
+ if (typeof handle.unref === "function")
11759
+ handle.unref();
11760
+ return { stop: () => clearInterval(handle) };
11761
+ }
11762
+ function defaultLog5(level, msg, meta) {
11763
+ const line = JSON.stringify({ level, msg, ...meta, ts: new Date().toISOString() });
11764
+ if (level === "info")
11765
+ process.stdout.write(line + `
11766
+ `);
11767
+ else
11768
+ process.stderr.write(line + `
11769
+ `);
11770
+ }
11771
+ var DEFAULT_RETENTION_MS, DEFAULT_INTERVAL_MS;
11772
+ var init_inbox_pruner = __esm(() => {
11773
+ DEFAULT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
11774
+ DEFAULT_INTERVAL_MS = 60 * 60 * 1000;
11775
+ });
11776
+
11493
11777
  // src/daemon/inbound.ts
11494
11778
  import { randomUUID as randomUUID4 } from "node:crypto";
11495
11779
  async function handleBrokerPush(msg, ctx) {
@@ -11806,6 +12090,12 @@ async function runDaemon(opts = {}) {
11806
12090
  bus.publish("broker_status", { mesh: mesh.slug, status: s });
11807
12091
  },
11808
12092
  onPush: (m) => {
12093
+ const senderMemberPk = String(m.senderMemberPubkey ?? "").toLowerCase();
12094
+ const senderPubkey = String(m.senderPubkey ?? "").toLowerCase();
12095
+ const ownMember = mesh.pubkey.toLowerCase();
12096
+ if (senderMemberPk && senderMemberPk === ownMember && senderPubkey === ownMember) {
12097
+ return;
12098
+ }
11809
12099
  handleBrokerPush(m, {
11810
12100
  db: inboxDb,
11811
12101
  bus,
@@ -11827,6 +12117,7 @@ async function runDaemon(opts = {}) {
11827
12117
  brokers,
11828
12118
  getSessionBrokerByPubkey: (pubkey) => sessionBrokersByPubkey.get(pubkey)
11829
12119
  });
12120
+ const inboxPruner = startInboxPruner({ db: inboxDb });
11830
12121
  setRegistryHooks({
11831
12122
  onRegister: (info) => {
11832
12123
  if (!info.presence)
@@ -11930,6 +12221,7 @@ async function runDaemon(opts = {}) {
11930
12221
  shuttingDown = true;
11931
12222
  process.stdout.write(JSON.stringify({ msg: "daemon_shutdown", signal: sig, ts: new Date().toISOString() }) + `
11932
12223
  `);
12224
+ inboxPruner.stop();
11933
12225
  if (drain)
11934
12226
  await drain.close();
11935
12227
  for (const b of brokers.values()) {
@@ -11966,6 +12258,7 @@ var init_run = __esm(() => {
11966
12258
  init_broker();
11967
12259
  init_session_broker();
11968
12260
  init_drain();
12261
+ init_inbox_pruner();
11969
12262
  init_inbound();
11970
12263
  init_identity();
11971
12264
  init_facade();
@@ -16093,7 +16386,15 @@ claudemesh message send <p> "..." --priority low # pull-only
16093
16386
  claudemesh inbox # all attached meshes, last 100
16094
16387
  claudemesh inbox --mesh <slug> # scoped to one mesh
16095
16388
  claudemesh inbox --mesh <slug> --limit 20 # custom cap
16096
- claudemesh inbox --json # full row (sender_pubkey, mesh, body, received_at, …)
16389
+ claudemesh inbox --json # full row (sender_pubkey, mesh, body, received_at, seen_at, …)
16390
+ claudemesh inbox --unread # 1.34.8+ only rows whose seen_at IS NULL
16391
+
16392
+ # inbox flush + delete — 1.34.7+
16393
+ claudemesh inbox flush --mesh <slug> # delete all rows on one mesh
16394
+ claudemesh inbox flush --before <iso-timestamp> # delete rows older than timestamp
16395
+ claudemesh inbox flush --all # delete every row on every mesh (required guard)
16396
+ claudemesh inbox delete <id> # delete one inbox row by id (alias: rm)
16397
+ claudemesh inbox flush --mesh <slug> --json # JSON: { ok: true, removed: N }
16097
16398
 
16098
16399
  # delivery status (alias: claudemesh msg-status <id>)
16099
16400
  claudemesh message status <message-id>
@@ -16102,6 +16403,12 @@ claudemesh message status <message-id> --json
16102
16403
 
16103
16404
  **Inbox source (1.34.0+):** \`claudemesh inbox\` queries the daemon's persistent \`~/.claudemesh/daemon/inbox.db\` over IPC — it is NOT a fresh broker-WS buffer drain. Rows survive daemon restarts. Sender attribution is the actual session pubkey of the launched session that originated the send (NOT the stable member pubkey of the sender's daemon), so two sibling sessions of the same human appear as distinct rows.
16104
16405
 
16406
+ **Read-state (1.34.8+):** every inbox row carries a \`seen_at\` timestamp. \`null\` = never surfaced; an ISO string = first surfaced at that moment. The flag flips automatically when (a) the row is returned by an interactive \`claudemesh inbox\` listing, or (b) the MCP server emits a live \`<channel>\` reminder for it. The launch welcome push uses \`unread_only=true\` to surface only rows the user hasn't seen — so a session relaunched a day later sees what it actually missed, not the same 24h batch every time. Use \`claudemesh inbox --unread\` to get the same filter from the CLI.
16407
+
16408
+ **Self-echo guard (1.34.8+):** broker fan-out paths sometimes mirror an outbound DM back to the originating session-WS. The daemon now drops those at the WS boundary (matching on \`senderPubkey === own.session_pubkey\`), so the sender no longer sees their own \`claudemesh send\` arrive as a \`← claudemesh: <self>: ...\` channel push immediately after dispatching it.
16409
+
16410
+ **Inbox TTL (1.34.8+):** the daemon runs an hourly prune that deletes rows older than 30 days. Without this the inbox grew unbounded; now it self-trims while preserving "I went on holiday and want to see what I missed" recovery for a generous window. No CLI knob — it's a built-in retention policy. To override, manually \`claudemesh inbox flush --before <iso>\`.
16411
+
16105
16412
  \`send\` JSON output: \`{"ok": true, "messageId": "...", "target": "..."}\`. Errors: \`{"ok": false, "error": "..."}\`.
16106
16413
 
16107
16414
  ### \`state\` — shared per-mesh key-value store
@@ -18205,6 +18512,32 @@ function daemonGet(path2, opts = {}) {
18205
18512
  req.end();
18206
18513
  });
18207
18514
  }
18515
+ function daemonMarkSeen(ids, sessionToken) {
18516
+ return new Promise((resolve3) => {
18517
+ if (ids.length === 0) {
18518
+ resolve3();
18519
+ return;
18520
+ }
18521
+ const body = JSON.stringify({ ids });
18522
+ const headers = {
18523
+ "Content-Type": "application/json",
18524
+ "Content-Length": String(Buffer.byteLength(body))
18525
+ };
18526
+ if (sessionToken)
18527
+ headers.Authorization = `ClaudeMesh-Session ${sessionToken}`;
18528
+ const req = httpRequest2({ socketPath: DAEMON_PATHS.SOCK_FILE, path: "/v1/inbox/seen", method: "POST", timeout: 3000, headers }, (res) => {
18529
+ res.on("data", () => {});
18530
+ res.on("end", () => resolve3());
18531
+ });
18532
+ req.on("error", () => resolve3());
18533
+ req.on("timeout", () => {
18534
+ req.destroy();
18535
+ resolve3();
18536
+ });
18537
+ req.write(body);
18538
+ req.end();
18539
+ });
18540
+ }
18208
18541
  function subscribeEvents(onEvent) {
18209
18542
  let active = true;
18210
18543
  let req = null;
@@ -18417,6 +18750,8 @@ ${mf.allowed_tools.map((t) => ` - ${t}`).join(`
18417
18750
  } catch {}
18418
18751
  };
18419
18752
  mcpLog("mcp_started", { version: VERSION });
18753
+ const { readSessionTokenFromEnv: readSessionTokenFromEnv2 } = await Promise.resolve().then(() => (init_token(), exports_token));
18754
+ const sessionTokenForSeen = readSessionTokenFromEnv2();
18420
18755
  const sub = subscribeEvents(async (ev) => {
18421
18756
  mcpLog("sse_event_received", { kind: ev.kind });
18422
18757
  if (ev.kind === "message") {
@@ -18449,6 +18784,10 @@ ${mf.allowed_tools.map((t) => ` - ${t}`).join(`
18449
18784
  }
18450
18785
  });
18451
18786
  mcpLog("channel_emitted", { content_preview: content.slice(0, 80), mesh: String(d.mesh ?? "") });
18787
+ const inboxRowId = String(d.id ?? "");
18788
+ if (inboxRowId) {
18789
+ daemonMarkSeen([inboxRowId], sessionTokenForSeen).catch(() => {});
18790
+ }
18452
18791
  } catch (err) {
18453
18792
  mcpLog("channel_emit_failed", { err: String(err) });
18454
18793
  process.stderr.write(`[claudemesh-mcp] channel emit failed: ${err}
@@ -18548,8 +18887,7 @@ async function emitMeshWelcome(server, mcpLog) {
18548
18887
  } catch (e) {
18549
18888
  mcpLog("welcome_peers_lookup_failed", { err: String(e) });
18550
18889
  }
18551
- const sinceIso = new Date(Date.now() - 86400000).toISOString();
18552
- const inboxPath = selfMeshSlug ? `/v1/inbox?mesh=${encodeURIComponent(selfMeshSlug)}&since=${encodeURIComponent(sinceIso)}&limit=20` : `/v1/inbox?since=${encodeURIComponent(sinceIso)}&limit=20`;
18890
+ const inboxPath = selfMeshSlug ? `/v1/inbox?mesh=${encodeURIComponent(selfMeshSlug)}&unread_only=true&mark_seen=false&limit=50` : `/v1/inbox?unread_only=true&mark_seen=false&limit=50`;
18553
18891
  let inboxItems = [];
18554
18892
  try {
18555
18893
  const { status, body } = await daemonGet(inboxPath, { sessionToken });
@@ -18573,9 +18911,9 @@ async function emitMeshWelcome(server, mcpLog) {
18573
18911
  lines.push(`\uD83D\uDC65 Peer list unavailable (daemon query failed).`);
18574
18912
  }
18575
18913
  if (inboxItems.length === 0) {
18576
- lines.push(`\uD83D\uDCE5 Inbox is empty (last 24h).`);
18914
+ lines.push(`\uD83D\uDCE5 No unread messages.`);
18577
18915
  } else {
18578
- lines.push(`\uD83D\uDCE5 ${inboxItems.length} message${inboxItems.length === 1 ? "" : "s"} in inbox (last 24h):`);
18916
+ lines.push(`\uD83D\uDCE5 ${inboxItems.length} unread message${inboxItems.length === 1 ? "" : "s"}:`);
18579
18917
  for (const it of inboxItems.slice(0, 3)) {
18580
18918
  const sender = String(it.sender_name ?? "unknown");
18581
18919
  const senderPub = String(it.sender_pubkey ?? "").slice(0, 8);
@@ -18614,6 +18952,12 @@ async function emitMeshWelcome(server, mcpLog) {
18614
18952
  peer_count: peerCount,
18615
18953
  unread_count: inboxItems.length
18616
18954
  });
18955
+ if (inboxItems.length > 0) {
18956
+ const ids = inboxItems.map((it) => String(it.id ?? "")).filter(Boolean);
18957
+ if (ids.length > 0) {
18958
+ daemonMarkSeen(ids, sessionToken).catch(() => {});
18959
+ }
18960
+ }
18617
18961
  } catch (err) {
18618
18962
  mcpLog("welcome_emit_failed", { err: String(err) });
18619
18963
  }
@@ -18876,7 +19220,8 @@ var BOOLEAN_FLAGS = new Set([
18876
19220
  "force",
18877
19221
  "dry-run",
18878
19222
  "verbose",
18879
- "skip-service"
19223
+ "skip-service",
19224
+ "unread"
18880
19225
  ]);
18881
19226
  function parseArgv(argv) {
18882
19227
  const args = argv.slice(2);
@@ -19489,8 +19834,14 @@ Message (resource form)
19489
19834
  fans out to every sibling session of your member)
19490
19835
  [--json] (machine-readable result)
19491
19836
  claudemesh message inbox read persisted inbox (alias: inbox)
19492
- flags: [--mesh <slug>] [--limit N] [--json]
19837
+ flags: [--mesh <slug>] [--limit N] [--unread] [--json]
19493
19838
  reads ~/.claudemesh/daemon/inbox.db via daemon
19839
+ --unread → only rows never surfaced before (seen_at IS NULL);
19840
+ listing stamps returned rows seen as a side effect
19841
+ claudemesh inbox flush bulk-delete inbox rows
19842
+ flags: [--mesh <slug>] [--before <iso-timestamp>] [--all]
19843
+ --all required when neither --mesh nor --before is set
19844
+ claudemesh inbox delete <id> delete one inbox row by id (alias: rm)
19494
19845
  claudemesh message status <id> delivery status (alias: msg-status)
19495
19846
 
19496
19847
  Memory (resource form)
@@ -19847,8 +20198,27 @@ async function main() {
19847
20198
  break;
19848
20199
  }
19849
20200
  case "inbox": {
19850
- const { runInbox: runInbox2 } = await Promise.resolve().then(() => (init_inbox(), exports_inbox));
19851
- await runInbox2({ mesh: flags.mesh, json: !!flags.json, limit: typeof flags.limit === "number" ? flags.limit : typeof flags.limit === "string" ? Number.parseInt(flags.limit, 10) : undefined });
20201
+ const sub = positionals[0];
20202
+ if (sub === "flush") {
20203
+ const { runInboxFlush: runInboxFlush2 } = await Promise.resolve().then(() => (init_inbox_actions(), exports_inbox_actions));
20204
+ await runInboxFlush2({
20205
+ mesh: flags.mesh,
20206
+ before: flags.before,
20207
+ all: !!flags.all,
20208
+ json: !!flags.json
20209
+ });
20210
+ } else if (sub === "delete" || sub === "rm") {
20211
+ const { runInboxDelete: runInboxDelete2 } = await Promise.resolve().then(() => (init_inbox_actions(), exports_inbox_actions));
20212
+ await runInboxDelete2(positionals[1] ?? "", { json: !!flags.json });
20213
+ } else {
20214
+ const { runInbox: runInbox2 } = await Promise.resolve().then(() => (init_inbox(), exports_inbox));
20215
+ await runInbox2({
20216
+ mesh: flags.mesh,
20217
+ json: !!flags.json,
20218
+ limit: typeof flags.limit === "number" ? flags.limit : typeof flags.limit === "string" ? Number.parseInt(flags.limit, 10) : undefined,
20219
+ unread: !!flags.unread
20220
+ });
20221
+ }
19852
20222
  break;
19853
20223
  }
19854
20224
  case "state": {
@@ -20102,8 +20472,27 @@ async function main() {
20102
20472
  const { runSend: runSend2 } = await Promise.resolve().then(() => (init_send(), exports_send));
20103
20473
  await runSend2({ mesh: flags.mesh, priority: flags.priority, json: !!flags.json, self: !!flags.self }, positionals[1] ?? "", positionals.slice(2).join(" "));
20104
20474
  } else if (sub === "inbox") {
20105
- const { runInbox: runInbox2 } = await Promise.resolve().then(() => (init_inbox(), exports_inbox));
20106
- await runInbox2({ mesh: flags.mesh, json: !!flags.json, limit: typeof flags.limit === "number" ? flags.limit : typeof flags.limit === "string" ? Number.parseInt(flags.limit, 10) : undefined });
20475
+ const sub2 = positionals[1];
20476
+ if (sub2 === "flush") {
20477
+ const { runInboxFlush: runInboxFlush2 } = await Promise.resolve().then(() => (init_inbox_actions(), exports_inbox_actions));
20478
+ await runInboxFlush2({
20479
+ mesh: flags.mesh,
20480
+ before: flags.before,
20481
+ all: !!flags.all,
20482
+ json: !!flags.json
20483
+ });
20484
+ } else if (sub2 === "delete" || sub2 === "rm") {
20485
+ const { runInboxDelete: runInboxDelete2 } = await Promise.resolve().then(() => (init_inbox_actions(), exports_inbox_actions));
20486
+ await runInboxDelete2(positionals[2] ?? "", { json: !!flags.json });
20487
+ } else {
20488
+ const { runInbox: runInbox2 } = await Promise.resolve().then(() => (init_inbox(), exports_inbox));
20489
+ await runInbox2({
20490
+ mesh: flags.mesh,
20491
+ json: !!flags.json,
20492
+ limit: typeof flags.limit === "number" ? flags.limit : typeof flags.limit === "string" ? Number.parseInt(flags.limit, 10) : undefined,
20493
+ unread: !!flags.unread
20494
+ });
20495
+ }
20107
20496
  } else if (sub === "status") {
20108
20497
  const { runMsgStatus: runMsgStatus2 } = await Promise.resolve().then(() => (init_broker_actions(), exports_broker_actions));
20109
20498
  process.exit(await runMsgStatus2(positionals[1], { mesh: flags.mesh, json: !!flags.json }));
@@ -20656,4 +21045,4 @@ main().catch((err) => {
20656
21045
  process.exit(EXIT.INTERNAL_ERROR);
20657
21046
  });
20658
21047
 
20659
- //# debugId=944F6ED12781882C64756E2164756E21
21048
+ //# debugId=99681AB539B313DF64756E2164756E21