feat: persist peer session state across disconnects ("welcome back" on reconnect)

Save groups, profile, visibility, summary, display name, and cumulative
stats to a new mesh.peer_state table on disconnect. On reconnect (same
meshId + memberId), restore them automatically — hello groups take
precedence over stored groups if provided. Broadcast peer_returned
system event with last-seen time and summary to other peers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-08 00:20:20 +01:00
parent e09671cdcb
commit fc8a7edc23
6 changed files with 315 additions and 17 deletions

View File

@@ -24,6 +24,22 @@ import type {
} from "./types";
import type { BrokerClient, InboundPush } from "../ws/client";
/** Compute a human-readable relative time string from an ISO timestamp. */
function relativeTime(isoStr: string): string {
const then = new Date(isoStr).getTime();
if (isNaN(then)) return "unknown";
const diffMs = Date.now() - then;
if (diffMs < 0) return "just now";
const seconds = Math.floor(diffMs / 1000);
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days} day${days !== 1 ? "s" : ""} ago`;
}
function text(msg: string, isError = false) {
return {
content: [{ type: "text" as const, text: msg }],
@@ -1352,6 +1368,14 @@ Your message mode is "${messageMode}".
content = `[heartbeat] tick ${tick} | sim time: ${simTime} | speed: x${speed}`;
} else if (eventName === "peer_joined") {
content = `[system] Peer "${data.name ?? "unknown"}" joined the mesh`;
} else if (eventName === "peer_returned") {
const peerName = String(data.name ?? "unknown");
const lastSeenAt = data.lastSeenAt ? relativeTime(String(data.lastSeenAt)) : "unknown";
const groups = Array.isArray(data.groups)
? (data.groups as Array<{ name: string; role?: string }>).map((g) => g.role ? `@${g.name}:${g.role}` : `@${g.name}`).join(", ")
: "";
const summary = data.summary ? ` Summary: "${data.summary}"` : "";
content = `[system] Welcome back, "${peerName}"! Last seen ${lastSeenAt}.${groups ? ` Restored: ${groups}` : ""}${summary}`;
} else if (eventName === "peer_left") {
content = `[system] Peer "${data.name ?? "unknown"}" left the mesh`;
} else if (eventName === "mcp_registered") {