feat(cli): 1.34.7 → 1.34.13 — multi-session correctness train
Seven-ship sequence that took the daemon from "works for one session"
to "internally consistent for N sessions on one daemon." Architecture
invariant after 1.34.13: every shared store / channel scopes by
recipient (SSE demux at bind layer + token forwarding, inbox per-
recipient columns, outbox sender-session routing).
- 1.34.7 inbox flush + delete commands
- 1.34.8 seen_at column + TTL prune + first echo guard
- 1.34.9 broader echo guard + system-event polish + staleness warning
- 1.34.10 per-session SSE demux (SseFilterOptions) + universal daemon
(--mesh / --name deprecated) + daemon_started version stamp
- 1.34.11 inbox per-recipient column (storage half of 1.34.10)
- 1.34.12 daemon up detaches by default (logs to ~/.claudemesh/daemon/
daemon.log; service units explicitly pass --foreground)
- 1.34.13 MCP forwards session token on /v1/events — the actual fix
that activates 1.34.10's demux. Without this header the
daemon's session resolved null, filter was empty, every MCP
received the unfiltered global stream.
Roadmap entry at docs/roadmap.md captures the timeline + the four
known gaps tracked for follow-ups (launch env-var leak, broker
listPeers mesh-filter, kick on control-plane no-op, session caps as
first-class concept).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
73
apps/cli/src/daemon/inbox-pruner.ts
Normal file
73
apps/cli/src/daemon/inbox-pruner.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// 1.34.8: TTL prune for inbox.db.
|
||||
//
|
||||
// The inbox grows monotonically — every received DM lands as a row and
|
||||
// nothing removes it except an explicit `claudemesh inbox flush`. For
|
||||
// chatty meshes that's tens of thousands of rows over a few weeks.
|
||||
// SQLite handles that volume fine, but the rows are sitting there
|
||||
// forever and `claudemesh inbox` queries get slower as the table grows.
|
||||
//
|
||||
// The pruner runs hourly inside the daemon process and deletes rows
|
||||
// whose received_at is older than `retentionMs`. Default is 30 days,
|
||||
// which is generous for the "I went on holiday and want to see what I
|
||||
// missed" case but won't carry old rows into next year.
|
||||
//
|
||||
// Best-effort: a failure logs a warning and the pruner keeps trying on
|
||||
// the next interval. There's no shared state to corrupt — pruneInboxBefore
|
||||
// is a single DELETE statement.
|
||||
|
||||
import { pruneInboxBefore } from "./db/inbox.js";
|
||||
import type { SqliteDb } from "./db/sqlite.js";
|
||||
|
||||
export interface InboxPrunerOptions {
|
||||
db: SqliteDb;
|
||||
/** Retention window in ms. Rows with received_at < (now - retentionMs)
|
||||
* are deleted. Default: 30 days. */
|
||||
retentionMs?: number;
|
||||
/** How often to run the prune. Default: 1 hour. */
|
||||
intervalMs?: number;
|
||||
log?: (level: "info" | "warn" | "error", msg: string, meta?: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
export interface InboxPrunerHandle {
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
const DEFAULT_INTERVAL_MS = 60 * 60 * 1000;
|
||||
|
||||
export function startInboxPruner(opts: InboxPrunerOptions): InboxPrunerHandle {
|
||||
const retentionMs = opts.retentionMs ?? DEFAULT_RETENTION_MS;
|
||||
const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
||||
const log = opts.log ?? defaultLog;
|
||||
|
||||
const tick = (): void => {
|
||||
try {
|
||||
const cutoff = Date.now() - retentionMs;
|
||||
const removed = pruneInboxBefore(opts.db, cutoff);
|
||||
if (removed > 0) {
|
||||
log("info", "inbox_prune_completed", {
|
||||
removed,
|
||||
retention_days: Math.round(retentionMs / (24 * 60 * 60 * 1000)),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log("warn", "inbox_prune_failed", { err: String(e) });
|
||||
}
|
||||
};
|
||||
|
||||
// Run once at startup so a daemon that's been down for weeks reaps
|
||||
// immediately rather than waiting an hour.
|
||||
tick();
|
||||
|
||||
const handle = setInterval(tick, intervalMs);
|
||||
// Don't let the pruner block daemon shutdown.
|
||||
if (typeof handle.unref === "function") handle.unref();
|
||||
|
||||
return { stop: () => clearInterval(handle) };
|
||||
}
|
||||
|
||||
function defaultLog(level: "info" | "warn" | "error", msg: string, meta?: Record<string, unknown>) {
|
||||
const line = JSON.stringify({ level, msg, ...meta, ts: new Date().toISOString() });
|
||||
if (level === "info") process.stdout.write(line + "\n");
|
||||
else process.stderr.write(line + "\n");
|
||||
}
|
||||
Reference in New Issue
Block a user