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>
74 lines
2.7 KiB
TypeScript
74 lines
2.7 KiB
TypeScript
// 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");
|
|
}
|