@codemem/server 0.20.9 → 0.21.0

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.
package/dist/index.d.ts CHANGED
@@ -24,7 +24,7 @@ export interface AppOptions {
24
24
  sweeper?: RawEventSweeper | null;
25
25
  observer?: ObserverClient | null;
26
26
  getSyncRuntimeStatus?: () => {
27
- phase: "starting" | "running" | "stopping" | "error" | "disabled" | null;
27
+ phase: "starting" | "running" | "stopping" | "error" | "disabled" | "rebootstrapping" | "needs_attention" | null;
28
28
  detail?: string | null;
29
29
  } | null;
30
30
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAiB,OAAO,EAAE,MAAM,eAAe,CAAC;AAE1F,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAS5B,OAAO,EAAE,OAAO,EAAE,CAAC;AAKnB,uFAAuF;AACvF,wBAAgB,QAAQ,IAAI,WAAW,CAKtC;AAED,mDAAmD;AACnD,wBAAgB,UAAU,IAAI,IAAI,CAGjC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IAC1B,YAAY,CAAC,EAAE,MAAM,WAAW,CAAC;IACjC,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACjC,oBAAoB,CAAC,EAAE,MAAM;QAC5B,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,IAAI,CAAC;QACzE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,GAAG,IAAI,CAAC;CACT;AAED,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,UAAU,8EAiD1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,UAAU,8EAS9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAiB,OAAO,EAAE,MAAM,eAAe,CAAC;AAE1F,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAS5B,OAAO,EAAE,OAAO,EAAE,CAAC;AAKnB,uFAAuF;AACvF,wBAAgB,QAAQ,IAAI,WAAW,CAKtC;AAED,mDAAmD;AACnD,wBAAgB,UAAU,IAAI,IAAI,CAGjC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IAC1B,YAAY,CAAC,EAAE,MAAM,WAAW,CAAC;IACjC,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACjC,oBAAoB,CAAC,EAAE,MAAM;QAC5B,KAAK,EACF,UAAU,GACV,SAAS,GACT,UAAU,GACV,OAAO,GACP,UAAU,GACV,iBAAiB,GACjB,iBAAiB,GACjB,IAAI,CAAC;QACR,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,GAAG,IAAI,CAAC;CACT;AAED,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,UAAU,8EAiD1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,UAAU,8EAS9C"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, MemoryStore, VERSION, applyReplicationOps, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, cleanupNonces, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorReviewJoinRequestAction, coordinatorStatusSnapshot, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, listCoordinatorJoinRequests, listObserverProviderOptions, loadReplicationOpsSince, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, resolveDbPath, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, verifySignature, writeCodememConfigFile } from "@codemem/core";
3
+ import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, MemoryStore, VERSION, applyReplicationOps, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, cleanupNonces, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorReviewJoinRequestAction, coordinatorStatusSnapshot, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSyncResetState, listCoordinatorJoinRequests, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, resolveDbPath, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, verifySignature, writeCodememConfigFile } from "@codemem/core";
4
4
  import { serveStatic } from "@hono/node-server/serve-static";
5
5
  import { Hono } from "hono";
6
6
  import { createMiddleware } from "hono/factory";
@@ -145,6 +145,9 @@ var ALLOWED_KEYS = [
145
145
  "pack_observation_limit",
146
146
  "pack_session_limit",
147
147
  "sync_enabled",
148
+ "sync_retention_enabled",
149
+ "sync_retention_max_age_days",
150
+ "sync_retention_max_size_mb",
148
151
  "sync_host",
149
152
  "sync_port",
150
153
  "sync_interval_s",
@@ -167,6 +170,9 @@ var DEFAULTS = {
167
170
  pack_observation_limit: 50,
168
171
  pack_session_limit: 10,
169
172
  sync_enabled: false,
173
+ sync_retention_enabled: false,
174
+ sync_retention_max_age_days: 30,
175
+ sync_retention_max_size_mb: 512,
170
176
  sync_host: "0.0.0.0",
171
177
  sync_port: 7337,
172
178
  sync_interval_s: 120,
@@ -281,7 +287,7 @@ function validateAndApplyUpdate(configData, key, value, providers) {
281
287
  else delete configData[key];
282
288
  return null;
283
289
  }
284
- if (key === "sync_enabled" || key === "sync_mdns") {
290
+ if (key === "sync_enabled" || key === "sync_retention_enabled" || key === "sync_mdns") {
285
291
  if (typeof value !== "boolean") return `${key} must be boolean`;
286
292
  configData[key] = value;
287
293
  return null;
@@ -1140,7 +1146,7 @@ function statsRoutes(getStore) {
1140
1146
  * Sync routes — status, peers, actors, attempts, pairing, mutations.
1141
1147
  */
1142
1148
  var SYNC_STALE_AFTER_SECONDS = 600;
1143
- var SYNC_PROTOCOL_VERSION = "1";
1149
+ var SYNC_PROTOCOL_VERSION = "2";
1144
1150
  function intEnvOr(name, fallback) {
1145
1151
  const value = Number.parseInt(process.env[name] ?? "", 10);
1146
1152
  return Number.isFinite(value) ? value : fallback;
@@ -1446,10 +1452,12 @@ function syncProtocolRoutes(getStore) {
1446
1452
  fingerprint
1447
1453
  };
1448
1454
  }
1455
+ const syncReset = getSyncResetState(store.db);
1449
1456
  return c.json({
1450
1457
  device_id: device.device_id,
1451
1458
  protocol_version: SYNC_PROTOCOL_VERSION,
1452
- fingerprint: device.fingerprint
1459
+ fingerprint: device.fingerprint,
1460
+ sync_reset: syncReset
1453
1461
  });
1454
1462
  } catch {
1455
1463
  return c.json({ error: "internal_error" }, 500);
@@ -1463,15 +1471,36 @@ function syncProtocolRoutes(getStore) {
1463
1471
  try {
1464
1472
  const since = c.req.query("since") ?? null;
1465
1473
  const rawLimit = Number.parseInt(c.req.query("limit") ?? "200", 10);
1474
+ const rawGeneration = c.req.query("generation");
1475
+ const generation = rawGeneration != null && rawGeneration.trim().length > 0 ? Number.parseInt(rawGeneration, 10) : null;
1476
+ const snapshotId = c.req.query("snapshot_id") ?? null;
1477
+ const baselineCursor = c.req.query("baseline_cursor") ?? null;
1466
1478
  const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 1e3)) : 200;
1467
1479
  let localDeviceId = store.db.prepare("SELECT device_id FROM sync_device LIMIT 1").get();
1468
1480
  if (!localDeviceId) {
1469
1481
  const [deviceId] = ensureDeviceIdentity(store.db);
1470
1482
  localDeviceId = { device_id: deviceId };
1471
1483
  }
1472
- const [ops, nextCursor] = loadReplicationOpsSince(store.db, since, limit, localDeviceId.device_id);
1484
+ const result = loadReplicationOpsForPeer(store.db, {
1485
+ since,
1486
+ limit,
1487
+ deviceId: localDeviceId.device_id,
1488
+ generation: Number.isFinite(generation) ? generation : null,
1489
+ snapshotId,
1490
+ baselineCursor
1491
+ });
1492
+ if (result.reset_required) return c.json({
1493
+ error: "reset_required",
1494
+ ...result.reset
1495
+ }, 409);
1496
+ const { ops, nextCursor, boundary } = result;
1473
1497
  const filtered = filterOpsForPeer(store, peerDeviceId, ops);
1474
1498
  return c.json({
1499
+ reset_required: false,
1500
+ generation: boundary.generation,
1501
+ snapshot_id: boundary.snapshot_id,
1502
+ baseline_cursor: boundary.baseline_cursor,
1503
+ retained_floor_cursor: boundary.retained_floor_cursor,
1475
1504
  ops: filtered.allowed,
1476
1505
  next_cursor: nextCursor,
1477
1506
  skipped: filtered.skipped
@@ -1515,6 +1544,49 @@ function syncProtocolRoutes(getStore) {
1515
1544
  skipped: result.skipped + filteredInbound.skipped
1516
1545
  });
1517
1546
  });
1547
+ app.get("/v1/bootstrap/memories", (c) => {
1548
+ const store = getStore();
1549
+ const auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
1550
+ if (!auth.ok) return c.json(unauthorizedPayload(auth.reason), 401);
1551
+ const peerDeviceId = auth.deviceId;
1552
+ try {
1553
+ const rawLimit = Number.parseInt(c.req.query("limit") ?? "200", 10);
1554
+ const rawGeneration = c.req.query("generation");
1555
+ const generation = rawGeneration != null && rawGeneration.trim().length > 0 ? Number.parseInt(rawGeneration, 10) : null;
1556
+ const snapshotId = c.req.query("snapshot_id") ?? null;
1557
+ const baselineCursor = c.req.query("baseline_cursor") ?? null;
1558
+ const pageToken = c.req.query("page_token") ?? null;
1559
+ const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 1e3)) : 200;
1560
+ const page = loadMemorySnapshotPageForPeer(store.db, {
1561
+ limit,
1562
+ pageToken,
1563
+ peerDeviceId,
1564
+ generation: Number.isFinite(generation) ? generation : null,
1565
+ snapshotId,
1566
+ baselineCursor
1567
+ });
1568
+ return c.json({
1569
+ generation: page.boundary.generation,
1570
+ snapshot_id: page.boundary.snapshot_id,
1571
+ baseline_cursor: page.boundary.baseline_cursor,
1572
+ retained_floor_cursor: page.boundary.retained_floor_cursor,
1573
+ items: page.items,
1574
+ next_page_token: page.nextPageToken,
1575
+ has_more: page.hasMore
1576
+ });
1577
+ } catch (err) {
1578
+ if (err instanceof Error && (err.message === "generation_mismatch" || err.message === "boundary_mismatch")) {
1579
+ const boundary = getSyncResetState(store.db);
1580
+ return c.json({
1581
+ error: "reset_required",
1582
+ reset_required: true,
1583
+ reason: err.message,
1584
+ ...boundary
1585
+ }, 409);
1586
+ }
1587
+ return c.json({ error: "internal_error" }, 500);
1588
+ }
1589
+ });
1518
1590
  return app;
1519
1591
  }
1520
1592
  /**
@@ -1533,6 +1605,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
1533
1605
  const includeJoinRequests = queryBool(c.req.query("includeJoinRequests"));
1534
1606
  const project = c.req.query("project") || null;
1535
1607
  const config = readCoordinatorSyncConfig();
1608
+ const syncReset = getSyncResetState(store.db);
1536
1609
  const d = drizzle(store.db, { schema });
1537
1610
  const deviceRow = d.select({
1538
1611
  device_id: schema.syncDevice.device_id,
@@ -1540,6 +1613,12 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
1540
1613
  }).from(schema.syncDevice).limit(1).get();
1541
1614
  const daemonState = d.select().from(schema.syncDaemonState).where(eq(schema.syncDaemonState.id, 1)).get();
1542
1615
  const peerCountRow = d.select({ total: count() }).from(schema.syncPeers).get();
1616
+ let retentionState;
1617
+ try {
1618
+ retentionState = d.select().from(schema.syncRetentionState).where(eq(schema.syncRetentionState.id, 1)).get();
1619
+ } catch {
1620
+ retentionState = void 0;
1621
+ }
1543
1622
  const lastSyncRow = d.select({ last_sync_at: max(schema.syncPeers.last_sync_at) }).from(schema.syncPeers).get();
1544
1623
  const lastError = daemonState?.last_error;
1545
1624
  const lastErrorAt = daemonState?.last_error_at;
@@ -1554,6 +1633,19 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
1554
1633
  const statusPayload = {
1555
1634
  enabled: config.syncEnabled,
1556
1635
  interval_s: config.syncIntervalS,
1636
+ retention: {
1637
+ enabled: config.syncRetentionEnabled,
1638
+ max_age_days: config.syncRetentionMaxAgeDays,
1639
+ max_size_mb: config.syncRetentionMaxSizeMb,
1640
+ retained_floor_cursor: syncReset.retained_floor_cursor ?? retentionState?.retained_floor_cursor ?? null,
1641
+ last_run_at: retentionState?.last_run_at ?? null,
1642
+ last_duration_ms: typeof retentionState?.last_duration_ms === "number" ? retentionState.last_duration_ms : null,
1643
+ last_deleted_ops: typeof retentionState?.last_deleted_ops === "number" ? retentionState.last_deleted_ops : null,
1644
+ last_estimated_bytes_before: typeof retentionState?.last_estimated_bytes_before === "number" ? retentionState.last_estimated_bytes_before : null,
1645
+ last_estimated_bytes_after: typeof retentionState?.last_estimated_bytes_after === "number" ? retentionState.last_estimated_bytes_after : null,
1646
+ last_error: retentionState?.last_error ?? null,
1647
+ last_error_at: retentionState?.last_error_at ?? null
1648
+ },
1557
1649
  peer_count: Number(peerCountRow?.total ?? 0),
1558
1650
  last_sync_at: lastSyncRow?.last_sync_at ?? null,
1559
1651
  daemon_state: daemonStateValue,
@@ -1601,6 +1693,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
1601
1693
  status: attemptStatus(row),
1602
1694
  address: null
1603
1695
  }));
1696
+ const latestAttemptError = String(attemptsItems[0]?.error || "").trim();
1604
1697
  const statusBlock = {
1605
1698
  ...statusPayload,
1606
1699
  peers: peersMap,
@@ -1617,6 +1710,13 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
1617
1710
  } catch {
1618
1711
  joinRequests = [];
1619
1712
  }
1713
+ if (daemonStateValue === "ok" && latestAttemptError.startsWith("needs_attention:")) {
1714
+ daemonStateValue = "needs_attention";
1715
+ statusPayload.daemon_state = daemonStateValue;
1716
+ statusPayload.daemon_detail = latestAttemptError.replace(/^needs_attention:/, "");
1717
+ statusBlock.daemon_state = daemonStateValue;
1718
+ statusBlock.daemon_detail = latestAttemptError.replace(/^needs_attention:/, "");
1719
+ }
1620
1720
  if (daemonStateValue === "ok") {
1621
1721
  const peerStates = new Set(peersItems.map((peer) => String(peer.status?.peer_state ?? "")));
1622
1722
  const latestFailedRecently = Boolean(attemptsItems[0] && attemptsItems[0].status === "error" && isRecentIso(attemptsItems[0].finished_at));
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/middleware.ts","../src/routes/config.ts","../src/helpers.ts","../src/routes/memory.ts","../src/routes/observer-status.ts","../src/routes/raw-events.ts","../src/routes/stats.ts","../src/routes/sync.ts","../src/index.ts"],"sourcesContent":["/**\n * CORS and cross-origin protection middleware.\n *\n * Ports Python's reject_cross_origin() logic from codemem/viewer_http.py.\n * GETs are allowed from any origin (viewer is local-only).\n * Mutations (POST/DELETE/PATCH/PUT) require an Origin header matching a\n * loopback address, or are rejected with 403.\n */\n\nimport type { Context, Next } from \"hono\";\nimport { createMiddleware } from \"hono/factory\";\n\nconst LOOPBACK_HOSTS = new Set([\"127.0.0.1\", \"localhost\", \"::1\"]);\n\n/**\n * Check whether an Origin header value is a valid loopback URL.\n * Mirrors Python's _is_allowed_loopback_origin_url().\n */\nfunction isLoopbackOrigin(origin: string): boolean {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(origin);\n\t} catch {\n\t\treturn false;\n\t}\n\tif (url.protocol !== \"http:\" && url.protocol !== \"https:\") return false;\n\tif (url.username || url.password) return false;\n\treturn LOOPBACK_HOSTS.has(url.hostname);\n}\n\n/** HTTP methods that mutate state and require origin validation. */\nconst UNSAFE_METHODS = new Set([\"POST\", \"DELETE\", \"PATCH\", \"PUT\"]);\n\n/**\n * Check whether a missing-Origin request looks like a cross-site browser\n * request. Matches Python's `_is_unsafe_missing_origin()`:\n *\n * - Sec-Fetch-Site present and NOT same-origin/same-site/none → unsafe\n * - Referer present and NOT loopback → unsafe\n * - Otherwise → safe (CLI / programmatic caller, no browser context)\n */\nfunction isUnsafeMissingOrigin(c: Context): boolean {\n\tconst secFetchSite = (c.req.header(\"Sec-Fetch-Site\") ?? \"\").trim().toLowerCase();\n\tif (secFetchSite && ![\"same-origin\", \"same-site\", \"none\"].includes(secFetchSite)) {\n\t\treturn true;\n\t}\n\tconst referer = c.req.header(\"Referer\");\n\tif (!referer) return false;\n\treturn !isLoopbackOrigin(referer);\n}\n\n/**\n * Cross-origin protection middleware.\n *\n * Ports Python's `reject_cross_origin(missing_origin_policy=\"reject_if_unsafe\")`:\n *\n * - GET/HEAD/OPTIONS: allowed from any origin (viewer is local-only).\n * - POST/DELETE/PATCH/PUT:\n * - Origin present + loopback → allowed (browser on localhost)\n * - Origin present + non-loopback → rejected 403\n * - No Origin + no suspicious browser signals → allowed (CLI callers)\n * - No Origin + suspicious Sec-Fetch-Site/Referer → rejected 403\n *\n * For same-origin requests (no Origin header) on safe methods, no\n * Access-Control-Allow-Origin is set — the browser doesn't need it.\n * For valid loopback origins, ACAO is echoed back.\n */\nexport function originGuard() {\n\treturn createMiddleware(async (c: Context, next: Next) => {\n\t\tconst origin = c.req.header(\"Origin\");\n\t\tconst method = c.req.method;\n\n\t\tif (UNSAFE_METHODS.has(method)) {\n\t\t\tif (origin) {\n\t\t\t\t// Origin present — must be loopback\n\t\t\t\tif (!isLoopbackOrigin(origin)) {\n\t\t\t\t\treturn c.json({ error: \"forbidden\" }, 403);\n\t\t\t\t}\n\t\t\t\t// Valid loopback origin — echo it for CORS\n\t\t\t\tc.header(\"Access-Control-Allow-Origin\", origin);\n\t\t\t\tc.header(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\");\n\t\t\t\tc.header(\"Access-Control-Allow-Headers\", \"Content-Type\");\n\t\t\t} else {\n\t\t\t\t// No Origin — reject only if browser signals indicate cross-site\n\t\t\t\t// (matches Python's reject_if_unsafe policy for API endpoints)\n\t\t\t\tif (isUnsafeMissingOrigin(c)) {\n\t\t\t\t\treturn c.json({ error: \"forbidden\" }, 403);\n\t\t\t\t}\n\t\t\t\t// CLI / programmatic caller — no CORS headers needed\n\t\t\t}\n\t\t} else if (origin && isLoopbackOrigin(origin)) {\n\t\t\t// Safe method with valid origin — echo for preflight\n\t\t\tc.header(\"Access-Control-Allow-Origin\", origin);\n\t\t\tc.header(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\");\n\t\t\tc.header(\"Access-Control-Allow-Headers\", \"Content-Type\");\n\t\t}\n\t\t// No origin or non-loopback on safe method: no ACAO header set.\n\t\t// Browser enforces same-origin; we don't set permissive headers.\n\n\t\tawait next();\n\t});\n}\n\n/**\n * Handle OPTIONS preflight requests.\n * Returns 204 with appropriate CORS headers for loopback origins.\n */\nexport function preflightHandler() {\n\treturn createMiddleware(async (c: Context, next: Next) => {\n\t\tif (c.req.method !== \"OPTIONS\") {\n\t\t\tawait next();\n\t\t\treturn;\n\t\t}\n\t\tconst origin = c.req.header(\"Origin\");\n\t\tif (origin && isLoopbackOrigin(origin)) {\n\t\t\tc.header(\"Access-Control-Allow-Origin\", origin);\n\t\t\tc.header(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\");\n\t\t\tc.header(\"Access-Control-Allow-Headers\", \"Content-Type\");\n\t\t\tc.header(\"Access-Control-Max-Age\", \"86400\");\n\t\t\treturn c.body(null, 204);\n\t\t}\n\t\treturn c.body(null, 204);\n\t});\n}\n","/**\n * Config routes — GET /api/config, POST /api/config.\n *\n * Ports the user-facing config read/write path from Python's\n * codemem/viewer_routes/config.py, scoped to the TS runtime's current needs.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n\tCODEMEM_CONFIG_ENV_OVERRIDES,\n\tgetCodememConfigPath,\n\tgetCodememEnvOverrides,\n\tlistObserverProviderOptions,\n\ttype RawEventSweeper,\n\treadCodememConfigFile,\n\tstripJsonComments,\n\tstripTrailingCommas,\n\twriteCodememConfigFile,\n} from \"@codemem/core\";\nimport { Hono } from \"hono\";\n\ntype ConfigData = Record<string, unknown>;\n\nconst RUNTIMES = new Set([\"api_http\", \"claude_sidecar\"]);\nconst AUTH_SOURCES = new Set([\"auto\", \"env\", \"file\", \"command\", \"none\"]);\nconst HOT_RELOAD_KEYS = new Set([\"raw_events_sweeper_interval_s\"]);\nconst ALLOWED_KEYS = [\n\t\"claude_command\",\n\t\"observer_base_url\",\n\t\"observer_provider\",\n\t\"observer_model\",\n\t\"observer_runtime\",\n\t\"observer_auth_source\",\n\t\"observer_auth_file\",\n\t\"observer_auth_command\",\n\t\"observer_auth_timeout_ms\",\n\t\"observer_auth_cache_ttl_s\",\n\t\"observer_headers\",\n\t\"observer_max_chars\",\n\t\"pack_observation_limit\",\n\t\"pack_session_limit\",\n\t\"sync_enabled\",\n\t\"sync_host\",\n\t\"sync_port\",\n\t\"sync_interval_s\",\n\t\"sync_mdns\",\n\t\"sync_coordinator_url\",\n\t\"sync_coordinator_group\",\n\t\"sync_coordinator_timeout_s\",\n\t\"sync_coordinator_presence_ttl_s\",\n\t\"raw_events_sweeper_interval_s\",\n] as const;\n\nconst DEFAULTS: ConfigData = {\n\tclaude_command: [\"claude\"],\n\tobserver_runtime: \"api_http\",\n\tobserver_auth_source: \"auto\",\n\tobserver_auth_command: [],\n\tobserver_auth_timeout_ms: 1500,\n\tobserver_auth_cache_ttl_s: 300,\n\tobserver_headers: {},\n\tobserver_max_chars: 12000,\n\tpack_observation_limit: 50,\n\tpack_session_limit: 10,\n\tsync_enabled: false,\n\tsync_host: \"0.0.0.0\",\n\tsync_port: 7337,\n\tsync_interval_s: 120,\n\tsync_mdns: true,\n\tsync_coordinator_timeout_s: 3,\n\tsync_coordinator_presence_ttl_s: 180,\n\traw_events_sweeper_interval_s: 30,\n};\n\nexport interface ConfigRouteOptions {\n\tgetSweeper?: () => RawEventSweeper | null;\n}\n\nfunction loadProviderOptions(): string[] {\n\treturn listObserverProviderOptions();\n}\n\nfunction getConfigPath(): string {\n\tconst envPath = process.env.CODEMEM_CONFIG;\n\tif (envPath) return envPath.replace(/^~/, homedir());\n\tconst configDir = join(homedir(), \".config\", \"codemem\");\n\tconst candidates = [join(configDir, \"config.json\"), join(configDir, \"config.jsonc\")];\n\treturn candidates.find((p) => existsSync(p)) ?? join(configDir, \"config.json\");\n}\n\nfunction readConfigFile(configPath: string): ConfigData {\n\tif (!existsSync(configPath)) return {};\n\ttry {\n\t\tlet text = readFileSync(configPath, \"utf-8\").trim();\n\t\tif (!text) return {};\n\t\ttry {\n\t\t\treturn JSON.parse(text) as ConfigData;\n\t\t} catch {\n\t\t\ttext = stripTrailingCommas(stripJsonComments(text));\n\t\t\treturn JSON.parse(text) as ConfigData;\n\t\t}\n\t} catch {\n\t\treturn {};\n\t}\n}\n\nfunction getEffectiveConfig(configData: ConfigData): ConfigData {\n\tconst effective: ConfigData = { ...DEFAULTS, ...configData };\n\tfor (const [key, envVar] of Object.entries(CODEMEM_CONFIG_ENV_OVERRIDES) as Array<\n\t\t[string, string]\n\t>) {\n\t\tconst val = process.env[envVar];\n\t\tif (val != null && val !== \"\") effective[key] = val;\n\t}\n\treturn effective;\n}\n\nfunction parsePositiveInt(value: unknown, allowZero = false): number | null {\n\tif (typeof value === \"boolean\") return null;\n\tconst parsed =\n\t\ttypeof value === \"number\"\n\t\t\t? value\n\t\t\t: typeof value === \"string\" && /^-?\\d+$/.test(value.trim())\n\t\t\t\t? Number(value.trim())\n\t\t\t\t: Number.NaN;\n\tif (!Number.isFinite(parsed) || !Number.isInteger(parsed)) return null;\n\tif (allowZero) return parsed >= 0 ? parsed : null;\n\treturn parsed > 0 ? parsed : null;\n}\n\nfunction asStringMap(value: unknown): Record<string, string> | null {\n\tif (value == null || typeof value !== \"object\" || Array.isArray(value)) return null;\n\tconst parsed: Record<string, string> = {};\n\tfor (const [key, item] of Object.entries(value)) {\n\t\tif (typeof item !== \"string\") return null;\n\t\tconst stripped = key.trim();\n\t\tif (!stripped) return null;\n\t\tparsed[stripped] = item;\n\t}\n\treturn parsed;\n}\n\nfunction asExecutableArgv(value: unknown): string[] | null {\n\tif (!Array.isArray(value)) return null;\n\tconst argv: string[] = [];\n\tfor (const item of value) {\n\t\tif (typeof item !== \"string\") return null;\n\t\tconst token = item.trim();\n\t\tif (!token) return null;\n\t\targv.push(token);\n\t}\n\treturn argv;\n}\n\nfunction validateAndApplyUpdate(\n\tconfigData: ConfigData,\n\tkey: (typeof ALLOWED_KEYS)[number],\n\tvalue: unknown,\n\tproviders: Set<string>,\n): string | null {\n\tif (value == null || value === \"\") {\n\t\tdelete configData[key];\n\t\treturn null;\n\t}\n\tif (key === \"observer_provider\") {\n\t\tif (typeof value !== \"string\") return \"observer_provider must be string\";\n\t\tconst provider = value.trim().toLowerCase();\n\t\tconst savedBaseUrl = configData.observer_base_url;\n\t\tconst hasSavedBaseUrl = typeof savedBaseUrl === \"string\" && savedBaseUrl.trim().length > 0;\n\t\tif (!providers.has(provider) && !hasSavedBaseUrl) {\n\t\t\treturn \"observer_provider must match a configured provider\";\n\t\t}\n\t\tconfigData[key] = provider;\n\t\treturn null;\n\t}\n\tif (key === \"observer_runtime\") {\n\t\tif (typeof value !== \"string\") return \"observer_runtime must be string\";\n\t\tconst runtime = value.trim().toLowerCase();\n\t\tif (!RUNTIMES.has(runtime)) {\n\t\t\treturn \"observer_runtime must be one of: api_http, claude_sidecar\";\n\t\t}\n\t\tconfigData[key] = runtime;\n\t\treturn null;\n\t}\n\tif (key === \"observer_auth_source\") {\n\t\tif (typeof value !== \"string\") return \"observer_auth_source must be string\";\n\t\tconst source = value.trim().toLowerCase();\n\t\tif (!AUTH_SOURCES.has(source)) {\n\t\t\treturn \"observer_auth_source must be one of: auto, env, file, command, none\";\n\t\t}\n\t\tconfigData[key] = source;\n\t\treturn null;\n\t}\n\tif (key === \"claude_command\" || key === \"observer_auth_command\") {\n\t\tconst argv = asExecutableArgv(value);\n\t\tif (argv == null) return `${key} must be string array`;\n\t\tif (argv.length > 0) configData[key] = argv;\n\t\telse delete configData[key];\n\t\treturn null;\n\t}\n\tif (key === \"observer_headers\") {\n\t\tconst headers = asStringMap(value);\n\t\tif (headers == null) return \"observer_headers must be object of string values\";\n\t\tif (Object.keys(headers).length > 0) configData[key] = headers;\n\t\telse delete configData[key];\n\t\treturn null;\n\t}\n\tif (key === \"sync_enabled\" || key === \"sync_mdns\") {\n\t\tif (typeof value !== \"boolean\") return `${key} must be boolean`;\n\t\tconfigData[key] = value;\n\t\treturn null;\n\t}\n\tif (\n\t\tkey === \"observer_base_url\" ||\n\t\tkey === \"observer_model\" ||\n\t\tkey === \"observer_auth_file\" ||\n\t\tkey === \"sync_host\" ||\n\t\tkey === \"sync_coordinator_url\" ||\n\t\tkey === \"sync_coordinator_group\"\n\t) {\n\t\tif (typeof value !== \"string\") return `${key} must be string`;\n\t\tconst trimmed = value.trim();\n\t\tif (!trimmed) delete configData[key];\n\t\telse configData[key] = trimmed;\n\t\treturn null;\n\t}\n\tconst allowZero = key === \"observer_auth_cache_ttl_s\";\n\tconst parsed = parsePositiveInt(value, allowZero);\n\tif (parsed == null) return `${key} must be ${allowZero ? \"non-negative int\" : \"int\"}`;\n\tconfigData[key] = parsed;\n\treturn null;\n}\n\nfunction applyRuntimeEffects(changedKeys: string[], opts: ConfigRouteOptions): string[] {\n\tconst applied: string[] = [];\n\tif (changedKeys.includes(\"raw_events_sweeper_interval_s\")) {\n\t\tconst configValue = readCodememConfigFile().raw_events_sweeper_interval_s;\n\t\tconst seconds =\n\t\t\ttypeof configValue === \"number\"\n\t\t\t\t? configValue\n\t\t\t\t: Number.parseInt(String(configValue ?? \"\"), 10);\n\t\tif (Number.isFinite(seconds) && seconds > 0) {\n\t\t\tprocess.env.CODEMEM_RAW_EVENTS_SWEEPER_INTERVAL_MS = String(seconds * 1000);\n\t\t} else {\n\t\t\tdelete process.env.CODEMEM_RAW_EVENTS_SWEEPER_INTERVAL_MS;\n\t\t}\n\t\topts.getSweeper?.()?.notifyConfigChanged();\n\t\tapplied.push(\"raw_events_sweeper_interval_s\");\n\t}\n\treturn applied;\n}\n\nexport function configRoutes(opts: ConfigRouteOptions = {}) {\n\tconst app = new Hono();\n\n\tapp.get(\"/api/config\", (c) => {\n\t\tconst configPath = getConfigPath();\n\t\tconst configData = readConfigFile(configPath);\n\t\treturn c.json({\n\t\t\tpath: configPath,\n\t\t\tconfig: configData,\n\t\t\tdefaults: DEFAULTS,\n\t\t\teffective: getEffectiveConfig(configData),\n\t\t\tenv_overrides: getCodememEnvOverrides(),\n\t\t\tproviders: loadProviderOptions(),\n\t\t});\n\t});\n\n\tapp.post(\"/api/config\", async (c) => {\n\t\tlet payload: unknown;\n\t\ttry {\n\t\t\tpayload = (await c.req.json()) as unknown;\n\t\t} catch {\n\t\t\treturn c.json({ error: \"invalid json\" }, 400);\n\t\t}\n\t\tif (payload == null || typeof payload !== \"object\" || Array.isArray(payload)) {\n\t\t\treturn c.json({ error: \"payload must be an object\" }, 400);\n\t\t}\n\t\tif (\n\t\t\t\"config\" in payload &&\n\t\t\t(payload as ConfigData).config != null &&\n\t\t\t(typeof (payload as ConfigData).config !== \"object\" ||\n\t\t\t\tArray.isArray((payload as ConfigData).config))\n\t\t) {\n\t\t\treturn c.json({ error: \"config must be an object\" }, 400);\n\t\t}\n\t\tconst updates =\n\t\t\t\"config\" in payload &&\n\t\t\t(payload as ConfigData).config != null &&\n\t\t\ttypeof (payload as ConfigData).config === \"object\" &&\n\t\t\t!Array.isArray((payload as ConfigData).config)\n\t\t\t\t? ((payload as ConfigData).config as ConfigData)\n\t\t\t\t: (payload as ConfigData);\n\n\t\tconst configPath = getCodememConfigPath();\n\t\tconst beforeConfig = readCodememConfigFile();\n\t\tconst beforeEffective = getEffectiveConfig(beforeConfig);\n\t\tconst nextConfig: ConfigData = { ...beforeConfig };\n\t\tconst providers = new Set(loadProviderOptions());\n\n\t\tconst touchedKeys = ALLOWED_KEYS.filter((key) => key in updates);\n\t\tfor (const key of ALLOWED_KEYS) {\n\t\t\tif (!(key in updates)) continue;\n\t\t\tconst error = validateAndApplyUpdate(nextConfig, key, updates[key], providers);\n\t\t\tif (error) return c.json({ error }, 400);\n\t\t}\n\n\t\tlet savedPath: string;\n\t\ttry {\n\t\t\tsavedPath = writeCodememConfigFile(nextConfig, configPath);\n\t\t} catch {\n\t\t\treturn c.json({ error: \"failed to write config\" }, 500);\n\t\t}\n\n\t\tconst afterEffective = getEffectiveConfig(nextConfig);\n\t\tconst savedChangedKeys = ALLOWED_KEYS.filter((key) => beforeConfig[key] !== nextConfig[key]);\n\t\tconst effectiveChangedKeys = ALLOWED_KEYS.filter(\n\t\t\t(key) => beforeEffective[key] !== afterEffective[key],\n\t\t);\n\t\tconst envOverrides = getCodememEnvOverrides();\n\t\tconst ignoredByEnvKeys = savedChangedKeys.filter(\n\t\t\t(key) => !effectiveChangedKeys.includes(key) && key in envOverrides,\n\t\t);\n\t\tconst runtimeChangedKeys = [\n\t\t\t...new Set([...touchedKeys, ...savedChangedKeys, ...effectiveChangedKeys]),\n\t\t];\n\t\tconst hotReloadedKeys = applyRuntimeEffects(runtimeChangedKeys, opts);\n\t\tconst restartRequiredKeys = effectiveChangedKeys.filter(\n\t\t\t(key) => !HOT_RELOAD_KEYS.has(key) && !(key in envOverrides),\n\t\t);\n\n\t\treturn c.json({\n\t\t\tpath: savedPath,\n\t\t\tconfig: nextConfig,\n\t\t\teffective: afterEffective,\n\t\t\teffects: {\n\t\t\t\tsaved_keys: savedChangedKeys,\n\t\t\t\teffective_keys: effectiveChangedKeys,\n\t\t\t\thot_reloaded_keys: hotReloadedKeys,\n\t\t\t\trestart_required_keys: restartRequiredKeys,\n\t\t\t\tignored_by_env_keys: ignoredByEnvKeys,\n\t\t\t\twarnings: ignoredByEnvKeys.map(\n\t\t\t\t\t(key) =>\n\t\t\t\t\t\t`${key} is currently controlled by ${envOverrides[key]}; saved config will not take effect until that override is removed.`,\n\t\t\t\t),\n\t\t\t},\n\t\t});\n\t});\n\n\treturn app;\n}\n","import { parseStrictInteger } from \"@codemem/core\";\n\n/**\n * Shared helpers for viewer-server routes.\n */\n\n/**\n * Parse a JSON string that should be an array of strings.\n * Returns an empty array on null, invalid JSON, or non-array values.\n * Mirrors Python's store._safe_json_list().\n */\nexport function safeJsonList(raw: string | null | undefined): string[] {\n\tif (raw == null) return [];\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(raw);\n\t\tif (!Array.isArray(parsed)) return [];\n\t\treturn parsed.filter((item): item is string => typeof item === \"string\");\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/**\n * Parse a query parameter as an integer, returning the default on failure.\n */\nexport function queryInt(value: string | undefined, defaultValue: number): number {\n\tif (value == null) return defaultValue;\n\tconst parsed = parseStrictInteger(value);\n\treturn parsed == null ? defaultValue : parsed;\n}\n\n/**\n * Parse a query parameter as a boolean flag.\n * Recognizes \"1\", \"true\", \"yes\" as truthy.\n */\nexport function queryBool(value: string | undefined): boolean {\n\tif (value == null) return false;\n\treturn value === \"1\" || value === \"true\" || value === \"yes\";\n}\n","/**\n * Memory routes — observations, summaries, sessions, projects, pack, artifacts.\n */\n\nimport type { MemoryStore } from \"@codemem/core\";\nimport { buildFilterClausesWithContext, fromJson, parseStrictInteger, schema } from \"@codemem/core\";\nimport { desc, eq, inArray, isNotNull } from \"drizzle-orm\";\nimport { drizzle } from \"drizzle-orm/better-sqlite3\";\nimport { Hono } from \"hono\";\nimport { queryInt } from \"../helpers.js\";\n\ntype StoreFactory = () => MemoryStore;\n\n/**\n * Attach session project/cwd fields to memory items.\n */\nfunction attachSessionFields(store: MemoryStore, items: Record<string, unknown>[]): void {\n\tconst sessionIds: number[] = [];\n\tconst seen = new Set<number>();\n\tfor (const item of items) {\n\t\tconst value = item.session_id;\n\t\tif (value == null) continue;\n\t\tconst sid = Number(value);\n\t\tif (Number.isNaN(sid) || seen.has(sid)) continue;\n\t\tseen.add(sid);\n\t\tsessionIds.push(sid);\n\t}\n\tif (sessionIds.length === 0) return;\n\n\tconst d = drizzle(store.db, { schema });\n\tconst rows = d\n\t\t.select({\n\t\t\tid: schema.sessions.id,\n\t\t\tproject: schema.sessions.project,\n\t\t\tcwd: schema.sessions.cwd,\n\t\t})\n\t\t.from(schema.sessions)\n\t\t.where(inArray(schema.sessions.id, sessionIds))\n\t\t.all();\n\n\tconst bySession = new Map<number, { project: string; cwd: string }>();\n\tfor (const row of rows) {\n\t\tconst projectRaw = String(row.project ?? \"\").trim();\n\t\tconst project = projectRaw ? projectBasename(projectRaw) : \"\";\n\t\tconst cwd = String(row.cwd ?? \"\");\n\t\tbySession.set(row.id, { project, cwd });\n\t}\n\n\tfor (const item of items) {\n\t\tconst sid = Number(item.session_id);\n\t\tif (Number.isNaN(sid)) continue;\n\t\tconst fields = bySession.get(sid);\n\t\tif (!fields) continue;\n\t\titem.project ??= fields.project;\n\t\titem.cwd ??= fields.cwd;\n\t}\n}\n\n/**\n * Extract the basename of a project path.\n * Strips \"fatal:\" prefixed values.\n */\nfunction projectBasename(raw: string): string {\n\tif (raw.toLowerCase().startsWith(\"fatal:\")) return \"\";\n\tconst parts = raw.replace(/\\\\/g, \"/\").split(\"/\");\n\treturn parts[parts.length - 1] ?? raw;\n}\n\nfunction normalizeScope(raw: string | undefined): \"mine\" | \"theirs\" | undefined {\n\tconst value = String(raw ?? \"\")\n\t\t.trim()\n\t\t.toLowerCase();\n\tif (value === \"mine\" || value === \"theirs\") return value;\n\treturn undefined;\n}\n\nfunction queryMemoryPage(\n\tstore: MemoryStore,\n\toptions: {\n\t\tlimit: number;\n\t\toffset: number;\n\t\tproject?: string;\n\t\tscope?: \"mine\" | \"theirs\";\n\t},\n): Record<string, unknown>[] {\n\tconst filters: Record<string, unknown> = {};\n\tif (options.project) filters.project = options.project;\n\tif (options.scope) filters.ownership_scope = options.scope;\n\n\tconst filterResult = buildFilterClausesWithContext(filters, {\n\t\tactorId: store.actorId,\n\t\tdeviceId: store.deviceId,\n\t});\n\tconst clauses = [\"memory_items.active = 1\", ...filterResult.clauses];\n\tconst where = clauses.join(\" AND \");\n\tconst from = filterResult.joinSessions\n\t\t? \"memory_items JOIN sessions ON sessions.id = memory_items.session_id\"\n\t\t: \"memory_items\";\n\n\tconst rows = store.db\n\t\t.prepare(\n\t\t\t`SELECT memory_items.* FROM ${from}\n\t\t\t WHERE ${where}\n\t\t\t ORDER BY memory_items.created_at DESC\n\t\t\t LIMIT ? OFFSET ?`,\n\t\t)\n\t\t.all(...filterResult.params, options.limit + 1, options.offset) as Record<string, unknown>[];\n\n\treturn rows.map((row) => ({\n\t\t...row,\n\t\tmetadata_json: fromJson((row.metadata_json as string) ?? null),\n\t}));\n}\n\nfunction isSummaryLikeMemory(item: Record<string, unknown>): boolean {\n\tif (String(item.kind ?? \"\").toLowerCase() === \"session_summary\") return true;\n\tconst metadata = (item.metadata_json ?? {}) as Record<string, unknown>;\n\tif (metadata.is_summary === true) return true;\n\treturn (\n\t\tString(metadata.source ?? \"\")\n\t\t\t.trim()\n\t\t\t.toLowerCase() === \"observer_summary\"\n\t);\n}\n\nfunction selectMemoryPage(\n\tstore: MemoryStore,\n\toptions: {\n\t\tlimit: number;\n\t\toffset: number;\n\t\tproject?: string;\n\t\tscope?: \"mine\" | \"theirs\";\n\t\tmatcher: (item: Record<string, unknown>) => boolean;\n\t},\n): Record<string, unknown>[] {\n\tconst pageSize = Math.max(options.limit + options.offset + 10, 50);\n\tlet rawOffset = 0;\n\tconst matched: Record<string, unknown>[] = [];\n\n\twhile (matched.length < options.offset + options.limit + 1) {\n\t\tconst page = queryMemoryPage(store, {\n\t\t\tlimit: pageSize,\n\t\t\toffset: rawOffset,\n\t\t\tproject: options.project,\n\t\t\tscope: options.scope,\n\t\t});\n\t\tif (page.length === 0) break;\n\t\tmatched.push(...page.filter(options.matcher));\n\t\tif (page.length < pageSize) break;\n\t\trawOffset += page.length;\n\t}\n\n\treturn matched.slice(options.offset, options.offset + options.limit + 1);\n}\n\nexport function memoryRoutes(getStore: StoreFactory) {\n\tconst app = new Hono();\n\n\t// GET /api/sessions\n\tapp.get(\"/api/sessions\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst limit = queryInt(c.req.query(\"limit\"), 20);\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst rows = d\n\t\t\t\t.select()\n\t\t\t\t.from(schema.sessions)\n\t\t\t\t.orderBy(desc(schema.sessions.started_at))\n\t\t\t\t.limit(limit)\n\t\t\t\t.all();\n\t\t\tconst items = rows.map((row) => ({\n\t\t\t\t...row,\n\t\t\t\tmetadata_json: fromJson(row.metadata_json),\n\t\t\t}));\n\t\t\treturn c.json({ items });\n\t\t}\n\t});\n\n\t// GET /api/projects\n\tapp.get(\"/api/projects\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst rows = d\n\t\t\t\t.selectDistinct({ project: schema.sessions.project })\n\t\t\t\t.from(schema.sessions)\n\t\t\t\t.where(isNotNull(schema.sessions.project))\n\t\t\t\t.all();\n\t\t\tconst projects = [\n\t\t\t\t...new Set(\n\t\t\t\t\trows\n\t\t\t\t\t\t.map((r) => String(r.project ?? \"\").trim())\n\t\t\t\t\t\t.filter((p) => p && !p.toLowerCase().startsWith(\"fatal:\"))\n\t\t\t\t\t\t.map((p) => projectBasename(p))\n\t\t\t\t\t\t.filter(Boolean),\n\t\t\t\t),\n\t\t\t].sort();\n\t\t\treturn c.json({ projects });\n\t\t}\n\t});\n\n\t// GET /api/observations (aliased from /api/memories)\n\tapp.get(\"/api/memories\", (c) => {\n\t\tconst search = new URL(c.req.url).search;\n\t\treturn c.redirect(`/api/observations${search}`, 301);\n\t});\n\n\tapp.get(\"/api/observations\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst limit = Math.max(1, queryInt(c.req.query(\"limit\"), 20));\n\t\t\tconst offset = Math.max(0, queryInt(c.req.query(\"offset\"), 0));\n\t\t\tconst project = c.req.query(\"project\") || undefined;\n\t\t\tconst scope = normalizeScope(c.req.query(\"scope\"));\n\t\t\tconst items = selectMemoryPage(store, {\n\t\t\t\tlimit,\n\t\t\t\toffset,\n\t\t\t\tproject,\n\t\t\t\tscope,\n\t\t\t\tmatcher: (item) => !isSummaryLikeMemory(item),\n\t\t\t});\n\t\t\tconst hasMore = items.length > limit;\n\t\t\tconst result = hasMore ? items.slice(0, limit) : items;\n\t\t\tconst asRecords = result as unknown as Record<string, unknown>[];\n\t\t\tattachSessionFields(store, asRecords);\n\t\t\treturn c.json({\n\t\t\t\titems: asRecords,\n\t\t\t\tpagination: {\n\t\t\t\t\tlimit,\n\t\t\t\t\toffset,\n\t\t\t\t\tnext_offset: hasMore ? offset + result.length : null,\n\t\t\t\t\thas_more: hasMore,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t});\n\n\t// GET /api/summaries\n\tapp.get(\"/api/summaries\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst limit = Math.max(1, queryInt(c.req.query(\"limit\"), 50));\n\t\t\tconst offset = Math.max(0, queryInt(c.req.query(\"offset\"), 0));\n\t\t\tconst project = c.req.query(\"project\") || undefined;\n\t\t\tconst scope = normalizeScope(c.req.query(\"scope\"));\n\t\t\tconst items = selectMemoryPage(store, {\n\t\t\t\tlimit,\n\t\t\t\toffset,\n\t\t\t\tproject,\n\t\t\t\tscope,\n\t\t\t\tmatcher: (item) => isSummaryLikeMemory(item),\n\t\t\t});\n\t\t\tconst hasMore = items.length > limit;\n\t\t\tconst result = hasMore ? items.slice(0, limit) : items;\n\t\t\tconst asRecords = result as unknown as Record<string, unknown>[];\n\t\t\tattachSessionFields(store, asRecords);\n\t\t\treturn c.json({\n\t\t\t\titems: asRecords,\n\t\t\t\tpagination: {\n\t\t\t\t\tlimit,\n\t\t\t\t\toffset,\n\t\t\t\t\tnext_offset: hasMore ? offset + result.length : null,\n\t\t\t\t\thas_more: hasMore,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t});\n\n\t// GET /api/session (aggregate counts)\n\tapp.get(\"/api/session\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst project = c.req.query(\"project\") || null;\n\t\t\tconst count = (sql: string, ...params: unknown[]): number => {\n\t\t\t\tconst row = store.db.prepare(sql).get(...params) as Record<string, unknown> | undefined;\n\t\t\t\treturn Number(row?.total ?? 0);\n\t\t\t};\n\n\t\t\tlet prompts: number;\n\t\t\tlet artifacts: number;\n\t\t\tlet memories: number;\n\t\t\tlet observations: number;\n\t\t\tconst countObservations = (scopeProject?: string) => {\n\t\t\t\tlet offset = 0;\n\t\t\t\tlet total = 0;\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst page = queryMemoryPage(store, {\n\t\t\t\t\t\tlimit: 200,\n\t\t\t\t\t\toffset,\n\t\t\t\t\t\tproject: scopeProject,\n\t\t\t\t\t});\n\t\t\t\t\tif (page.length === 0) break;\n\t\t\t\t\ttotal += page.filter((item) => !isSummaryLikeMemory(item)).length;\n\t\t\t\t\tif (page.length < 200) break;\n\t\t\t\t\toffset += page.length;\n\t\t\t\t}\n\t\t\t\treturn total;\n\t\t\t};\n\t\t\tif (project) {\n\t\t\t\tprompts = count(\"SELECT COUNT(*) AS total FROM user_prompts WHERE project = ?\", project);\n\t\t\t\tartifacts = count(\n\t\t\t\t\t`SELECT COUNT(*) AS total FROM artifacts\n\t\t\t\t\t JOIN sessions ON sessions.id = artifacts.session_id\n\t\t\t\t\t WHERE sessions.project = ?`,\n\t\t\t\t\tproject,\n\t\t\t\t);\n\t\t\t\tmemories = count(\n\t\t\t\t\t`SELECT COUNT(*) AS total FROM memory_items\n\t\t\t\t\t JOIN sessions ON sessions.id = memory_items.session_id\n\t\t\t\t\t WHERE sessions.project = ?`,\n\t\t\t\t\tproject,\n\t\t\t\t);\n\t\t\t\tobservations = countObservations(project);\n\t\t\t} else {\n\t\t\t\tprompts = count(\"SELECT COUNT(*) AS total FROM user_prompts\");\n\t\t\t\tartifacts = count(\"SELECT COUNT(*) AS total FROM artifacts\");\n\t\t\t\tmemories = count(\"SELECT COUNT(*) AS total FROM memory_items\");\n\t\t\t\tobservations = countObservations();\n\t\t\t}\n\t\t\tconst total = prompts + artifacts + memories;\n\t\t\treturn c.json({ total, memories, artifacts, prompts, observations });\n\t\t}\n\t});\n\n\t// GET /api/pack\n\tapp.get(\"/api/pack\", async (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst context = c.req.query(\"context\") || \"\";\n\t\t\tif (!context) {\n\t\t\t\treturn c.json({ error: \"context required\" }, 400);\n\t\t\t}\n\t\t\tconst limit = queryInt(c.req.query(\"limit\"), 10);\n\t\t\tconst tokenBudgetStr = c.req.query(\"token_budget\");\n\t\t\tlet tokenBudget: number | undefined;\n\t\t\tif (tokenBudgetStr) {\n\t\t\t\ttokenBudget = parseStrictInteger(tokenBudgetStr) ?? undefined;\n\t\t\t\tif (tokenBudget === undefined) {\n\t\t\t\t\treturn c.json({ error: \"token_budget must be int\" }, 400);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst project = c.req.query(\"project\") || undefined;\n\t\t\tconst filters: { project?: string } = {};\n\t\t\tif (project) filters.project = project;\n\t\t\tconst pack = await store.buildMemoryPackAsync(context, limit, tokenBudget ?? null, filters);\n\t\t\treturn c.json(pack);\n\t\t}\n\t});\n\n\t// GET /api/memory\n\tapp.get(\"/api/memory\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst limit = queryInt(c.req.query(\"limit\"), 20);\n\t\t\tconst kind = c.req.query(\"kind\") || undefined;\n\t\t\tconst project = c.req.query(\"project\") || undefined;\n\t\t\tconst filters: Record<string, unknown> = {};\n\t\t\tif (kind) filters.kind = kind;\n\t\t\tif (project) filters.project = project;\n\t\t\tconst items = store.recent(limit, filters);\n\t\t\tconst asRecords = items as unknown as Record<string, unknown>[];\n\t\t\tattachSessionFields(store, asRecords);\n\t\t\treturn c.json({ items: asRecords });\n\t\t}\n\t});\n\n\t// GET /api/artifacts\n\tapp.get(\"/api/artifacts\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst sessionIdStr = c.req.query(\"session_id\");\n\t\t\tif (!sessionIdStr) {\n\t\t\t\treturn c.json({ error: \"session_id required\" }, 400);\n\t\t\t}\n\t\t\tconst sessionId = parseStrictInteger(sessionIdStr);\n\t\t\tif (sessionId == null) {\n\t\t\t\treturn c.json({ error: \"session_id must be int\" }, 400);\n\t\t\t}\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst rows = d\n\t\t\t\t.select()\n\t\t\t\t.from(schema.artifacts)\n\t\t\t\t.where(eq(schema.artifacts.session_id, sessionId))\n\t\t\t\t.all();\n\t\t\treturn c.json({ items: rows });\n\t\t}\n\t});\n\n\t// POST /api/memories/visibility\n\tapp.post(\"/api/memories/visibility\", async (c) => {\n\t\tconst store = getStore();\n\t\tlet body: Record<string, unknown>;\n\t\ttry {\n\t\t\tbody = await c.req.json<Record<string, unknown>>();\n\t\t} catch {\n\t\t\treturn c.json({ error: \"invalid JSON\" }, 400);\n\t\t}\n\t\tconst memoryId = parseStrictInteger(\n\t\t\ttypeof body.memory_id === \"string\" ? body.memory_id : String(body.memory_id ?? \"\"),\n\t\t);\n\t\tif (memoryId == null || memoryId <= 0) {\n\t\t\treturn c.json({ error: \"memory_id must be int\" }, 400);\n\t\t}\n\t\tconst visibility = String(body.visibility ?? \"\").trim();\n\t\tif (visibility !== \"private\" && visibility !== \"shared\") {\n\t\t\treturn c.json({ error: \"visibility must be private or shared\" }, 400);\n\t\t}\n\t\ttry {\n\t\t\tconst item = store.updateMemoryVisibility(memoryId, visibility);\n\t\t\treturn c.json({ item });\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tif (msg.includes(\"not found\")) return c.json({ error: msg }, 404);\n\t\t\tif (msg.includes(\"not owned\")) return c.json({ error: msg }, 403);\n\t\t\treturn c.json({ error: msg }, 400);\n\t\t}\n\t});\n\n\treturn app;\n}\n","/**\n * Observer status route — GET /api/observer-status.\n *\n * Ports Python's viewer_routes/observer_status.py.\n * Returns observer runtime info, credential availability, and queue status.\n */\n\nimport type { ObserverClient } from \"@codemem/core\";\nimport { type MemoryStore, probeAvailableCredentials, type RawEventSweeper } from \"@codemem/core\";\nimport { Hono } from \"hono\";\n\ntype StoreFactory = () => MemoryStore;\n\nexport interface ObserverStatusDeps {\n\tgetStore: StoreFactory;\n\tgetSweeper: () => RawEventSweeper | null;\n\tgetObserver?: () => ObserverClient | null;\n}\n\nfunction normalizeActiveObserver(active: ReturnType<ObserverClient[\"getStatus\"]> | null) {\n\tif (!active) return null;\n\treturn {\n\t\t...active,\n\t\tauth: {\n\t\t\t...active.auth,\n\t\t\tmethod: active.auth.type,\n\t\t\ttoken_present: active.auth.hasToken,\n\t\t},\n\t};\n}\n\nfunction buildFailureImpact(\n\tlatestFailure: Record<string, unknown> | null,\n\tqueueTotals: { pending: number; sessions: number },\n\tauthBackoff: { active: boolean; remainingS: number },\n): string | null {\n\tif (!latestFailure) return null;\n\tif (authBackoff.active) {\n\t\treturn `Queue retries paused for ~${authBackoff.remainingS}s after an observer auth failure.`;\n\t}\n\tif (queueTotals.pending > 0) {\n\t\treturn `${queueTotals.pending} queued raw events across ${queueTotals.sessions} session(s) are waiting on a successful flush.`;\n\t}\n\treturn \"Failed flush batches are pending retry.\";\n}\n\nexport function observerStatusRoutes(deps?: ObserverStatusDeps) {\n\tconst app = new Hono();\n\n\tapp.get(\"/api/observer-status\", (c) => {\n\t\tconst store = deps?.getStore();\n\t\tconst sweeper = deps?.getSweeper();\n\t\tconst observer = deps?.getObserver?.() ?? null;\n\n\t\t// Stub fallback when store doesn't have the required methods (e.g. tests with mock store)\n\t\tif (!store || typeof store.rawEventBacklogTotals !== \"function\") {\n\t\t\treturn c.json({\n\t\t\t\tactive: null,\n\t\t\t\tavailable_credentials: {},\n\t\t\t\tlatest_failure: null,\n\t\t\t\tqueue: {\n\t\t\t\t\tpending: 0,\n\t\t\t\t\tsessions: 0,\n\t\t\t\t\tauth_backoff_active: false,\n\t\t\t\t\tauth_backoff_remaining_s: 0,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\tconst queueTotals = store.rawEventBacklogTotals();\n\t\tconst authBackoff = sweeper?.authBackoffStatus() ?? { active: false, remainingS: 0 };\n\t\tconst latestFailure = store.latestRawEventFlushFailure();\n\t\tconst active = normalizeActiveObserver(observer?.getStatus() ?? null);\n\t\tconst availableCredentials = probeAvailableCredentials();\n\t\tconst shouldShowFailure =\n\t\t\tlatestFailure != null && (authBackoff.active || queueTotals.pending > 0);\n\n\t\tconst failureWithImpact =\n\t\t\tshouldShowFailure && latestFailure\n\t\t\t\t? { ...latestFailure, impact: buildFailureImpact(latestFailure, queueTotals, authBackoff) }\n\t\t\t\t: null;\n\n\t\treturn c.json({\n\t\t\tactive,\n\t\t\tavailable_credentials: availableCredentials,\n\t\t\tlatest_failure: failureWithImpact,\n\t\t\tqueue: {\n\t\t\t\t...queueTotals,\n\t\t\t\tauth_backoff_active: authBackoff.active,\n\t\t\t\tauth_backoff_remaining_s: authBackoff.remainingS,\n\t\t\t},\n\t\t});\n\t});\n\n\treturn app;\n}\n","/**\n * Raw events routes — GET & POST /api/raw-events, GET /api/raw-events/status,\n * POST /api/claude-hooks.\n */\n\nimport { createHash } from \"node:crypto\";\nimport type { MemoryStore, RawEventSweeper } from \"@codemem/core\";\nimport { buildRawEventEnvelopeFromHook, schema, stripPrivateObj } from \"@codemem/core\";\nimport { desc } from \"drizzle-orm\";\nimport { drizzle } from \"drizzle-orm/better-sqlite3\";\nimport { Hono } from \"hono\";\nimport { queryInt } from \"../helpers.js\";\n\ntype StoreFactory = () => MemoryStore;\n\nconst MAX_RAW_EVENTS_BODY_BYTES =\n\tNumber.parseInt(process.env.CODEMEM_RAW_EVENTS_MAX_BODY_BYTES ?? \"\", 10) || 1048576;\n\n/** Keys to check (in priority order) when resolving a session stream id. */\nconst SESSION_ID_KEYS = [\n\t\"session_stream_id\",\n\t\"session_id\",\n\t\"stream_id\",\n\t\"opencode_session_id\",\n] as const;\n\n/**\n * Resolve a session stream id from a payload object.\n * Checks multiple field aliases. Throws on conflicting values.\n */\nfunction resolveSessionStreamId(payload: Record<string, unknown>): string | null {\n\tconst values = new Map<string, string>();\n\tfor (const key of SESSION_ID_KEYS) {\n\t\tconst value = payload[key];\n\t\tif (value == null) continue;\n\t\tif (typeof value !== \"string\") throw new Error(`${key} must be string`);\n\t\tconst text = value.trim();\n\t\tif (text) values.set(key, text);\n\t}\n\tif (values.size === 0) return null;\n\tconst unique = new Set(values.values());\n\tif (unique.size > 1) throw new Error(\"conflicting session id fields\");\n\t// Return the first matching key's value (preserves priority order)\n\tfor (const key of SESSION_ID_KEYS) {\n\t\tconst v = values.get(key);\n\t\tif (v) return v;\n\t}\n\treturn null;\n}\n\n/**\n * Parse and validate a JSON object body, enforcing size limits.\n * Returns the parsed payload or a Hono Response on error.\n */\nasync function parseJsonObjectBody(\n\tc: {\n\t\treq: { header: (name: string) => string | undefined; text: () => Promise<string> };\n\t\tjson: (data: unknown, status?: number) => Response;\n\t},\n\tmaxBytes: number,\n): Promise<Record<string, unknown> | Response> {\n\tconst contentLength = Number.parseInt(c.req.header(\"content-length\") ?? \"0\", 10);\n\tif (Number.isNaN(contentLength) || contentLength < 0) {\n\t\treturn c.json({ error: \"invalid content-length\" }, 400);\n\t}\n\tif (contentLength > maxBytes) {\n\t\treturn c.json({ error: \"payload too large\", max_bytes: maxBytes }, 413);\n\t}\n\tlet raw: string;\n\ttry {\n\t\traw = await c.req.text();\n\t} catch {\n\t\treturn c.json({ error: \"invalid json\" }, 400);\n\t}\n\tif (Buffer.byteLength(raw, \"utf-8\") > maxBytes) {\n\t\treturn c.json({ error: \"payload too large\", max_bytes: maxBytes }, 413);\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = raw ? JSON.parse(raw) : {};\n\t} catch {\n\t\treturn c.json({ error: \"invalid json\" }, 400);\n\t}\n\tif (parsed == null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n\t\treturn c.json({ error: \"payload must be an object\" }, 400);\n\t}\n\treturn parsed as Record<string, unknown>;\n}\n\n/** Nudge the sweeper safely — never crashes the caller. */\nfunction nudgeSweeper(\n\tsweeper: RawEventSweeper | null | undefined,\n\tsessionIds: Iterable<string>,\n\tsource = \"opencode\",\n): void {\n\ttry {\n\t\tfor (const sessionId of sessionIds) {\n\t\t\tsweeper?.nudge(sessionId, source);\n\t\t}\n\t} catch {\n\t\t// never crash the request if sweeper nudge fails\n\t}\n}\n\nexport function rawEventsRoutes(getStore: StoreFactory, sweeper?: RawEventSweeper | null) {\n\tconst app = new Hono();\n\n\t// GET /api/raw-events (compat endpoint for stats panel)\n\tapp.get(\"/api/raw-events\", (c) => {\n\t\tconst store = getStore();\n\t\tconst totals = store.rawEventBacklogTotals();\n\t\treturn c.json(totals);\n\t});\n\n\t// GET /api/raw-events/status\n\tapp.get(\"/api/raw-events/status\", (c) => {\n\t\tconst store = getStore();\n\t\tconst limit = queryInt(c.req.query(\"limit\"), 25);\n\t\tconst d = drizzle(store.db, { schema });\n\t\tconst rows = d\n\t\t\t.select({\n\t\t\t\tsource: schema.rawEventSessions.source,\n\t\t\t\tstream_id: schema.rawEventSessions.stream_id,\n\t\t\t\topencode_session_id: schema.rawEventSessions.opencode_session_id,\n\t\t\t\tcwd: schema.rawEventSessions.cwd,\n\t\t\t\tproject: schema.rawEventSessions.project,\n\t\t\t\tstarted_at: schema.rawEventSessions.started_at,\n\t\t\t\tlast_seen_ts_wall_ms: schema.rawEventSessions.last_seen_ts_wall_ms,\n\t\t\t\tlast_received_event_seq: schema.rawEventSessions.last_received_event_seq,\n\t\t\t\tlast_flushed_event_seq: schema.rawEventSessions.last_flushed_event_seq,\n\t\t\t\tupdated_at: schema.rawEventSessions.updated_at,\n\t\t\t})\n\t\t\t.from(schema.rawEventSessions)\n\t\t\t.orderBy(desc(schema.rawEventSessions.updated_at))\n\t\t\t.limit(limit)\n\t\t\t.all();\n\t\tconst items = rows.map((row) => {\n\t\t\tconst streamId = String(row.stream_id ?? row.opencode_session_id ?? \"\");\n\t\t\treturn {\n\t\t\t\t...row,\n\t\t\t\tsession_stream_id: streamId,\n\t\t\t\tsession_id: streamId,\n\t\t\t};\n\t\t});\n\t\tconst totals = store.rawEventBacklogTotals();\n\t\treturn c.json({\n\t\t\titems,\n\t\t\ttotals,\n\t\t\tingest: {\n\t\t\t\tavailable: true,\n\t\t\t\tmode: \"stream_queue\",\n\t\t\t\tmax_body_bytes: MAX_RAW_EVENTS_BODY_BYTES,\n\t\t\t},\n\t\t});\n\t});\n\n\t// POST /api/raw-events — ingest raw events from plugin\n\tapp.post(\"/api/raw-events\", async (c) => {\n\t\tconst result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);\n\t\tif (result instanceof Response) return result;\n\t\tconst payload = result;\n\n\t\tconst store = getStore();\n\t\ttry {\n\t\t\t// Validate top-level string fields\n\t\t\tconst cwd = payload.cwd;\n\t\t\tif (cwd != null && typeof cwd !== \"string\") {\n\t\t\t\treturn c.json({ error: \"cwd must be string\" }, 400);\n\t\t\t}\n\t\t\tconst project = payload.project;\n\t\t\tif (project != null && typeof project !== \"string\") {\n\t\t\t\treturn c.json({ error: \"project must be string\" }, 400);\n\t\t\t}\n\t\t\tconst startedAt = payload.started_at;\n\t\t\tif (startedAt != null && typeof startedAt !== \"string\") {\n\t\t\t\treturn c.json({ error: \"started_at must be string\" }, 400);\n\t\t\t}\n\n\t\t\t// Determine event list: batch (events array) or single-event payload\n\t\t\tlet items = payload.events;\n\t\t\tif (items == null) {\n\t\t\t\titems = [payload];\n\t\t\t}\n\t\t\tif (!Array.isArray(items)) {\n\t\t\t\treturn c.json({ error: \"events must be a list\" }, 400);\n\t\t\t}\n\n\t\t\t// Resolve default session id from top-level payload\n\t\t\tlet defaultSessionId: string;\n\t\t\ttry {\n\t\t\t\tdefaultSessionId = resolveSessionStreamId(payload) ?? \"\";\n\t\t\t} catch (err) {\n\t\t\t\treturn c.json({ error: (err as Error).message }, 400);\n\t\t\t}\n\t\t\tif (defaultSessionId.startsWith(\"msg_\")) {\n\t\t\t\treturn c.json({ error: \"invalid session id\" }, 400);\n\t\t\t}\n\n\t\t\tlet inserted = 0;\n\t\t\tconst lastSeenBySession = new Map<string, number>();\n\t\t\tconst metaBySession = new Map<string, Record<string, string>>();\n\t\t\tconst sessionIds = new Set<string>();\n\t\t\tconst batchBySession = new Map<string, Record<string, unknown>[]>();\n\n\t\t\tfor (const item of items) {\n\t\t\t\tif (item == null || typeof item !== \"object\" || Array.isArray(item)) {\n\t\t\t\t\treturn c.json({ error: \"event must be an object\" }, 400);\n\t\t\t\t}\n\t\t\t\tconst itemObj = item as Record<string, unknown>;\n\n\t\t\t\tlet itemSessionId: string | null;\n\t\t\t\ttry {\n\t\t\t\t\titemSessionId = resolveSessionStreamId(itemObj);\n\t\t\t\t} catch (err) {\n\t\t\t\t\treturn c.json({ error: (err as Error).message }, 400);\n\t\t\t\t}\n\t\t\t\tconst opencodeSessionId = String(itemSessionId ?? defaultSessionId ?? \"\");\n\t\t\t\tif (!opencodeSessionId) {\n\t\t\t\t\treturn c.json({ error: \"session id required\" }, 400);\n\t\t\t\t}\n\t\t\t\tif (opencodeSessionId.startsWith(\"msg_\")) {\n\t\t\t\t\treturn c.json({ error: \"invalid session id\" }, 400);\n\t\t\t\t}\n\n\t\t\t\tlet eventId = String(itemObj.event_id ?? \"\");\n\t\t\t\tconst eventType = String(itemObj.event_type ?? \"\");\n\t\t\t\tif (!eventType) {\n\t\t\t\t\treturn c.json({ error: \"event_type required\" }, 400);\n\t\t\t\t}\n\n\t\t\t\tconst eventSeqValue = itemObj.event_seq;\n\t\t\t\tif (eventSeqValue != null) {\n\t\t\t\t\tconst parsed = Number(eventSeqValue);\n\t\t\t\t\tif (!Number.isFinite(parsed) || parsed !== Math.floor(parsed)) {\n\t\t\t\t\t\treturn c.json({ error: \"event_seq must be int\" }, 400);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlet tsWallMs = itemObj.ts_wall_ms;\n\t\t\t\tif (tsWallMs != null) {\n\t\t\t\t\tconst parsed = Number(tsWallMs);\n\t\t\t\t\tif (!Number.isFinite(parsed)) {\n\t\t\t\t\t\treturn c.json({ error: \"ts_wall_ms must be int\" }, 400);\n\t\t\t\t\t}\n\t\t\t\t\ttsWallMs = Math.floor(parsed);\n\t\t\t\t\tconst prev = lastSeenBySession.get(opencodeSessionId) ?? (tsWallMs as number);\n\t\t\t\t\tlastSeenBySession.set(opencodeSessionId, Math.max(prev, tsWallMs as number));\n\t\t\t\t}\n\n\t\t\t\tlet tsMonoMs = itemObj.ts_mono_ms;\n\t\t\t\tif (tsMonoMs != null) {\n\t\t\t\t\tconst parsed = Number(tsMonoMs);\n\t\t\t\t\tif (!Number.isFinite(parsed)) {\n\t\t\t\t\t\treturn c.json({ error: \"ts_mono_ms must be number\" }, 400);\n\t\t\t\t\t}\n\t\t\t\t\ttsMonoMs = parsed;\n\t\t\t\t}\n\n\t\t\t\tlet eventPayload = itemObj.payload;\n\t\t\t\tif (eventPayload == null) eventPayload = {};\n\t\t\t\tif (typeof eventPayload !== \"object\" || Array.isArray(eventPayload)) {\n\t\t\t\t\treturn c.json({ error: \"payload must be an object\" }, 400);\n\t\t\t\t}\n\n\t\t\t\t// Per-item meta fields\n\t\t\t\tconst itemCwd = itemObj.cwd;\n\t\t\t\tif (itemCwd != null && typeof itemCwd !== \"string\") {\n\t\t\t\t\treturn c.json({ error: \"cwd must be string\" }, 400);\n\t\t\t\t}\n\t\t\t\tconst itemProject = itemObj.project;\n\t\t\t\tif (itemProject != null && typeof itemProject !== \"string\") {\n\t\t\t\t\treturn c.json({ error: \"project must be string\" }, 400);\n\t\t\t\t}\n\t\t\t\tconst itemStartedAt = itemObj.started_at;\n\t\t\t\tif (itemStartedAt != null && typeof itemStartedAt !== \"string\") {\n\t\t\t\t\treturn c.json({ error: \"started_at must be string\" }, 400);\n\t\t\t\t}\n\n\t\t\t\t// Sanitize payload\n\t\t\t\teventPayload = stripPrivateObj(eventPayload) as Record<string, unknown>;\n\n\t\t\t\t// Generate stable event_id for legacy senders.\n\t\t\t\t// Python uses json.dumps(sort_keys=True) which recursively sorts all keys.\n\t\t\t\t// We replicate with a recursive key-sorting replacer.\n\t\t\t\tif (!eventId) {\n\t\t\t\t\tconst sortedStringify = (obj: unknown): string =>\n\t\t\t\t\t\tJSON.stringify(obj, (_key, value) => {\n\t\t\t\t\t\t\tif (value != null && typeof value === \"object\" && !Array.isArray(value)) {\n\t\t\t\t\t\t\t\tconst sorted: Record<string, unknown> = {};\n\t\t\t\t\t\t\t\tfor (const k of Object.keys(value as Record<string, unknown>).sort()) {\n\t\t\t\t\t\t\t\t\tsorted[k] = (value as Record<string, unknown>)[k];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn sorted;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t});\n\t\t\t\t\tif (eventSeqValue != null) {\n\t\t\t\t\t\tconst rawId = sortedStringify({\n\t\t\t\t\t\t\ts: eventSeqValue,\n\t\t\t\t\t\t\tt: eventType,\n\t\t\t\t\t\t\tp: eventPayload,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tconst hash = createHash(\"sha256\").update(rawId, \"utf-8\").digest(\"hex\").slice(0, 16);\n\t\t\t\t\t\teventId = `legacy-seq-${eventSeqValue}-${hash}`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst rawId = sortedStringify({\n\t\t\t\t\t\t\tm: tsMonoMs ?? null,\n\t\t\t\t\t\t\tp: eventPayload,\n\t\t\t\t\t\t\tt: eventType,\n\t\t\t\t\t\t\tw: tsWallMs ?? null,\n\t\t\t\t\t\t});\n\t\t\t\t\t\teventId = `legacy-${createHash(\"sha256\").update(rawId, \"utf-8\").digest(\"hex\").slice(0, 16)}`;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst eventEntry: Record<string, unknown> = {\n\t\t\t\t\tevent_id: eventId,\n\t\t\t\t\tevent_type: eventType,\n\t\t\t\t\tpayload: eventPayload,\n\t\t\t\t\tts_wall_ms: tsWallMs ?? null,\n\t\t\t\t\tts_mono_ms: tsMonoMs ?? null,\n\t\t\t\t};\n\n\t\t\t\tsessionIds.add(opencodeSessionId);\n\t\t\t\tconst list = batchBySession.get(opencodeSessionId) ?? [];\n\t\t\t\tlist.push({ ...eventEntry });\n\t\t\t\tbatchBySession.set(opencodeSessionId, list);\n\n\t\t\t\tif (itemCwd || itemProject || itemStartedAt) {\n\t\t\t\t\tconst perSession = metaBySession.get(opencodeSessionId) ?? {};\n\t\t\t\t\tif (itemCwd) perSession.cwd = itemCwd as string;\n\t\t\t\t\tif (itemProject) perSession.project = itemProject as string;\n\t\t\t\t\tif (itemStartedAt) perSession.started_at = itemStartedAt as string;\n\t\t\t\t\tmetaBySession.set(opencodeSessionId, perSession);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Insert events\n\t\t\tif (sessionIds.size === 1) {\n\t\t\t\tconst singleSessionId = sessionIds.values().next().value as string;\n\t\t\t\tconst batch = batchBySession.get(singleSessionId) ?? [];\n\t\t\t\tconst result = store.recordRawEventsBatch(singleSessionId, batch);\n\t\t\t\tinserted = result.inserted;\n\t\t\t} else {\n\t\t\t\tfor (const [sid, sidEvents] of batchBySession) {\n\t\t\t\t\tconst result = store.recordRawEventsBatch(sid, sidEvents);\n\t\t\t\t\tinserted += result.inserted;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Update session metadata\n\t\t\tfor (const metaSessionId of sessionIds) {\n\t\t\t\tconst sessionMeta = metaBySession.get(metaSessionId) ?? {};\n\t\t\t\tconst applyRequestMeta = sessionIds.size === 1 || metaSessionId === defaultSessionId;\n\t\t\t\tstore.updateRawEventSessionMeta({\n\t\t\t\t\topencodeSessionId: metaSessionId,\n\t\t\t\t\tcwd:\n\t\t\t\t\t\tsessionMeta.cwd ?? (applyRequestMeta ? (cwd as string | undefined) : undefined) ?? null,\n\t\t\t\t\tproject:\n\t\t\t\t\t\tsessionMeta.project ??\n\t\t\t\t\t\t(applyRequestMeta ? (project as string | undefined) : undefined) ??\n\t\t\t\t\t\tnull,\n\t\t\t\t\tstartedAt:\n\t\t\t\t\t\tsessionMeta.started_at ??\n\t\t\t\t\t\t(applyRequestMeta ? (startedAt as string | undefined) : undefined) ??\n\t\t\t\t\t\tnull,\n\t\t\t\t\tlastSeenTsWallMs: lastSeenBySession.get(metaSessionId) ?? null,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Note per-session activity for optional debounced auto-flush.\n\t\t\tnudgeSweeper(sweeper, sessionIds);\n\n\t\t\treturn c.json({ inserted, received: (items as unknown[]).length });\n\t\t} catch (err) {\n\t\t\tconst response: Record<string, unknown> = { error: \"internal server error\" };\n\t\t\tif (process.env.CODEMEM_VIEWER_DEBUG === \"1\") {\n\t\t\t\tresponse.detail = (err as Error).message;\n\t\t\t}\n\t\t\treturn c.json(response, 500);\n\t\t}\n\t});\n\n\t// POST /api/claude-hooks — ingest Claude Code hook events\n\tapp.post(\"/api/claude-hooks\", async (c) => {\n\t\tconst result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);\n\t\tif (result instanceof Response) return result;\n\t\tconst payload = result;\n\n\t\t// Map hook payload → raw event envelope\n\t\tconst envelope = buildRawEventEnvelopeFromHook(payload);\n\t\tif (envelope === null) {\n\t\t\t// Unsupported event type or missing required fields — skip gracefully\n\t\t\treturn c.json({ inserted: 0, skipped: 1 });\n\t\t}\n\n\t\tconst store = getStore();\n\t\ttry {\n\t\t\tconst opencodeSessionId = envelope.opencode_session_id;\n\t\t\tconst source = envelope.source;\n\t\t\tconst strippedPayload = stripPrivateObj(envelope.payload) as Record<string, unknown>;\n\n\t\t\tconst inserted = store.recordRawEvent({\n\t\t\t\topencodeSessionId,\n\t\t\t\tsource,\n\t\t\t\teventId: envelope.event_id,\n\t\t\t\teventType: \"claude.hook\",\n\t\t\t\tpayload: strippedPayload,\n\t\t\t\ttsWallMs: envelope.ts_wall_ms,\n\t\t\t});\n\n\t\t\tstore.updateRawEventSessionMeta({\n\t\t\t\topencodeSessionId,\n\t\t\t\tsource,\n\t\t\t\tcwd: envelope.cwd,\n\t\t\t\tproject: envelope.project,\n\t\t\t\tstartedAt: envelope.started_at,\n\t\t\t\tlastSeenTsWallMs: envelope.ts_wall_ms,\n\t\t\t});\n\n\t\t\t// Note activity for optional debounced auto-flush.\n\t\t\tnudgeSweeper(sweeper, [opencodeSessionId], source);\n\n\t\t\treturn c.json({ inserted: inserted ? 1 : 0, skipped: 0 });\n\t\t} catch (err) {\n\t\t\tconst response: Record<string, unknown> = { error: \"internal server error\" };\n\t\t\tif (process.env.CODEMEM_VIEWER_DEBUG === \"1\") {\n\t\t\t\tresponse.detail = (err as Error).message;\n\t\t\t}\n\t\t\treturn c.json(response, 500);\n\t\t}\n\t});\n\n\treturn app;\n}\n","/**\n * Stats routes — GET /api/stats, GET /api/usage.\n *\n * Ports Python's viewer_routes/stats.py.\n */\n\nimport type { MemoryStore } from \"@codemem/core\";\nimport { Hono } from \"hono\";\n\n/**\n * Create stats routes. The store factory is called per-request to get a\n * fresh connection (matching the Python viewer pattern).\n */\nexport function statsRoutes(getStore: () => MemoryStore) {\n\tconst app = new Hono();\n\n\tapp.get(\"/api/stats\", (c) => {\n\t\tconst store = getStore();\n\t\treturn c.json({\n\t\t\t...store.stats(),\n\t\t\tviewer_pid: process.pid,\n\t\t});\n\t});\n\n\tapp.get(\"/api/usage\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst projectFilter = c.req.query(\"project\") || null;\n\t\t\tconst eventsGlobal = store.db\n\t\t\t\t.prepare(\n\t\t\t\t\t`SELECT event,\n\t\t\t\t\t\tSUM(tokens_read) AS total_tokens_read,\n\t\t\t\t\t\tSUM(tokens_written) AS total_tokens_written,\n\t\t\t\t\t\tSUM(tokens_saved) AS total_tokens_saved,\n\t\t\t\t\t\tCOUNT(*) AS count\n\t\t\t\t\t FROM usage_events GROUP BY event ORDER BY event`,\n\t\t\t\t)\n\t\t\t\t.all() as Record<string, unknown>[];\n\t\t\tconst totalsGlobal = store.db\n\t\t\t\t.prepare(\n\t\t\t\t\t`SELECT COALESCE(SUM(tokens_read),0) AS tokens_read,\n\t\t\t\t\t\tCOALESCE(SUM(tokens_written),0) AS tokens_written,\n\t\t\t\t\t\tCOALESCE(SUM(tokens_saved),0) AS tokens_saved,\n\t\t\t\t\t\tCOUNT(*) AS count\n\t\t\t\t\t FROM usage_events`,\n\t\t\t\t)\n\t\t\t\t.get() as Record<string, unknown>;\n\t\t\tlet eventsFiltered: Record<string, unknown>[] | null = null;\n\t\t\tlet totalsFiltered: Record<string, unknown> | null = null;\n\t\t\tif (projectFilter) {\n\t\t\t\teventsFiltered = store.db\n\t\t\t\t\t.prepare(\n\t\t\t\t\t\t`SELECT event,\n\t\t\t\t\t\t\tSUM(tokens_read) AS total_tokens_read,\n\t\t\t\t\t\t\tSUM(tokens_written) AS total_tokens_written,\n\t\t\t\t\t\t\tSUM(tokens_saved) AS total_tokens_saved,\n\t\t\t\t\t\t\tCOUNT(*) AS count\n\t\t\t\t\t\t FROM usage_events\n\t\t\t\t\t\t JOIN sessions ON sessions.id = usage_events.session_id\n\t\t\t\t\t\t WHERE sessions.project = ?\n\t\t\t\t\t\t GROUP BY event ORDER BY event`,\n\t\t\t\t\t)\n\t\t\t\t\t.all(projectFilter) as Record<string, unknown>[];\n\t\t\t\ttotalsFiltered = store.db\n\t\t\t\t\t.prepare(\n\t\t\t\t\t\t`SELECT COALESCE(SUM(tokens_read),0) AS tokens_read,\n\t\t\t\t\t\t\tCOALESCE(SUM(tokens_written),0) AS tokens_written,\n\t\t\t\t\t\t\tCOALESCE(SUM(tokens_saved),0) AS tokens_saved,\n\t\t\t\t\t\t\tCOUNT(*) AS count\n\t\t\t\t\t\t FROM usage_events\n\t\t\t\t\t\t JOIN sessions ON sessions.id = usage_events.session_id\n\t\t\t\t\t\t WHERE sessions.project = ?`,\n\t\t\t\t\t)\n\t\t\t\t\t.get(projectFilter) as Record<string, unknown>;\n\t\t\t}\n\t\t\treturn c.json({\n\t\t\t\tproject: projectFilter,\n\t\t\t\tevents: projectFilter ? eventsFiltered : eventsGlobal,\n\t\t\t\ttotals: projectFilter ? totalsFiltered : totalsGlobal,\n\t\t\t\tevents_global: eventsGlobal,\n\t\t\t\ttotals_global: totalsGlobal,\n\t\t\t\tevents_filtered: eventsFiltered,\n\t\t\t\ttotals_filtered: totalsFiltered,\n\t\t\t\trecent_packs: [],\n\t\t\t});\n\t\t}\n\t});\n\n\treturn app;\n}\n","/**\n * Sync routes — status, peers, actors, attempts, pairing, mutations.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport net from \"node:net\";\nimport { dirname, join } from \"node:path\";\nimport type { MemoryStore, ReplicationOp } from \"@codemem/core\";\nimport {\n\tapplyReplicationOps,\n\tcleanupNonces,\n\tcoordinatorCreateInviteAction,\n\tcoordinatorImportInviteAction,\n\tcoordinatorReviewJoinRequestAction,\n\tcoordinatorStatusSnapshot,\n\tDEFAULT_TIME_WINDOW_S,\n\tensureDeviceIdentity,\n\textractReplicationOps,\n\tfingerprintPublicKey,\n\tlistCoordinatorJoinRequests,\n\tloadReplicationOpsSince,\n\treadCoordinatorSyncConfig,\n\trecordNonce,\n\tschema,\n\tverifySignature,\n} from \"@codemem/core\";\nimport { count, desc, eq, max, ne } from \"drizzle-orm\";\nimport { drizzle } from \"drizzle-orm/better-sqlite3\";\nimport { Hono } from \"hono\";\nimport { queryBool, queryInt, safeJsonList } from \"../helpers.js\";\n\ntype StoreFactory = () => MemoryStore;\ntype SyncRuntimeStatus = {\n\tphase: \"starting\" | \"running\" | \"stopping\" | \"error\" | \"disabled\" | null;\n\tdetail?: string | null;\n};\n\nconst SYNC_STALE_AFTER_SECONDS = 10 * 60;\nconst SYNC_PROTOCOL_VERSION = \"1\";\n\nfunction intEnvOr(name: string, fallback: number): number {\n\tconst value = Number.parseInt(process.env[name] ?? \"\", 10);\n\treturn Number.isFinite(value) ? value : fallback;\n}\n\nconst MAX_SYNC_BODY_BYTES = intEnvOr(\"CODEMEM_SYNC_MAX_BODY_BYTES\", 1_048_576);\nconst MAX_SYNC_OPS = intEnvOr(\"CODEMEM_SYNC_MAX_OPS\", 2000);\n\nconst PAIRING_FILTER_HINT =\n\t\"Run this on another device with codemem sync pair --accept '<payload>'. \" +\n\t\"On the accepting device, --include/--exclude control both what it sends and what it accepts from that peer.\";\n\nfunction pathWithQuery(url: string): string {\n\tconst parsed = new URL(url);\n\treturn parsed.search ? `${parsed.pathname}${parsed.search}` : parsed.pathname;\n}\n\nfunction unauthorizedPayload(reason: string): Record<string, string> {\n\tif (process.env.CODEMEM_SYNC_AUTH_DIAGNOSTICS === \"1\") {\n\t\treturn { error: \"unauthorized\", reason };\n\t}\n\treturn { error: \"unauthorized\" };\n}\n\nfunction authorizeSyncRequest(\n\tstore: MemoryStore,\n\trequest: { method: string; url: string; header(name: string): string | undefined },\n\tbody: Buffer,\n): { ok: boolean; reason: string; deviceId: string } {\n\tconst deviceId = (request.header(\"X-Opencode-Device\") ?? \"\").trim();\n\tconst signature = request.header(\"X-Opencode-Signature\") ?? \"\";\n\tconst timestamp = request.header(\"X-Opencode-Timestamp\") ?? \"\";\n\tconst nonce = request.header(\"X-Opencode-Nonce\") ?? \"\";\n\tif (!deviceId || !signature || !timestamp || !nonce) {\n\t\treturn { ok: false, reason: \"missing_headers\", deviceId };\n\t}\n\n\tconst peerRow = store.db\n\t\t.prepare(\n\t\t\t\"SELECT pinned_fingerprint, public_key FROM sync_peers WHERE peer_device_id = ? LIMIT 1\",\n\t\t)\n\t\t.get(deviceId) as { pinned_fingerprint: string | null; public_key: string | null } | undefined;\n\tif (!peerRow) {\n\t\treturn { ok: false, reason: \"unknown_peer\", deviceId };\n\t}\n\n\tconst pinnedFingerprint = String(peerRow.pinned_fingerprint ?? \"\").trim();\n\tconst publicKey = String(peerRow.public_key ?? \"\").trim();\n\tif (!pinnedFingerprint || !publicKey) {\n\t\treturn { ok: false, reason: \"peer_record_incomplete\", deviceId };\n\t}\n\tif (fingerprintPublicKey(publicKey) !== pinnedFingerprint) {\n\t\treturn { ok: false, reason: \"fingerprint_mismatch\", deviceId };\n\t}\n\n\tlet valid = false;\n\ttry {\n\t\tvalid = verifySignature({\n\t\t\tmethod: request.method,\n\t\t\tpathWithQuery: pathWithQuery(request.url),\n\t\t\tbodyBytes: body,\n\t\t\ttimestamp,\n\t\t\tnonce,\n\t\t\tsignature,\n\t\t\tpublicKey,\n\t\t\tdeviceId,\n\t\t});\n\t} catch {\n\t\treturn { ok: false, reason: \"signature_verification_error\", deviceId };\n\t}\n\n\tif (!valid) {\n\t\treturn { ok: false, reason: \"invalid_signature\", deviceId };\n\t}\n\n\tconst createdAt = new Date().toISOString();\n\tif (!recordNonce(store.db, deviceId, nonce, createdAt)) {\n\t\treturn { ok: false, reason: \"nonce_replay\", deviceId };\n\t}\n\n\tconst cutoff = new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1000).toISOString();\n\tcleanupNonces(store.db, cutoff);\n\treturn { ok: true, reason: \"ok\", deviceId };\n}\n\nfunction projectBasename(value: string | null | undefined): string {\n\tconst project = String(value ?? \"\")\n\t\t.trim()\n\t\t.replaceAll(\"\\\\\", \"/\");\n\tif (!project) return \"\";\n\tconst parts = project.split(\"/\").filter(Boolean);\n\treturn parts.length > 0 ? (parts[parts.length - 1] ?? \"\") : \"\";\n}\n\nfunction parseJsonList(value: unknown): string[] {\n\tif (value == null) return [];\n\tif (typeof value === \"string\") {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(value) as unknown;\n\t\t\tif (!Array.isArray(parsed)) return [];\n\t\t\treturn parsed.map((entry) => String(entry ?? \"\").trim()).filter(Boolean);\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\tif (!Array.isArray(value)) return [];\n\treturn value.map((entry) => String(entry ?? \"\").trim()).filter(Boolean);\n}\n\nfunction readPeerProjectFilters(\n\tstore: MemoryStore,\n\tpeerDeviceId: string,\n): { include: string[]; exclude: string[] } {\n\tconst globalConfig = readCoordinatorSyncConfig();\n\tconst row = store.db\n\t\t.prepare(\n\t\t\t\"SELECT projects_include_json, projects_exclude_json FROM sync_peers WHERE peer_device_id = ? LIMIT 1\",\n\t\t)\n\t\t.get(peerDeviceId) as\n\t\t| { projects_include_json: string | null; projects_exclude_json: string | null }\n\t\t| undefined;\n\tif (!row) {\n\t\treturn {\n\t\t\tinclude: globalConfig.syncProjectsInclude,\n\t\t\texclude: globalConfig.syncProjectsExclude,\n\t\t};\n\t}\n\tconst hasOverride = row.projects_include_json != null || row.projects_exclude_json != null;\n\tif (!hasOverride) {\n\t\treturn {\n\t\t\tinclude: globalConfig.syncProjectsInclude,\n\t\t\texclude: globalConfig.syncProjectsExclude,\n\t\t};\n\t}\n\treturn {\n\t\tinclude: parseJsonList(row.projects_include_json),\n\t\texclude: parseJsonList(row.projects_exclude_json),\n\t};\n}\n\nfunction peerClaimedLocalActor(store: MemoryStore, peerDeviceId: string): boolean {\n\tconst row = store.db\n\t\t.prepare(\"SELECT claimed_local_actor FROM sync_peers WHERE peer_device_id = ? LIMIT 1\")\n\t\t.get(peerDeviceId) as { claimed_local_actor: number | null } | undefined;\n\treturn Boolean(row?.claimed_local_actor);\n}\n\nfunction parseOpPayload(op: { payload_json: string | null }): Record<string, unknown> | null {\n\tif (!op.payload_json || !String(op.payload_json).trim()) return null;\n\ttry {\n\t\tconst parsed = JSON.parse(op.payload_json) as unknown;\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n\t\treturn parsed as Record<string, unknown>;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction isSharedVisibility(payload: Record<string, unknown> | null): boolean {\n\tif (!payload) return false;\n\tlet visibility = String(payload.visibility ?? \"\")\n\t\t.trim()\n\t\t.toLowerCase();\n\tconst metadata =\n\t\tpayload.metadata_json &&\n\t\ttypeof payload.metadata_json === \"object\" &&\n\t\t!Array.isArray(payload.metadata_json)\n\t\t\t? (payload.metadata_json as Record<string, unknown>)\n\t\t\t: {};\n\tconst metadataVisibility = String(metadata.visibility ?? \"\")\n\t\t.trim()\n\t\t.toLowerCase();\n\tif (!visibility && metadataVisibility) visibility = metadataVisibility;\n\tif (!visibility) {\n\t\tlet workspaceKind = String(payload.workspace_kind ?? \"\")\n\t\t\t.trim()\n\t\t\t.toLowerCase();\n\t\tlet workspaceId = String(payload.workspace_id ?? \"\")\n\t\t\t.trim()\n\t\t\t.toLowerCase();\n\t\tif (!workspaceKind)\n\t\t\tworkspaceKind = String(metadata.workspace_kind ?? \"\")\n\t\t\t\t.trim()\n\t\t\t\t.toLowerCase();\n\t\tif (!workspaceId)\n\t\t\tworkspaceId = String(metadata.workspace_id ?? \"\")\n\t\t\t\t.trim()\n\t\t\t\t.toLowerCase();\n\t\tif (workspaceKind === \"shared\" || workspaceId.startsWith(\"shared:\")) {\n\t\t\tvisibility = \"shared\";\n\t\t} else {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn visibility === \"shared\";\n}\n\nfunction projectAllowed(\n\tprojectValue: string | null,\n\tfilters: { include: string[]; exclude: string[] },\n): boolean {\n\tconst value = String(projectValue ?? \"\").trim();\n\tconst valueBase = projectBasename(value);\n\tfor (const blocked of filters.exclude) {\n\t\tif (blocked === value || blocked === valueBase) return false;\n\t}\n\tif (filters.include.length === 0) return true;\n\tfor (const allowed of filters.include) {\n\t\tif (allowed === value || allowed === valueBase) return true;\n\t}\n\treturn false;\n}\n\nfunction filterOpsForPeer(\n\tstore: MemoryStore,\n\tpeerDeviceId: string,\n\tops: ReplicationOp[],\n): { allowed: ReplicationOp[]; skipped: number } {\n\tconst filters = readPeerProjectFilters(store, peerDeviceId);\n\tconst allowPrivate = peerClaimedLocalActor(store, peerDeviceId);\n\tconst allowed: ReplicationOp[] = [];\n\tlet skipped = 0;\n\tfor (const op of ops) {\n\t\tif (op.entity_type !== \"memory_item\") {\n\t\t\tallowed.push(op);\n\t\t\tcontinue;\n\t\t}\n\t\tconst payload = parseOpPayload(op);\n\t\tif (!allowPrivate && !isSharedVisibility(payload)) {\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\tconst project = payload && typeof payload.project === \"string\" ? payload.project : null;\n\t\tif (!projectAllowed(project, filters)) {\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\tallowed.push(op);\n\t}\n\treturn { allowed, skipped };\n}\n\n// ---------------------------------------------------------------------------\n// Peer row mapping — deduplicated helper (fix #4)\n// ---------------------------------------------------------------------------\n\n/**\n * Map a raw sync_peers DB row to the API response shape.\n * When showDiag is false, sensitive fields (fingerprint, last_error, addresses)\n * are redacted.\n */\nfunction mapPeerRow(row: Record<string, unknown>, showDiag: boolean): Record<string, unknown> {\n\treturn {\n\t\tpeer_device_id: row.peer_device_id,\n\t\tname: row.name,\n\t\tfingerprint: showDiag ? row.pinned_fingerprint : null,\n\t\tpinned: Boolean(row.pinned_fingerprint),\n\t\taddresses: showDiag ? safeJsonList(row.addresses_json as string | null) : [],\n\t\tlast_seen_at: row.last_seen_at,\n\t\tlast_sync_at: row.last_sync_at,\n\t\tlast_error: showDiag ? row.last_error : null,\n\t\thas_error: Boolean(row.last_error),\n\t\tclaimed_local_actor: Boolean(row.claimed_local_actor),\n\t\tactor_id: row.actor_id ?? null,\n\t\tactor_display_name: row.actor_display_name ?? null,\n\t\tproject_scope: {\n\t\t\tinclude: safeJsonList(row.projects_include_json as string | null),\n\t\t\texclude: safeJsonList(row.projects_exclude_json as string | null),\n\t\t\teffective_include: safeJsonList(row.projects_include_json as string | null),\n\t\t\teffective_exclude: safeJsonList(row.projects_exclude_json as string | null),\n\t\t\tinherits_global: row.projects_include_json == null && row.projects_exclude_json == null,\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Peer status helpers\n// ---------------------------------------------------------------------------\n\nfunction isRecentIso(value: unknown, windowS = SYNC_STALE_AFTER_SECONDS): boolean {\n\tconst raw = String(value ?? \"\").trim();\n\tif (!raw) return false;\n\tconst normalized = raw.replace(\"Z\", \"+00:00\");\n\tconst hasOffset = /(?:Z|[+-]\\d{2}:?\\d{2})$/i.test(raw);\n\tconst ts = new Date(hasOffset ? normalized : `${normalized}+00:00`);\n\tif (Number.isNaN(ts.getTime())) return false;\n\tconst ageS = (Date.now() - ts.getTime()) / 1000;\n\treturn ageS >= 0 && ageS <= windowS;\n}\n\nfunction peerStatus(peer: Record<string, unknown>): Record<string, unknown> {\n\tconst lastSyncAt = peer.last_sync_at;\n\tconst lastPingAt = peer.last_seen_at;\n\tconst hasError = Boolean(peer.has_error);\n\n\tconst syncFresh = isRecentIso(lastSyncAt);\n\tconst pingFresh = isRecentIso(lastPingAt);\n\n\tlet peerState: string;\n\tif (hasError && !(syncFresh || pingFresh)) peerState = \"offline\";\n\telse if (hasError) peerState = \"degraded\";\n\telse if (syncFresh || pingFresh) peerState = \"online\";\n\telse if (lastSyncAt || lastPingAt) peerState = \"stale\";\n\telse peerState = \"unknown\";\n\n\tconst syncStatus = hasError ? \"error\" : syncFresh ? \"ok\" : lastSyncAt ? \"stale\" : \"unknown\";\n\tconst pingStatus = pingFresh ? \"ok\" : lastPingAt ? \"stale\" : \"unknown\";\n\n\treturn {\n\t\tsync_status: syncStatus,\n\t\tping_status: pingStatus,\n\t\tpeer_state: peerState,\n\t\tfresh: syncFresh || pingFresh,\n\t\tlast_sync_at: lastSyncAt,\n\t\tlast_ping_at: lastPingAt,\n\t};\n}\n\nfunction attemptStatus(attempt: Record<string, unknown>): string {\n\tif (attempt.ok) return \"ok\";\n\tif (attempt.error) return \"error\";\n\treturn \"unknown\";\n}\n\nfunction readViewerBinding(dbPath: string): { host: string; port: number } | null {\n\ttry {\n\t\tconst raw = readFileSync(join(dirname(dbPath), \"viewer.pid\"), \"utf8\");\n\t\tconst parsed = JSON.parse(raw) as Partial<{ host: string; port: number }>;\n\t\tif (typeof parsed.host === \"string\" && typeof parsed.port === \"number\") {\n\t\t\treturn { host: parsed.host, port: parsed.port };\n\t\t}\n\t} catch {\n\t\t// ignore missing/malformed pidfile\n\t}\n\treturn null;\n}\n\nasync function portOpen(host: string, port: number): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tconst socket = net.createConnection({ host, port });\n\t\tconst done = (ok: boolean) => {\n\t\t\tsocket.removeAllListeners();\n\t\t\tsocket.destroy();\n\t\t\tresolve(ok);\n\t\t};\n\t\tsocket.setTimeout(300);\n\t\tsocket.once(\"connect\", () => done(true));\n\t\tsocket.once(\"timeout\", () => done(false));\n\t\tsocket.once(\"error\", () => done(false));\n\t});\n}\n\nconst PEERS_QUERY = `\n\tSELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,\n\t p.last_seen_at, p.last_sync_at, p.last_error,\n\t p.projects_include_json, p.projects_exclude_json, p.claimed_local_actor,\n\t p.actor_id, a.display_name AS actor_display_name\n\tFROM sync_peers AS p\n\tLEFT JOIN actors AS a ON a.actor_id = p.actor_id\n\tORDER BY name, peer_device_id\n`;\n\n// ---------------------------------------------------------------------------\n// Route factory\n// ---------------------------------------------------------------------------\n\n/**\n * Peer-to-peer sync protocol routes (/v1/*).\n *\n * These are mounted on the sync listener (0.0.0.0:7337) and are\n * network-accessible. All requests are auth-gated via signature\n * verification so unauthenticated callers are rejected.\n */\nexport function syncProtocolRoutes(getStore: StoreFactory) {\n\tconst app = new Hono();\n\n\t// GET /v1/status (peer sync protocol)\n\tapp.get(\"/v1/status\", (c) => {\n\t\tconst store = getStore();\n\t\tconst auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));\n\t\tif (!auth.ok) return c.json(unauthorizedPayload(auth.reason), 401);\n\n\t\ttry {\n\t\t\tlet device = store.db\n\t\t\t\t.prepare(\"SELECT device_id, fingerprint FROM sync_device LIMIT 1\")\n\t\t\t\t.get() as { device_id: string; fingerprint: string } | undefined;\n\t\t\tif (!device) {\n\t\t\t\tconst [deviceId, fingerprint] = ensureDeviceIdentity(store.db);\n\t\t\t\tdevice = { device_id: deviceId, fingerprint };\n\t\t\t}\n\t\t\treturn c.json({\n\t\t\t\tdevice_id: device.device_id,\n\t\t\t\tprotocol_version: SYNC_PROTOCOL_VERSION,\n\t\t\t\tfingerprint: device.fingerprint,\n\t\t\t});\n\t\t} catch {\n\t\t\treturn c.json({ error: \"internal_error\" }, 500);\n\t\t}\n\t});\n\n\t// GET /v1/ops (peer sync protocol)\n\tapp.get(\"/v1/ops\", (c) => {\n\t\tconst store = getStore();\n\t\tconst auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));\n\t\tif (!auth.ok) return c.json(unauthorizedPayload(auth.reason), 401);\n\t\tconst peerDeviceId = auth.deviceId;\n\n\t\ttry {\n\t\t\tconst since = c.req.query(\"since\") ?? null;\n\t\t\tconst rawLimit = Number.parseInt(c.req.query(\"limit\") ?? \"200\", 10);\n\t\t\tconst limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 1000)) : 200;\n\t\t\tlet localDeviceId = store.db.prepare(\"SELECT device_id FROM sync_device LIMIT 1\").get() as\n\t\t\t\t| { device_id: string }\n\t\t\t\t| undefined;\n\t\t\tif (!localDeviceId) {\n\t\t\t\tconst [deviceId] = ensureDeviceIdentity(store.db);\n\t\t\t\tlocalDeviceId = { device_id: deviceId };\n\t\t\t}\n\t\t\tconst [ops, nextCursor] = loadReplicationOpsSince(\n\t\t\t\tstore.db,\n\t\t\t\tsince,\n\t\t\t\tlimit,\n\t\t\t\tlocalDeviceId.device_id,\n\t\t\t);\n\t\t\tconst filtered = filterOpsForPeer(store, peerDeviceId, ops);\n\t\t\treturn c.json({\n\t\t\t\tops: filtered.allowed,\n\t\t\t\tnext_cursor: nextCursor,\n\t\t\t\tskipped: filtered.skipped,\n\t\t\t});\n\t\t} catch {\n\t\t\treturn c.json({ error: \"internal_error\" }, 500);\n\t\t}\n\t});\n\n\t// POST /v1/ops (peer sync protocol)\n\tapp.post(\"/v1/ops\", async (c) => {\n\t\tconst store = getStore();\n\t\tconst raw = Buffer.from(await c.req.arrayBuffer());\n\t\tif (raw.length > MAX_SYNC_BODY_BYTES) {\n\t\t\treturn c.json({ error: \"payload_too_large\" }, 413);\n\t\t}\n\n\t\tconst auth = authorizeSyncRequest(store, c.req, raw);\n\t\tif (!auth.ok) return c.json(unauthorizedPayload(auth.reason), 401);\n\t\tconst peerDeviceId = auth.deviceId;\n\n\t\tlet body: Record<string, unknown>;\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(raw.toString(\"utf-8\")) as unknown;\n\t\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n\t\t\t\treturn c.json({ error: \"invalid_json\" }, 400);\n\t\t\t}\n\t\t\tbody = parsed as Record<string, unknown>;\n\t\t} catch {\n\t\t\treturn c.json({ error: \"invalid_json\" }, 400);\n\t\t}\n\n\t\tif (!Array.isArray(body.ops)) {\n\t\t\treturn c.json({ error: \"invalid_ops\" }, 400);\n\t\t}\n\t\tif (body.ops.length > MAX_SYNC_OPS) {\n\t\t\treturn c.json({ error: \"too_many_ops\" }, 413);\n\t\t}\n\n\t\tconst normalizedOps = extractReplicationOps(body);\n\t\tfor (const op of normalizedOps) {\n\t\t\tif (op.device_id !== peerDeviceId || op.clock_device_id !== peerDeviceId) {\n\t\t\t\treturn c.json(\n\t\t\t\t\t{\n\t\t\t\t\t\terror: \"invalid_op_device\",\n\t\t\t\t\t\treason: \"device_id_mismatch\",\n\t\t\t\t\t\top_id: op.op_id,\n\t\t\t\t\t},\n\t\t\t\t\t400,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tlet localDeviceId = store.db.prepare(\"SELECT device_id FROM sync_device LIMIT 1\").get() as\n\t\t\t| { device_id: string }\n\t\t\t| undefined;\n\t\tif (!localDeviceId) {\n\t\t\tconst [deviceId] = ensureDeviceIdentity(store.db);\n\t\t\tlocalDeviceId = { device_id: deviceId };\n\t\t}\n\n\t\tconst filteredInbound = filterOpsForPeer(store, peerDeviceId, normalizedOps);\n\t\tconst result = applyReplicationOps(store.db, filteredInbound.allowed, localDeviceId.device_id);\n\t\treturn c.json({\n\t\t\t...result,\n\t\t\tskipped: result.skipped + filteredInbound.skipped,\n\t\t});\n\t});\n\n\treturn app;\n}\n\n/**\n * Viewer-facing sync management routes (/api/sync/*).\n *\n * These are mounted on the viewer listener (127.0.0.1:38888) and\n * provide sync status, peer management, and coordinator UI for the\n * local viewer.\n */\nexport function syncRoutes(\n\tgetStore: StoreFactory,\n\tgetSyncRuntimeStatus?: () => SyncRuntimeStatus | null,\n) {\n\tconst app = new Hono();\n\n\t// GET /api/sync/status\n\tapp.get(\"/api/sync/status\", async (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst showDiag = queryBool(c.req.query(\"includeDiagnostics\"));\n\t\t\tconst includeJoinRequests = queryBool(c.req.query(\"includeJoinRequests\"));\n\t\t\tconst project = c.req.query(\"project\") || null;\n\t\t\tconst config = readCoordinatorSyncConfig();\n\n\t\t\tconst d = drizzle(store.db, { schema });\n\n\t\t\tconst deviceRow = d\n\t\t\t\t.select({\n\t\t\t\t\tdevice_id: schema.syncDevice.device_id,\n\t\t\t\t\tfingerprint: schema.syncDevice.fingerprint,\n\t\t\t\t})\n\t\t\t\t.from(schema.syncDevice)\n\t\t\t\t.limit(1)\n\t\t\t\t.get();\n\n\t\t\tconst daemonState = d\n\t\t\t\t.select()\n\t\t\t\t.from(schema.syncDaemonState)\n\t\t\t\t.where(eq(schema.syncDaemonState.id, 1))\n\t\t\t\t.get();\n\n\t\t\tconst peerCountRow = d.select({ total: count() }).from(schema.syncPeers).get();\n\n\t\t\tconst lastSyncRow = d\n\t\t\t\t.select({ last_sync_at: max(schema.syncPeers.last_sync_at) })\n\t\t\t\t.from(schema.syncPeers)\n\t\t\t\t.get();\n\n\t\t\tconst lastError = daemonState?.last_error as string | null;\n\t\t\tconst lastErrorAt = daemonState?.last_error_at as string | null;\n\t\t\tconst lastOkAt = daemonState?.last_ok_at as string | null;\n\t\t\tconst viewerBinding = readViewerBinding(store.dbPath);\n\t\t\tconst daemonRunning = viewerBinding\n\t\t\t\t? await portOpen(viewerBinding.host, viewerBinding.port)\n\t\t\t\t: false;\n\t\t\tconst daemonDetail = viewerBinding\n\t\t\t\t? daemonRunning\n\t\t\t\t\t? `viewer pidfile at ${viewerBinding.host}:${viewerBinding.port}`\n\t\t\t\t\t: `pidfile present but ${viewerBinding.host}:${viewerBinding.port} is unreachable`\n\t\t\t\t: null;\n\n\t\t\tlet daemonStateValue = \"ok\";\n\t\t\tif (!config.syncEnabled) {\n\t\t\t\tdaemonStateValue = \"disabled\";\n\t\t\t} else if (lastError && (!lastOkAt || String(lastOkAt) < String(lastErrorAt ?? \"\"))) {\n\t\t\t\tdaemonStateValue = \"error\";\n\t\t\t} else if (!daemonRunning) {\n\t\t\t\tdaemonStateValue = \"stopped\";\n\t\t\t}\n\n\t\t\tconst statusPayload: Record<string, unknown> = {\n\t\t\t\tenabled: config.syncEnabled,\n\t\t\t\tinterval_s: config.syncIntervalS,\n\t\t\t\tpeer_count: Number(peerCountRow?.total ?? 0),\n\t\t\t\tlast_sync_at: lastSyncRow?.last_sync_at ?? null,\n\t\t\t\tdaemon_state: daemonStateValue,\n\t\t\t\tdaemon_running: daemonRunning,\n\t\t\t\tdaemon_detail: daemonDetail,\n\t\t\t\tproject_filter_active:\n\t\t\t\t\tconfig.syncProjectsInclude.length > 0 || config.syncProjectsExclude.length > 0,\n\t\t\t\tproject_filter: {\n\t\t\t\t\tinclude: config.syncProjectsInclude,\n\t\t\t\t\texclude: config.syncProjectsExclude,\n\t\t\t\t},\n\t\t\t\tredacted: !showDiag,\n\t\t\t};\n\n\t\t\tif (showDiag) {\n\t\t\t\tstatusPayload.device_id = deviceRow?.device_id ?? null;\n\t\t\t\tstatusPayload.fingerprint = deviceRow?.fingerprint ?? null;\n\t\t\t\tstatusPayload.bind = `${config.syncHost}:${config.syncPort}`;\n\t\t\t\tstatusPayload.daemon_last_error = lastError;\n\t\t\t\tstatusPayload.daemon_last_error_at = lastErrorAt;\n\t\t\t\tstatusPayload.daemon_last_ok_at = lastOkAt;\n\t\t\t}\n\n\t\t\tconst runtimeStatus = getSyncRuntimeStatus?.() ?? null;\n\t\t\tif (runtimeStatus?.phase && runtimeStatus.phase !== \"running\") {\n\t\t\t\tdaemonStateValue = runtimeStatus.phase;\n\t\t\t\tstatusPayload.daemon_state = daemonStateValue;\n\t\t\t\tstatusPayload.daemon_running = runtimeStatus.phase === \"starting\" || daemonRunning;\n\t\t\t\tstatusPayload.daemon_detail = runtimeStatus.detail ?? daemonDetail;\n\t\t\t}\n\n\t\t\t// Build peers list using deduplicated mapPeerRow\n\t\t\tconst peerRows = store.db.prepare(PEERS_QUERY).all() as Record<string, unknown>[];\n\t\t\tconst peersItems = peerRows.map((row) => {\n\t\t\t\tconst peer = mapPeerRow(row, showDiag);\n\t\t\t\tpeer.status = peerStatus(peer);\n\t\t\t\treturn peer;\n\t\t\t});\n\n\t\t\tconst peersMap: Record<string, unknown> = {};\n\t\t\tfor (const peer of peersItems) {\n\t\t\t\tpeersMap[String(peer.peer_device_id)] = peer.status;\n\t\t\t}\n\n\t\t\t// Attempts\n\t\t\tconst attemptRows = d\n\t\t\t\t.select({\n\t\t\t\t\tpeer_device_id: schema.syncAttempts.peer_device_id,\n\t\t\t\t\tok: schema.syncAttempts.ok,\n\t\t\t\t\terror: schema.syncAttempts.error,\n\t\t\t\t\tstarted_at: schema.syncAttempts.started_at,\n\t\t\t\t\tfinished_at: schema.syncAttempts.finished_at,\n\t\t\t\t\tops_in: schema.syncAttempts.ops_in,\n\t\t\t\t\tops_out: schema.syncAttempts.ops_out,\n\t\t\t\t})\n\t\t\t\t.from(schema.syncAttempts)\n\t\t\t\t.orderBy(desc(schema.syncAttempts.finished_at))\n\t\t\t\t.limit(25)\n\t\t\t\t.all();\n\t\t\tconst attemptsItems = attemptRows.map((row) => ({\n\t\t\t\t...row,\n\t\t\t\tstatus: attemptStatus(row),\n\t\t\t\taddress: null,\n\t\t\t}));\n\n\t\t\tconst statusBlock: Record<string, unknown> = {\n\t\t\t\t...statusPayload,\n\t\t\t\tpeers: peersMap,\n\t\t\t\tpending: 0,\n\t\t\t\tsync: {},\n\t\t\t\tping: {},\n\t\t\t};\n\t\t\tconst legacyDevices = store.claimableLegacyDeviceIds();\n\t\t\tconst sharingReview = store.sharingReviewSummary(project);\n\t\t\tconst coordinator = await coordinatorStatusSnapshot(store, config);\n\t\t\tlet joinRequests: Record<string, unknown>[] = [];\n\t\t\tif (includeJoinRequests && config.syncCoordinatorAdminSecret) {\n\t\t\t\ttry {\n\t\t\t\t\tjoinRequests = await listCoordinatorJoinRequests(config);\n\t\t\t\t} catch {\n\t\t\t\t\tjoinRequests = [];\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (daemonStateValue === \"ok\") {\n\t\t\t\tconst peerStates = new Set(\n\t\t\t\t\tpeersItems.map((peer) =>\n\t\t\t\t\t\tString((peer.status as Record<string, unknown> | undefined)?.peer_state ?? \"\"),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tconst latestFailedRecently = Boolean(\n\t\t\t\t\tattemptsItems[0] &&\n\t\t\t\t\t\tattemptsItems[0].status === \"error\" &&\n\t\t\t\t\t\tisRecentIso(attemptsItems[0].finished_at),\n\t\t\t\t);\n\t\t\t\tconst allOffline =\n\t\t\t\t\tpeersItems.length > 0 &&\n\t\t\t\t\tpeersItems.every(\n\t\t\t\t\t\t(peer) =>\n\t\t\t\t\t\t\tString((peer.status as Record<string, unknown>)?.peer_state ?? \"\") === \"offline\",\n\t\t\t\t\t);\n\t\t\t\tif (latestFailedRecently) {\n\t\t\t\t\tconst hasLivePeer = peerStates.has(\"online\") || peerStates.has(\"degraded\");\n\t\t\t\t\tif (hasLivePeer) daemonStateValue = \"degraded\";\n\t\t\t\t\telse if (allOffline) daemonStateValue = \"offline-peers\";\n\t\t\t\t\telse if (peersItems.length > 0) daemonStateValue = \"stale\";\n\t\t\t\t} else if (peerStates.has(\"degraded\")) {\n\t\t\t\t\tdaemonStateValue = \"degraded\";\n\t\t\t\t} else if (allOffline) {\n\t\t\t\t\tdaemonStateValue = \"offline-peers\";\n\t\t\t\t} else if (peersItems.length > 0 && !peerStates.has(\"online\")) {\n\t\t\t\t\tdaemonStateValue = \"stale\";\n\t\t\t\t}\n\t\t\t\tstatusPayload.daemon_state = daemonStateValue;\n\t\t\t\tstatusBlock.daemon_state = daemonStateValue;\n\t\t\t}\n\n\t\t\tconst responsePayload: Record<string, unknown> = {\n\t\t\t\t...statusPayload,\n\t\t\t\tstatus: statusBlock,\n\t\t\t\tpeers: peersItems,\n\t\t\t\tattempts: attemptsItems.slice(0, 5),\n\t\t\t\tlegacy_devices: legacyDevices,\n\t\t\t\tsharing_review: sharingReview,\n\t\t\t\tcoordinator,\n\t\t\t};\n\t\t\tif (includeJoinRequests) {\n\t\t\t\tresponsePayload.join_requests = joinRequests;\n\t\t\t}\n\t\t\treturn c.json(responsePayload);\n\t\t}\n\t});\n\n\t// GET /api/sync/peers\n\tapp.get(\"/api/sync/peers\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst showDiag = queryBool(c.req.query(\"includeDiagnostics\"));\n\t\t\tconst rows = store.db.prepare(PEERS_QUERY).all() as Record<string, unknown>[];\n\t\t\t// Use deduplicated mapPeerRow helper (fix #4)\n\t\t\tconst peers = rows.map((row) => mapPeerRow(row, showDiag));\n\t\t\treturn c.json({ items: peers, redacted: !showDiag });\n\t\t}\n\t});\n\n\t// GET /api/sync/actors\n\tapp.get(\"/api/sync/actors\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst includeMerged = queryBool(c.req.query(\"includeMerged\"));\n\t\t\tconst query = d.select().from(schema.actors);\n\t\t\tconst rows = includeMerged\n\t\t\t\t? query.orderBy(schema.actors.display_name).all()\n\t\t\t\t: query.where(ne(schema.actors.status, \"merged\")).orderBy(schema.actors.display_name).all();\n\t\t\treturn c.json({ items: rows });\n\t\t}\n\t});\n\n\t// GET /api/sync/attempts\n\tapp.get(\"/api/sync/attempts\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tlet limit = queryInt(c.req.query(\"limit\"), 25);\n\t\t\tif (limit <= 0) return c.json({ error: \"invalid_limit\" }, 400);\n\t\t\tlimit = Math.min(limit, 500);\n\t\t\tconst rows = d\n\t\t\t\t.select({\n\t\t\t\t\tpeer_device_id: schema.syncAttempts.peer_device_id,\n\t\t\t\t\tok: schema.syncAttempts.ok,\n\t\t\t\t\terror: schema.syncAttempts.error,\n\t\t\t\t\tstarted_at: schema.syncAttempts.started_at,\n\t\t\t\t\tfinished_at: schema.syncAttempts.finished_at,\n\t\t\t\t\tops_in: schema.syncAttempts.ops_in,\n\t\t\t\t\tops_out: schema.syncAttempts.ops_out,\n\t\t\t\t})\n\t\t\t\t.from(schema.syncAttempts)\n\t\t\t\t.orderBy(desc(schema.syncAttempts.finished_at))\n\t\t\t\t.limit(limit)\n\t\t\t\t.all();\n\t\t\treturn c.json({ items: rows });\n\t\t}\n\t});\n\n\t// GET /api/sync/pairing — uses ensureDeviceIdentity from core (fix #5 context)\n\tapp.get(\"/api/sync/pairing\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst showDiag = queryBool(c.req.query(\"includeDiagnostics\"));\n\t\t\tif (!showDiag) {\n\t\t\t\treturn c.json({\n\t\t\t\t\tredacted: true,\n\t\t\t\t\tpairing_filter_hint: PAIRING_FILTER_HINT,\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst deviceRow = d\n\t\t\t\t.select({\n\t\t\t\t\tdevice_id: schema.syncDevice.device_id,\n\t\t\t\t\tpublic_key: schema.syncDevice.public_key,\n\t\t\t\t\tfingerprint: schema.syncDevice.fingerprint,\n\t\t\t\t})\n\t\t\t\t.from(schema.syncDevice)\n\t\t\t\t.limit(1)\n\t\t\t\t.get();\n\n\t\t\tlet deviceId: string | undefined;\n\t\t\tlet publicKey: string | undefined;\n\t\t\tlet fingerprint: string | undefined;\n\n\t\t\tif (deviceRow) {\n\t\t\t\tdeviceId = String(deviceRow.device_id);\n\t\t\t\tpublicKey = String(deviceRow.public_key);\n\t\t\t\tfingerprint = String(deviceRow.fingerprint);\n\t\t\t} else {\n\t\t\t\t// Fall back to ensureDeviceIdentity if no row exists\n\t\t\t\ttry {\n\t\t\t\t\tconst [id, fp] = ensureDeviceIdentity(store.db);\n\t\t\t\t\tdeviceId = id;\n\t\t\t\t\tfingerprint = fp;\n\t\t\t\t\tconst newRow = d\n\t\t\t\t\t\t.select({ public_key: schema.syncDevice.public_key })\n\t\t\t\t\t\t.from(schema.syncDevice)\n\t\t\t\t\t\t.where(eq(schema.syncDevice.device_id, id))\n\t\t\t\t\t\t.get();\n\t\t\t\t\tpublicKey = newRow?.public_key ?? \"\";\n\t\t\t\t} catch {\n\t\t\t\t\treturn c.json({ error: \"device identity unavailable\" }, 500);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!deviceId || !fingerprint) {\n\t\t\t\treturn c.json({ error: \"public key missing\" }, 500);\n\t\t\t}\n\n\t\t\treturn c.json({\n\t\t\t\tdevice_id: deviceId,\n\t\t\t\tfingerprint,\n\t\t\t\tpublic_key: publicKey ?? null,\n\t\t\t\tpairing_filter_hint: PAIRING_FILTER_HINT,\n\t\t\t\taddresses: [],\n\t\t\t});\n\t\t}\n\t});\n\n\t// ------------------------------------------------------------------\n\t// POST mutations\n\t// ------------------------------------------------------------------\n\n\t// POST /api/sync/peers/rename\n\tapp.post(\"/api/sync/peers/rename\", async (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst body = await c.req.json<Record<string, unknown>>();\n\t\t\tconst peerDeviceId = String(body.peer_device_id ?? \"\").trim();\n\t\t\tconst name = String(body.name ?? \"\").trim();\n\t\t\tif (!peerDeviceId) return c.json({ error: \"peer_device_id required\" }, 400);\n\t\t\tif (!name) return c.json({ error: \"name required\" }, 400);\n\t\t\tconst exists = d\n\t\t\t\t.select({ peer_device_id: schema.syncPeers.peer_device_id })\n\t\t\t\t.from(schema.syncPeers)\n\t\t\t\t.where(eq(schema.syncPeers.peer_device_id, peerDeviceId))\n\t\t\t\t.get();\n\t\t\tif (!exists) return c.json({ error: \"peer not found\" }, 404);\n\t\t\td.update(schema.syncPeers)\n\t\t\t\t.set({ name })\n\t\t\t\t.where(eq(schema.syncPeers.peer_device_id, peerDeviceId))\n\t\t\t\t.run();\n\t\t\treturn c.json({ ok: true });\n\t\t}\n\t});\n\n\tapp.post(\"/api/sync/invites/create\", async (c) => {\n\t\tlet body: Record<string, unknown>;\n\t\ttry {\n\t\t\tbody = await c.req.json<Record<string, unknown>>();\n\t\t} catch {\n\t\t\treturn c.json({ error: \"invalid json\" }, 400);\n\t\t}\n\t\tconst groupId = String(body.group_id ?? \"\").trim();\n\t\tconst coordinatorUrl = body.coordinator_url == null ? null : String(body.coordinator_url ?? \"\");\n\t\tconst policy = String(body.policy ?? \"auto_admit\").trim();\n\t\tconst ttlHours = Number.parseInt(String(body.ttl_hours ?? 24), 10);\n\t\tif (!groupId) return c.json({ error: \"group_id required\" }, 400);\n\t\tif (body.coordinator_url != null && typeof body.coordinator_url !== \"string\") {\n\t\t\treturn c.json({ error: \"coordinator_url must be string\" }, 400);\n\t\t}\n\t\tif (![\"auto_admit\", \"approval_required\"].includes(policy)) {\n\t\t\treturn c.json({ error: \"policy must be auto_admit or approval_required\" }, 400);\n\t\t}\n\t\tif (!Number.isFinite(ttlHours)) return c.json({ error: \"ttl_hours must be int\" }, 400);\n\t\ttry {\n\t\t\tconst config = readCoordinatorSyncConfig();\n\t\t\tconst result = await coordinatorCreateInviteAction({\n\t\t\t\tgroupId,\n\t\t\t\tcoordinatorUrl,\n\t\t\t\tpolicy,\n\t\t\t\tttlHours,\n\t\t\t\tcreatedBy: null,\n\t\t\t\tremoteUrl: config.syncCoordinatorUrl || null,\n\t\t\t\tadminSecret: config.syncCoordinatorAdminSecret || null,\n\t\t\t});\n\t\t\treturn c.json(result);\n\t\t} catch (error) {\n\t\t\treturn c.json({ error: error instanceof Error ? error.message : String(error) }, 400);\n\t\t}\n\t});\n\n\tapp.post(\"/api/sync/invites/import\", async (c) => {\n\t\tconst store = getStore();\n\t\tlet body: Record<string, unknown>;\n\t\ttry {\n\t\t\tbody = await c.req.json<Record<string, unknown>>();\n\t\t} catch {\n\t\t\treturn c.json({ error: \"invalid json\" }, 400);\n\t\t}\n\t\tconst inviteValue = String(body.invite ?? \"\").trim();\n\t\tif (!inviteValue) return c.json({ error: \"invite required\" }, 400);\n\t\ttry {\n\t\t\tconst result = await coordinatorImportInviteAction({ inviteValue, dbPath: store.dbPath });\n\t\t\treturn c.json(result);\n\t\t} catch (error) {\n\t\t\treturn c.json({ error: error instanceof Error ? error.message : String(error) }, 400);\n\t\t}\n\t});\n\n\tapp.post(\"/api/sync/join-requests/review\", async (c) => {\n\t\tlet body: Record<string, unknown>;\n\t\ttry {\n\t\t\tbody = await c.req.json<Record<string, unknown>>();\n\t\t} catch {\n\t\t\treturn c.json({ error: \"invalid json\" }, 400);\n\t\t}\n\t\tconst requestId = String(body.request_id ?? \"\").trim();\n\t\tconst action = String(body.action ?? \"\").trim();\n\t\tif (!requestId) return c.json({ error: \"request_id required\" }, 400);\n\t\tif (![\"approve\", \"deny\"].includes(action)) {\n\t\t\treturn c.json({ error: \"action must be approve or deny\" }, 400);\n\t\t}\n\t\ttry {\n\t\t\tconst config = readCoordinatorSyncConfig();\n\t\t\tconst result = await coordinatorReviewJoinRequestAction({\n\t\t\t\trequestId,\n\t\t\t\tapprove: action === \"approve\",\n\t\t\t\treviewedBy: null,\n\t\t\t\tremoteUrl: config.syncCoordinatorUrl || null,\n\t\t\t\tadminSecret: config.syncCoordinatorAdminSecret || null,\n\t\t\t});\n\t\t\tif (!result) return c.json({ error: \"join request not found\" }, 404);\n\t\t\treturn c.json({ ok: true, request: result });\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\treturn c.json(\n\t\t\t\t{ error: message },\n\t\t\t\tmessage.includes(\"request_not_found\") || message.includes(\"not found\") ? 404 : 400,\n\t\t\t);\n\t\t}\n\t});\n\n\t// DELETE /api/sync/peers/:peer_device_id\n\tapp.delete(\"/api/sync/peers/:peer_device_id\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst peerDeviceId = c.req.param(\"peer_device_id\")?.trim();\n\t\t\tif (!peerDeviceId) return c.json({ error: \"peer_device_id required\" }, 400);\n\t\t\tconst exists = d\n\t\t\t\t.select({ peer_device_id: schema.syncPeers.peer_device_id })\n\t\t\t\t.from(schema.syncPeers)\n\t\t\t\t.where(eq(schema.syncPeers.peer_device_id, peerDeviceId))\n\t\t\t\t.get();\n\t\t\tif (!exists) return c.json({ error: \"peer not found\" }, 404);\n\t\t\td.delete(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).run();\n\t\t\treturn c.json({ ok: true });\n\t\t}\n\t});\n\n\treturn app;\n}\n","/**\n * @codemem/server — HTTP server (viewer, sync, API).\n *\n * Single HTTP server handling viewer routes and sync daemon.\n * Shares one better-sqlite3 connection between viewer and sync.\n * Embedding inference runs in a worker_thread (lazy-started).\n *\n * Entry: `codemem serve`\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { ObserverClient } from \"@codemem/core\";\nimport { MemoryStore, type RawEventSweeper, resolveDbPath, VERSION } from \"@codemem/core\";\nimport { serveStatic } from \"@hono/node-server/serve-static\";\nimport { Hono } from \"hono\";\nimport { originGuard, preflightHandler } from \"./middleware.js\";\nimport { configRoutes } from \"./routes/config.js\";\nimport { memoryRoutes } from \"./routes/memory.js\";\nimport { observerStatusRoutes } from \"./routes/observer-status.js\";\nimport { rawEventsRoutes } from \"./routes/raw-events.js\";\nimport { statsRoutes } from \"./routes/stats.js\";\nimport { syncProtocolRoutes, syncRoutes } from \"./routes/sync.js\";\n\nexport { VERSION };\n\n/** Shared store instance — SQLite WAL mode handles concurrent reads safely. */\nlet sharedStore: MemoryStore | null = null;\n\n/** Get (or create) the shared store instance. Exported so the sweeper can share it. */\nexport function getStore(): MemoryStore {\n\tif (!sharedStore) {\n\t\tsharedStore = new MemoryStore(resolveDbPath());\n\t}\n\treturn sharedStore;\n}\n\n/** Close the shared store (called on shutdown). */\nexport function closeStore(): void {\n\tsharedStore?.close();\n\tsharedStore = null;\n}\n\n/**\n * Create the Hono app with all viewer routes.\n * Exported for testing — pass a custom store factory to inject test DBs.\n */\nexport interface AppOptions {\n\tstoreFactory?: () => MemoryStore;\n\tsweeper?: RawEventSweeper | null;\n\tobserver?: ObserverClient | null;\n\tgetSyncRuntimeStatus?: () => {\n\t\tphase: \"starting\" | \"running\" | \"stopping\" | \"error\" | \"disabled\" | null;\n\t\tdetail?: string | null;\n\t} | null;\n}\n\nexport function createApp(opts?: AppOptions) {\n\tconst storeFactory = opts?.storeFactory ?? getStore;\n\tconst sweeper = opts?.sweeper ?? null;\n\tconst observer = opts?.observer ?? null;\n\tconst getSyncRuntimeStatus = opts?.getSyncRuntimeStatus ?? (() => null);\n\tconst app = new Hono();\n\n\t// CORS / origin guard\n\tapp.use(\"*\", preflightHandler());\n\tapp.use(\"*\", originGuard());\n\n\t// API routes\n\tapp.route(\"/\", statsRoutes(storeFactory));\n\tapp.route(\"/\", memoryRoutes(storeFactory));\n\tapp.route(\n\t\t\"/\",\n\t\tobserverStatusRoutes({\n\t\t\tgetStore: storeFactory,\n\t\t\tgetSweeper: () => sweeper,\n\t\t\tgetObserver: () => observer,\n\t\t}),\n\t);\n\tapp.route(\"/\", configRoutes({ getSweeper: () => sweeper }));\n\tapp.route(\"/\", rawEventsRoutes(storeFactory, sweeper));\n\tapp.route(\"/\", syncRoutes(storeFactory, getSyncRuntimeStatus));\n\n\t// Static assets — serve under /assets/*\n\t// Resolves to packages/viewer-server/static/ both in dev and when installed from npm.\n\tconst staticRoot =\n\t\tprocess.env.CODEMEM_VIEWER_STATIC_DIR ?? join(import.meta.dirname ?? \".\", \"../static\");\n\n\tapp.use(\n\t\t\"/assets/*\",\n\t\tserveStatic({\n\t\t\troot: staticRoot,\n\t\t\trewriteRequestPath: (path) => path.replace(/^\\/assets/, \"\"),\n\t\t}),\n\t);\n\n\t// SPA — serve index.html for root and all client-side routes\n\tconst indexHtml = readFileSync(join(staticRoot, \"index.html\"), \"utf-8\");\n\tapp.get(\"*\", (c) => {\n\t\tif (c.req.path.startsWith(\"/api/\")) {\n\t\t\treturn c.json({ error: \"not found\" }, 404);\n\t\t}\n\t\treturn c.html(indexHtml);\n\t});\n\n\treturn app;\n}\n\n/**\n * Create the Hono app for peer-to-peer sync protocol routes only.\n *\n * This app is bound to a separate network-accessible listener\n * (default 0.0.0.0:7337) so remote peers can reach the sync\n * endpoints without exposing the viewer UI or local API routes.\n *\n * No CORS/origin guard is applied — sync requests are authenticated\n * via cryptographic signature verification instead.\n */\nexport function createSyncApp(opts?: AppOptions) {\n\tconst storeFactory = opts?.storeFactory ?? getStore;\n\tconst app = new Hono();\n\tapp.route(\"/\", syncProtocolRoutes(storeFactory));\n\n\t// Catch-all — return generic 404 to avoid fingerprinting the server.\n\tapp.all(\"*\", (c) => c.json({ error: \"not found\" }, 404));\n\n\treturn app;\n}\n\n// No auto-start — the CLI's `serve` command owns server startup.\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/middleware.ts","../src/routes/config.ts","../src/helpers.ts","../src/routes/memory.ts","../src/routes/observer-status.ts","../src/routes/raw-events.ts","../src/routes/stats.ts","../src/routes/sync.ts","../src/index.ts"],"sourcesContent":["/**\n * CORS and cross-origin protection middleware.\n *\n * Ports Python's reject_cross_origin() logic from codemem/viewer_http.py.\n * GETs are allowed from any origin (viewer is local-only).\n * Mutations (POST/DELETE/PATCH/PUT) require an Origin header matching a\n * loopback address, or are rejected with 403.\n */\n\nimport type { Context, Next } from \"hono\";\nimport { createMiddleware } from \"hono/factory\";\n\nconst LOOPBACK_HOSTS = new Set([\"127.0.0.1\", \"localhost\", \"::1\"]);\n\n/**\n * Check whether an Origin header value is a valid loopback URL.\n * Mirrors Python's _is_allowed_loopback_origin_url().\n */\nfunction isLoopbackOrigin(origin: string): boolean {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(origin);\n\t} catch {\n\t\treturn false;\n\t}\n\tif (url.protocol !== \"http:\" && url.protocol !== \"https:\") return false;\n\tif (url.username || url.password) return false;\n\treturn LOOPBACK_HOSTS.has(url.hostname);\n}\n\n/** HTTP methods that mutate state and require origin validation. */\nconst UNSAFE_METHODS = new Set([\"POST\", \"DELETE\", \"PATCH\", \"PUT\"]);\n\n/**\n * Check whether a missing-Origin request looks like a cross-site browser\n * request. Matches Python's `_is_unsafe_missing_origin()`:\n *\n * - Sec-Fetch-Site present and NOT same-origin/same-site/none → unsafe\n * - Referer present and NOT loopback → unsafe\n * - Otherwise → safe (CLI / programmatic caller, no browser context)\n */\nfunction isUnsafeMissingOrigin(c: Context): boolean {\n\tconst secFetchSite = (c.req.header(\"Sec-Fetch-Site\") ?? \"\").trim().toLowerCase();\n\tif (secFetchSite && ![\"same-origin\", \"same-site\", \"none\"].includes(secFetchSite)) {\n\t\treturn true;\n\t}\n\tconst referer = c.req.header(\"Referer\");\n\tif (!referer) return false;\n\treturn !isLoopbackOrigin(referer);\n}\n\n/**\n * Cross-origin protection middleware.\n *\n * Ports Python's `reject_cross_origin(missing_origin_policy=\"reject_if_unsafe\")`:\n *\n * - GET/HEAD/OPTIONS: allowed from any origin (viewer is local-only).\n * - POST/DELETE/PATCH/PUT:\n * - Origin present + loopback → allowed (browser on localhost)\n * - Origin present + non-loopback → rejected 403\n * - No Origin + no suspicious browser signals → allowed (CLI callers)\n * - No Origin + suspicious Sec-Fetch-Site/Referer → rejected 403\n *\n * For same-origin requests (no Origin header) on safe methods, no\n * Access-Control-Allow-Origin is set — the browser doesn't need it.\n * For valid loopback origins, ACAO is echoed back.\n */\nexport function originGuard() {\n\treturn createMiddleware(async (c: Context, next: Next) => {\n\t\tconst origin = c.req.header(\"Origin\");\n\t\tconst method = c.req.method;\n\n\t\tif (UNSAFE_METHODS.has(method)) {\n\t\t\tif (origin) {\n\t\t\t\t// Origin present — must be loopback\n\t\t\t\tif (!isLoopbackOrigin(origin)) {\n\t\t\t\t\treturn c.json({ error: \"forbidden\" }, 403);\n\t\t\t\t}\n\t\t\t\t// Valid loopback origin — echo it for CORS\n\t\t\t\tc.header(\"Access-Control-Allow-Origin\", origin);\n\t\t\t\tc.header(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\");\n\t\t\t\tc.header(\"Access-Control-Allow-Headers\", \"Content-Type\");\n\t\t\t} else {\n\t\t\t\t// No Origin — reject only if browser signals indicate cross-site\n\t\t\t\t// (matches Python's reject_if_unsafe policy for API endpoints)\n\t\t\t\tif (isUnsafeMissingOrigin(c)) {\n\t\t\t\t\treturn c.json({ error: \"forbidden\" }, 403);\n\t\t\t\t}\n\t\t\t\t// CLI / programmatic caller — no CORS headers needed\n\t\t\t}\n\t\t} else if (origin && isLoopbackOrigin(origin)) {\n\t\t\t// Safe method with valid origin — echo for preflight\n\t\t\tc.header(\"Access-Control-Allow-Origin\", origin);\n\t\t\tc.header(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\");\n\t\t\tc.header(\"Access-Control-Allow-Headers\", \"Content-Type\");\n\t\t}\n\t\t// No origin or non-loopback on safe method: no ACAO header set.\n\t\t// Browser enforces same-origin; we don't set permissive headers.\n\n\t\tawait next();\n\t});\n}\n\n/**\n * Handle OPTIONS preflight requests.\n * Returns 204 with appropriate CORS headers for loopback origins.\n */\nexport function preflightHandler() {\n\treturn createMiddleware(async (c: Context, next: Next) => {\n\t\tif (c.req.method !== \"OPTIONS\") {\n\t\t\tawait next();\n\t\t\treturn;\n\t\t}\n\t\tconst origin = c.req.header(\"Origin\");\n\t\tif (origin && isLoopbackOrigin(origin)) {\n\t\t\tc.header(\"Access-Control-Allow-Origin\", origin);\n\t\t\tc.header(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\");\n\t\t\tc.header(\"Access-Control-Allow-Headers\", \"Content-Type\");\n\t\t\tc.header(\"Access-Control-Max-Age\", \"86400\");\n\t\t\treturn c.body(null, 204);\n\t\t}\n\t\treturn c.body(null, 204);\n\t});\n}\n","/**\n * Config routes — GET /api/config, POST /api/config.\n *\n * Ports the user-facing config read/write path from Python's\n * codemem/viewer_routes/config.py, scoped to the TS runtime's current needs.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n\tCODEMEM_CONFIG_ENV_OVERRIDES,\n\tgetCodememConfigPath,\n\tgetCodememEnvOverrides,\n\tlistObserverProviderOptions,\n\ttype RawEventSweeper,\n\treadCodememConfigFile,\n\tstripJsonComments,\n\tstripTrailingCommas,\n\twriteCodememConfigFile,\n} from \"@codemem/core\";\nimport { Hono } from \"hono\";\n\ntype ConfigData = Record<string, unknown>;\n\nconst RUNTIMES = new Set([\"api_http\", \"claude_sidecar\"]);\nconst AUTH_SOURCES = new Set([\"auto\", \"env\", \"file\", \"command\", \"none\"]);\nconst HOT_RELOAD_KEYS = new Set([\"raw_events_sweeper_interval_s\"]);\nconst ALLOWED_KEYS = [\n\t\"claude_command\",\n\t\"observer_base_url\",\n\t\"observer_provider\",\n\t\"observer_model\",\n\t\"observer_runtime\",\n\t\"observer_auth_source\",\n\t\"observer_auth_file\",\n\t\"observer_auth_command\",\n\t\"observer_auth_timeout_ms\",\n\t\"observer_auth_cache_ttl_s\",\n\t\"observer_headers\",\n\t\"observer_max_chars\",\n\t\"pack_observation_limit\",\n\t\"pack_session_limit\",\n\t\"sync_enabled\",\n\t\"sync_retention_enabled\",\n\t\"sync_retention_max_age_days\",\n\t\"sync_retention_max_size_mb\",\n\t\"sync_host\",\n\t\"sync_port\",\n\t\"sync_interval_s\",\n\t\"sync_mdns\",\n\t\"sync_coordinator_url\",\n\t\"sync_coordinator_group\",\n\t\"sync_coordinator_timeout_s\",\n\t\"sync_coordinator_presence_ttl_s\",\n\t\"raw_events_sweeper_interval_s\",\n] as const;\n\nconst DEFAULTS: ConfigData = {\n\tclaude_command: [\"claude\"],\n\tobserver_runtime: \"api_http\",\n\tobserver_auth_source: \"auto\",\n\tobserver_auth_command: [],\n\tobserver_auth_timeout_ms: 1500,\n\tobserver_auth_cache_ttl_s: 300,\n\tobserver_headers: {},\n\tobserver_max_chars: 12000,\n\tpack_observation_limit: 50,\n\tpack_session_limit: 10,\n\tsync_enabled: false,\n\tsync_retention_enabled: false,\n\tsync_retention_max_age_days: 30,\n\tsync_retention_max_size_mb: 512,\n\tsync_host: \"0.0.0.0\",\n\tsync_port: 7337,\n\tsync_interval_s: 120,\n\tsync_mdns: true,\n\tsync_coordinator_timeout_s: 3,\n\tsync_coordinator_presence_ttl_s: 180,\n\traw_events_sweeper_interval_s: 30,\n};\n\nexport interface ConfigRouteOptions {\n\tgetSweeper?: () => RawEventSweeper | null;\n}\n\nfunction loadProviderOptions(): string[] {\n\treturn listObserverProviderOptions();\n}\n\nfunction getConfigPath(): string {\n\tconst envPath = process.env.CODEMEM_CONFIG;\n\tif (envPath) return envPath.replace(/^~/, homedir());\n\tconst configDir = join(homedir(), \".config\", \"codemem\");\n\tconst candidates = [join(configDir, \"config.json\"), join(configDir, \"config.jsonc\")];\n\treturn candidates.find((p) => existsSync(p)) ?? join(configDir, \"config.json\");\n}\n\nfunction readConfigFile(configPath: string): ConfigData {\n\tif (!existsSync(configPath)) return {};\n\ttry {\n\t\tlet text = readFileSync(configPath, \"utf-8\").trim();\n\t\tif (!text) return {};\n\t\ttry {\n\t\t\treturn JSON.parse(text) as ConfigData;\n\t\t} catch {\n\t\t\ttext = stripTrailingCommas(stripJsonComments(text));\n\t\t\treturn JSON.parse(text) as ConfigData;\n\t\t}\n\t} catch {\n\t\treturn {};\n\t}\n}\n\nfunction getEffectiveConfig(configData: ConfigData): ConfigData {\n\tconst effective: ConfigData = { ...DEFAULTS, ...configData };\n\tfor (const [key, envVar] of Object.entries(CODEMEM_CONFIG_ENV_OVERRIDES) as Array<\n\t\t[string, string]\n\t>) {\n\t\tconst val = process.env[envVar];\n\t\tif (val != null && val !== \"\") effective[key] = val;\n\t}\n\treturn effective;\n}\n\nfunction parsePositiveInt(value: unknown, allowZero = false): number | null {\n\tif (typeof value === \"boolean\") return null;\n\tconst parsed =\n\t\ttypeof value === \"number\"\n\t\t\t? value\n\t\t\t: typeof value === \"string\" && /^-?\\d+$/.test(value.trim())\n\t\t\t\t? Number(value.trim())\n\t\t\t\t: Number.NaN;\n\tif (!Number.isFinite(parsed) || !Number.isInteger(parsed)) return null;\n\tif (allowZero) return parsed >= 0 ? parsed : null;\n\treturn parsed > 0 ? parsed : null;\n}\n\nfunction asStringMap(value: unknown): Record<string, string> | null {\n\tif (value == null || typeof value !== \"object\" || Array.isArray(value)) return null;\n\tconst parsed: Record<string, string> = {};\n\tfor (const [key, item] of Object.entries(value)) {\n\t\tif (typeof item !== \"string\") return null;\n\t\tconst stripped = key.trim();\n\t\tif (!stripped) return null;\n\t\tparsed[stripped] = item;\n\t}\n\treturn parsed;\n}\n\nfunction asExecutableArgv(value: unknown): string[] | null {\n\tif (!Array.isArray(value)) return null;\n\tconst argv: string[] = [];\n\tfor (const item of value) {\n\t\tif (typeof item !== \"string\") return null;\n\t\tconst token = item.trim();\n\t\tif (!token) return null;\n\t\targv.push(token);\n\t}\n\treturn argv;\n}\n\nfunction validateAndApplyUpdate(\n\tconfigData: ConfigData,\n\tkey: (typeof ALLOWED_KEYS)[number],\n\tvalue: unknown,\n\tproviders: Set<string>,\n): string | null {\n\tif (value == null || value === \"\") {\n\t\tdelete configData[key];\n\t\treturn null;\n\t}\n\tif (key === \"observer_provider\") {\n\t\tif (typeof value !== \"string\") return \"observer_provider must be string\";\n\t\tconst provider = value.trim().toLowerCase();\n\t\tconst savedBaseUrl = configData.observer_base_url;\n\t\tconst hasSavedBaseUrl = typeof savedBaseUrl === \"string\" && savedBaseUrl.trim().length > 0;\n\t\tif (!providers.has(provider) && !hasSavedBaseUrl) {\n\t\t\treturn \"observer_provider must match a configured provider\";\n\t\t}\n\t\tconfigData[key] = provider;\n\t\treturn null;\n\t}\n\tif (key === \"observer_runtime\") {\n\t\tif (typeof value !== \"string\") return \"observer_runtime must be string\";\n\t\tconst runtime = value.trim().toLowerCase();\n\t\tif (!RUNTIMES.has(runtime)) {\n\t\t\treturn \"observer_runtime must be one of: api_http, claude_sidecar\";\n\t\t}\n\t\tconfigData[key] = runtime;\n\t\treturn null;\n\t}\n\tif (key === \"observer_auth_source\") {\n\t\tif (typeof value !== \"string\") return \"observer_auth_source must be string\";\n\t\tconst source = value.trim().toLowerCase();\n\t\tif (!AUTH_SOURCES.has(source)) {\n\t\t\treturn \"observer_auth_source must be one of: auto, env, file, command, none\";\n\t\t}\n\t\tconfigData[key] = source;\n\t\treturn null;\n\t}\n\tif (key === \"claude_command\" || key === \"observer_auth_command\") {\n\t\tconst argv = asExecutableArgv(value);\n\t\tif (argv == null) return `${key} must be string array`;\n\t\tif (argv.length > 0) configData[key] = argv;\n\t\telse delete configData[key];\n\t\treturn null;\n\t}\n\tif (key === \"observer_headers\") {\n\t\tconst headers = asStringMap(value);\n\t\tif (headers == null) return \"observer_headers must be object of string values\";\n\t\tif (Object.keys(headers).length > 0) configData[key] = headers;\n\t\telse delete configData[key];\n\t\treturn null;\n\t}\n\tif (key === \"sync_enabled\" || key === \"sync_retention_enabled\" || key === \"sync_mdns\") {\n\t\tif (typeof value !== \"boolean\") return `${key} must be boolean`;\n\t\tconfigData[key] = value;\n\t\treturn null;\n\t}\n\tif (\n\t\tkey === \"observer_base_url\" ||\n\t\tkey === \"observer_model\" ||\n\t\tkey === \"observer_auth_file\" ||\n\t\tkey === \"sync_host\" ||\n\t\tkey === \"sync_coordinator_url\" ||\n\t\tkey === \"sync_coordinator_group\"\n\t) {\n\t\tif (typeof value !== \"string\") return `${key} must be string`;\n\t\tconst trimmed = value.trim();\n\t\tif (!trimmed) delete configData[key];\n\t\telse configData[key] = trimmed;\n\t\treturn null;\n\t}\n\tconst allowZero = key === \"observer_auth_cache_ttl_s\";\n\tconst parsed = parsePositiveInt(value, allowZero);\n\tif (parsed == null) return `${key} must be ${allowZero ? \"non-negative int\" : \"int\"}`;\n\tconfigData[key] = parsed;\n\treturn null;\n}\n\nfunction applyRuntimeEffects(changedKeys: string[], opts: ConfigRouteOptions): string[] {\n\tconst applied: string[] = [];\n\tif (changedKeys.includes(\"raw_events_sweeper_interval_s\")) {\n\t\tconst configValue = readCodememConfigFile().raw_events_sweeper_interval_s;\n\t\tconst seconds =\n\t\t\ttypeof configValue === \"number\"\n\t\t\t\t? configValue\n\t\t\t\t: Number.parseInt(String(configValue ?? \"\"), 10);\n\t\tif (Number.isFinite(seconds) && seconds > 0) {\n\t\t\tprocess.env.CODEMEM_RAW_EVENTS_SWEEPER_INTERVAL_MS = String(seconds * 1000);\n\t\t} else {\n\t\t\tdelete process.env.CODEMEM_RAW_EVENTS_SWEEPER_INTERVAL_MS;\n\t\t}\n\t\topts.getSweeper?.()?.notifyConfigChanged();\n\t\tapplied.push(\"raw_events_sweeper_interval_s\");\n\t}\n\treturn applied;\n}\n\nexport function configRoutes(opts: ConfigRouteOptions = {}) {\n\tconst app = new Hono();\n\n\tapp.get(\"/api/config\", (c) => {\n\t\tconst configPath = getConfigPath();\n\t\tconst configData = readConfigFile(configPath);\n\t\treturn c.json({\n\t\t\tpath: configPath,\n\t\t\tconfig: configData,\n\t\t\tdefaults: DEFAULTS,\n\t\t\teffective: getEffectiveConfig(configData),\n\t\t\tenv_overrides: getCodememEnvOverrides(),\n\t\t\tproviders: loadProviderOptions(),\n\t\t});\n\t});\n\n\tapp.post(\"/api/config\", async (c) => {\n\t\tlet payload: unknown;\n\t\ttry {\n\t\t\tpayload = (await c.req.json()) as unknown;\n\t\t} catch {\n\t\t\treturn c.json({ error: \"invalid json\" }, 400);\n\t\t}\n\t\tif (payload == null || typeof payload !== \"object\" || Array.isArray(payload)) {\n\t\t\treturn c.json({ error: \"payload must be an object\" }, 400);\n\t\t}\n\t\tif (\n\t\t\t\"config\" in payload &&\n\t\t\t(payload as ConfigData).config != null &&\n\t\t\t(typeof (payload as ConfigData).config !== \"object\" ||\n\t\t\t\tArray.isArray((payload as ConfigData).config))\n\t\t) {\n\t\t\treturn c.json({ error: \"config must be an object\" }, 400);\n\t\t}\n\t\tconst updates =\n\t\t\t\"config\" in payload &&\n\t\t\t(payload as ConfigData).config != null &&\n\t\t\ttypeof (payload as ConfigData).config === \"object\" &&\n\t\t\t!Array.isArray((payload as ConfigData).config)\n\t\t\t\t? ((payload as ConfigData).config as ConfigData)\n\t\t\t\t: (payload as ConfigData);\n\n\t\tconst configPath = getCodememConfigPath();\n\t\tconst beforeConfig = readCodememConfigFile();\n\t\tconst beforeEffective = getEffectiveConfig(beforeConfig);\n\t\tconst nextConfig: ConfigData = { ...beforeConfig };\n\t\tconst providers = new Set(loadProviderOptions());\n\n\t\tconst touchedKeys = ALLOWED_KEYS.filter((key) => key in updates);\n\t\tfor (const key of ALLOWED_KEYS) {\n\t\t\tif (!(key in updates)) continue;\n\t\t\tconst error = validateAndApplyUpdate(nextConfig, key, updates[key], providers);\n\t\t\tif (error) return c.json({ error }, 400);\n\t\t}\n\n\t\tlet savedPath: string;\n\t\ttry {\n\t\t\tsavedPath = writeCodememConfigFile(nextConfig, configPath);\n\t\t} catch {\n\t\t\treturn c.json({ error: \"failed to write config\" }, 500);\n\t\t}\n\n\t\tconst afterEffective = getEffectiveConfig(nextConfig);\n\t\tconst savedChangedKeys = ALLOWED_KEYS.filter((key) => beforeConfig[key] !== nextConfig[key]);\n\t\tconst effectiveChangedKeys = ALLOWED_KEYS.filter(\n\t\t\t(key) => beforeEffective[key] !== afterEffective[key],\n\t\t);\n\t\tconst envOverrides = getCodememEnvOverrides();\n\t\tconst ignoredByEnvKeys = savedChangedKeys.filter(\n\t\t\t(key) => !effectiveChangedKeys.includes(key) && key in envOverrides,\n\t\t);\n\t\tconst runtimeChangedKeys = [\n\t\t\t...new Set([...touchedKeys, ...savedChangedKeys, ...effectiveChangedKeys]),\n\t\t];\n\t\tconst hotReloadedKeys = applyRuntimeEffects(runtimeChangedKeys, opts);\n\t\tconst restartRequiredKeys = effectiveChangedKeys.filter(\n\t\t\t(key) => !HOT_RELOAD_KEYS.has(key) && !(key in envOverrides),\n\t\t);\n\n\t\treturn c.json({\n\t\t\tpath: savedPath,\n\t\t\tconfig: nextConfig,\n\t\t\teffective: afterEffective,\n\t\t\teffects: {\n\t\t\t\tsaved_keys: savedChangedKeys,\n\t\t\t\teffective_keys: effectiveChangedKeys,\n\t\t\t\thot_reloaded_keys: hotReloadedKeys,\n\t\t\t\trestart_required_keys: restartRequiredKeys,\n\t\t\t\tignored_by_env_keys: ignoredByEnvKeys,\n\t\t\t\twarnings: ignoredByEnvKeys.map(\n\t\t\t\t\t(key) =>\n\t\t\t\t\t\t`${key} is currently controlled by ${envOverrides[key]}; saved config will not take effect until that override is removed.`,\n\t\t\t\t),\n\t\t\t},\n\t\t});\n\t});\n\n\treturn app;\n}\n","import { parseStrictInteger } from \"@codemem/core\";\n\n/**\n * Shared helpers for viewer-server routes.\n */\n\n/**\n * Parse a JSON string that should be an array of strings.\n * Returns an empty array on null, invalid JSON, or non-array values.\n * Mirrors Python's store._safe_json_list().\n */\nexport function safeJsonList(raw: string | null | undefined): string[] {\n\tif (raw == null) return [];\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(raw);\n\t\tif (!Array.isArray(parsed)) return [];\n\t\treturn parsed.filter((item): item is string => typeof item === \"string\");\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/**\n * Parse a query parameter as an integer, returning the default on failure.\n */\nexport function queryInt(value: string | undefined, defaultValue: number): number {\n\tif (value == null) return defaultValue;\n\tconst parsed = parseStrictInteger(value);\n\treturn parsed == null ? defaultValue : parsed;\n}\n\n/**\n * Parse a query parameter as a boolean flag.\n * Recognizes \"1\", \"true\", \"yes\" as truthy.\n */\nexport function queryBool(value: string | undefined): boolean {\n\tif (value == null) return false;\n\treturn value === \"1\" || value === \"true\" || value === \"yes\";\n}\n","/**\n * Memory routes — observations, summaries, sessions, projects, pack, artifacts.\n */\n\nimport type { MemoryStore } from \"@codemem/core\";\nimport { buildFilterClausesWithContext, fromJson, parseStrictInteger, schema } from \"@codemem/core\";\nimport { desc, eq, inArray, isNotNull } from \"drizzle-orm\";\nimport { drizzle } from \"drizzle-orm/better-sqlite3\";\nimport { Hono } from \"hono\";\nimport { queryInt } from \"../helpers.js\";\n\ntype StoreFactory = () => MemoryStore;\n\n/**\n * Attach session project/cwd fields to memory items.\n */\nfunction attachSessionFields(store: MemoryStore, items: Record<string, unknown>[]): void {\n\tconst sessionIds: number[] = [];\n\tconst seen = new Set<number>();\n\tfor (const item of items) {\n\t\tconst value = item.session_id;\n\t\tif (value == null) continue;\n\t\tconst sid = Number(value);\n\t\tif (Number.isNaN(sid) || seen.has(sid)) continue;\n\t\tseen.add(sid);\n\t\tsessionIds.push(sid);\n\t}\n\tif (sessionIds.length === 0) return;\n\n\tconst d = drizzle(store.db, { schema });\n\tconst rows = d\n\t\t.select({\n\t\t\tid: schema.sessions.id,\n\t\t\tproject: schema.sessions.project,\n\t\t\tcwd: schema.sessions.cwd,\n\t\t})\n\t\t.from(schema.sessions)\n\t\t.where(inArray(schema.sessions.id, sessionIds))\n\t\t.all();\n\n\tconst bySession = new Map<number, { project: string; cwd: string }>();\n\tfor (const row of rows) {\n\t\tconst projectRaw = String(row.project ?? \"\").trim();\n\t\tconst project = projectRaw ? projectBasename(projectRaw) : \"\";\n\t\tconst cwd = String(row.cwd ?? \"\");\n\t\tbySession.set(row.id, { project, cwd });\n\t}\n\n\tfor (const item of items) {\n\t\tconst sid = Number(item.session_id);\n\t\tif (Number.isNaN(sid)) continue;\n\t\tconst fields = bySession.get(sid);\n\t\tif (!fields) continue;\n\t\titem.project ??= fields.project;\n\t\titem.cwd ??= fields.cwd;\n\t}\n}\n\n/**\n * Extract the basename of a project path.\n * Strips \"fatal:\" prefixed values.\n */\nfunction projectBasename(raw: string): string {\n\tif (raw.toLowerCase().startsWith(\"fatal:\")) return \"\";\n\tconst parts = raw.replace(/\\\\/g, \"/\").split(\"/\");\n\treturn parts[parts.length - 1] ?? raw;\n}\n\nfunction normalizeScope(raw: string | undefined): \"mine\" | \"theirs\" | undefined {\n\tconst value = String(raw ?? \"\")\n\t\t.trim()\n\t\t.toLowerCase();\n\tif (value === \"mine\" || value === \"theirs\") return value;\n\treturn undefined;\n}\n\nfunction queryMemoryPage(\n\tstore: MemoryStore,\n\toptions: {\n\t\tlimit: number;\n\t\toffset: number;\n\t\tproject?: string;\n\t\tscope?: \"mine\" | \"theirs\";\n\t},\n): Record<string, unknown>[] {\n\tconst filters: Record<string, unknown> = {};\n\tif (options.project) filters.project = options.project;\n\tif (options.scope) filters.ownership_scope = options.scope;\n\n\tconst filterResult = buildFilterClausesWithContext(filters, {\n\t\tactorId: store.actorId,\n\t\tdeviceId: store.deviceId,\n\t});\n\tconst clauses = [\"memory_items.active = 1\", ...filterResult.clauses];\n\tconst where = clauses.join(\" AND \");\n\tconst from = filterResult.joinSessions\n\t\t? \"memory_items JOIN sessions ON sessions.id = memory_items.session_id\"\n\t\t: \"memory_items\";\n\n\tconst rows = store.db\n\t\t.prepare(\n\t\t\t`SELECT memory_items.* FROM ${from}\n\t\t\t WHERE ${where}\n\t\t\t ORDER BY memory_items.created_at DESC\n\t\t\t LIMIT ? OFFSET ?`,\n\t\t)\n\t\t.all(...filterResult.params, options.limit + 1, options.offset) as Record<string, unknown>[];\n\n\treturn rows.map((row) => ({\n\t\t...row,\n\t\tmetadata_json: fromJson((row.metadata_json as string) ?? null),\n\t}));\n}\n\nfunction isSummaryLikeMemory(item: Record<string, unknown>): boolean {\n\tif (String(item.kind ?? \"\").toLowerCase() === \"session_summary\") return true;\n\tconst metadata = (item.metadata_json ?? {}) as Record<string, unknown>;\n\tif (metadata.is_summary === true) return true;\n\treturn (\n\t\tString(metadata.source ?? \"\")\n\t\t\t.trim()\n\t\t\t.toLowerCase() === \"observer_summary\"\n\t);\n}\n\nfunction selectMemoryPage(\n\tstore: MemoryStore,\n\toptions: {\n\t\tlimit: number;\n\t\toffset: number;\n\t\tproject?: string;\n\t\tscope?: \"mine\" | \"theirs\";\n\t\tmatcher: (item: Record<string, unknown>) => boolean;\n\t},\n): Record<string, unknown>[] {\n\tconst pageSize = Math.max(options.limit + options.offset + 10, 50);\n\tlet rawOffset = 0;\n\tconst matched: Record<string, unknown>[] = [];\n\n\twhile (matched.length < options.offset + options.limit + 1) {\n\t\tconst page = queryMemoryPage(store, {\n\t\t\tlimit: pageSize,\n\t\t\toffset: rawOffset,\n\t\t\tproject: options.project,\n\t\t\tscope: options.scope,\n\t\t});\n\t\tif (page.length === 0) break;\n\t\tmatched.push(...page.filter(options.matcher));\n\t\tif (page.length < pageSize) break;\n\t\trawOffset += page.length;\n\t}\n\n\treturn matched.slice(options.offset, options.offset + options.limit + 1);\n}\n\nexport function memoryRoutes(getStore: StoreFactory) {\n\tconst app = new Hono();\n\n\t// GET /api/sessions\n\tapp.get(\"/api/sessions\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst limit = queryInt(c.req.query(\"limit\"), 20);\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst rows = d\n\t\t\t\t.select()\n\t\t\t\t.from(schema.sessions)\n\t\t\t\t.orderBy(desc(schema.sessions.started_at))\n\t\t\t\t.limit(limit)\n\t\t\t\t.all();\n\t\t\tconst items = rows.map((row) => ({\n\t\t\t\t...row,\n\t\t\t\tmetadata_json: fromJson(row.metadata_json),\n\t\t\t}));\n\t\t\treturn c.json({ items });\n\t\t}\n\t});\n\n\t// GET /api/projects\n\tapp.get(\"/api/projects\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst rows = d\n\t\t\t\t.selectDistinct({ project: schema.sessions.project })\n\t\t\t\t.from(schema.sessions)\n\t\t\t\t.where(isNotNull(schema.sessions.project))\n\t\t\t\t.all();\n\t\t\tconst projects = [\n\t\t\t\t...new Set(\n\t\t\t\t\trows\n\t\t\t\t\t\t.map((r) => String(r.project ?? \"\").trim())\n\t\t\t\t\t\t.filter((p) => p && !p.toLowerCase().startsWith(\"fatal:\"))\n\t\t\t\t\t\t.map((p) => projectBasename(p))\n\t\t\t\t\t\t.filter(Boolean),\n\t\t\t\t),\n\t\t\t].sort();\n\t\t\treturn c.json({ projects });\n\t\t}\n\t});\n\n\t// GET /api/observations (aliased from /api/memories)\n\tapp.get(\"/api/memories\", (c) => {\n\t\tconst search = new URL(c.req.url).search;\n\t\treturn c.redirect(`/api/observations${search}`, 301);\n\t});\n\n\tapp.get(\"/api/observations\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst limit = Math.max(1, queryInt(c.req.query(\"limit\"), 20));\n\t\t\tconst offset = Math.max(0, queryInt(c.req.query(\"offset\"), 0));\n\t\t\tconst project = c.req.query(\"project\") || undefined;\n\t\t\tconst scope = normalizeScope(c.req.query(\"scope\"));\n\t\t\tconst items = selectMemoryPage(store, {\n\t\t\t\tlimit,\n\t\t\t\toffset,\n\t\t\t\tproject,\n\t\t\t\tscope,\n\t\t\t\tmatcher: (item) => !isSummaryLikeMemory(item),\n\t\t\t});\n\t\t\tconst hasMore = items.length > limit;\n\t\t\tconst result = hasMore ? items.slice(0, limit) : items;\n\t\t\tconst asRecords = result as unknown as Record<string, unknown>[];\n\t\t\tattachSessionFields(store, asRecords);\n\t\t\treturn c.json({\n\t\t\t\titems: asRecords,\n\t\t\t\tpagination: {\n\t\t\t\t\tlimit,\n\t\t\t\t\toffset,\n\t\t\t\t\tnext_offset: hasMore ? offset + result.length : null,\n\t\t\t\t\thas_more: hasMore,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t});\n\n\t// GET /api/summaries\n\tapp.get(\"/api/summaries\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst limit = Math.max(1, queryInt(c.req.query(\"limit\"), 50));\n\t\t\tconst offset = Math.max(0, queryInt(c.req.query(\"offset\"), 0));\n\t\t\tconst project = c.req.query(\"project\") || undefined;\n\t\t\tconst scope = normalizeScope(c.req.query(\"scope\"));\n\t\t\tconst items = selectMemoryPage(store, {\n\t\t\t\tlimit,\n\t\t\t\toffset,\n\t\t\t\tproject,\n\t\t\t\tscope,\n\t\t\t\tmatcher: (item) => isSummaryLikeMemory(item),\n\t\t\t});\n\t\t\tconst hasMore = items.length > limit;\n\t\t\tconst result = hasMore ? items.slice(0, limit) : items;\n\t\t\tconst asRecords = result as unknown as Record<string, unknown>[];\n\t\t\tattachSessionFields(store, asRecords);\n\t\t\treturn c.json({\n\t\t\t\titems: asRecords,\n\t\t\t\tpagination: {\n\t\t\t\t\tlimit,\n\t\t\t\t\toffset,\n\t\t\t\t\tnext_offset: hasMore ? offset + result.length : null,\n\t\t\t\t\thas_more: hasMore,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t});\n\n\t// GET /api/session (aggregate counts)\n\tapp.get(\"/api/session\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst project = c.req.query(\"project\") || null;\n\t\t\tconst count = (sql: string, ...params: unknown[]): number => {\n\t\t\t\tconst row = store.db.prepare(sql).get(...params) as Record<string, unknown> | undefined;\n\t\t\t\treturn Number(row?.total ?? 0);\n\t\t\t};\n\n\t\t\tlet prompts: number;\n\t\t\tlet artifacts: number;\n\t\t\tlet memories: number;\n\t\t\tlet observations: number;\n\t\t\tconst countObservations = (scopeProject?: string) => {\n\t\t\t\tlet offset = 0;\n\t\t\t\tlet total = 0;\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst page = queryMemoryPage(store, {\n\t\t\t\t\t\tlimit: 200,\n\t\t\t\t\t\toffset,\n\t\t\t\t\t\tproject: scopeProject,\n\t\t\t\t\t});\n\t\t\t\t\tif (page.length === 0) break;\n\t\t\t\t\ttotal += page.filter((item) => !isSummaryLikeMemory(item)).length;\n\t\t\t\t\tif (page.length < 200) break;\n\t\t\t\t\toffset += page.length;\n\t\t\t\t}\n\t\t\t\treturn total;\n\t\t\t};\n\t\t\tif (project) {\n\t\t\t\tprompts = count(\"SELECT COUNT(*) AS total FROM user_prompts WHERE project = ?\", project);\n\t\t\t\tartifacts = count(\n\t\t\t\t\t`SELECT COUNT(*) AS total FROM artifacts\n\t\t\t\t\t JOIN sessions ON sessions.id = artifacts.session_id\n\t\t\t\t\t WHERE sessions.project = ?`,\n\t\t\t\t\tproject,\n\t\t\t\t);\n\t\t\t\tmemories = count(\n\t\t\t\t\t`SELECT COUNT(*) AS total FROM memory_items\n\t\t\t\t\t JOIN sessions ON sessions.id = memory_items.session_id\n\t\t\t\t\t WHERE sessions.project = ?`,\n\t\t\t\t\tproject,\n\t\t\t\t);\n\t\t\t\tobservations = countObservations(project);\n\t\t\t} else {\n\t\t\t\tprompts = count(\"SELECT COUNT(*) AS total FROM user_prompts\");\n\t\t\t\tartifacts = count(\"SELECT COUNT(*) AS total FROM artifacts\");\n\t\t\t\tmemories = count(\"SELECT COUNT(*) AS total FROM memory_items\");\n\t\t\t\tobservations = countObservations();\n\t\t\t}\n\t\t\tconst total = prompts + artifacts + memories;\n\t\t\treturn c.json({ total, memories, artifacts, prompts, observations });\n\t\t}\n\t});\n\n\t// GET /api/pack\n\tapp.get(\"/api/pack\", async (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst context = c.req.query(\"context\") || \"\";\n\t\t\tif (!context) {\n\t\t\t\treturn c.json({ error: \"context required\" }, 400);\n\t\t\t}\n\t\t\tconst limit = queryInt(c.req.query(\"limit\"), 10);\n\t\t\tconst tokenBudgetStr = c.req.query(\"token_budget\");\n\t\t\tlet tokenBudget: number | undefined;\n\t\t\tif (tokenBudgetStr) {\n\t\t\t\ttokenBudget = parseStrictInteger(tokenBudgetStr) ?? undefined;\n\t\t\t\tif (tokenBudget === undefined) {\n\t\t\t\t\treturn c.json({ error: \"token_budget must be int\" }, 400);\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst project = c.req.query(\"project\") || undefined;\n\t\t\tconst filters: { project?: string } = {};\n\t\t\tif (project) filters.project = project;\n\t\t\tconst pack = await store.buildMemoryPackAsync(context, limit, tokenBudget ?? null, filters);\n\t\t\treturn c.json(pack);\n\t\t}\n\t});\n\n\t// GET /api/memory\n\tapp.get(\"/api/memory\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst limit = queryInt(c.req.query(\"limit\"), 20);\n\t\t\tconst kind = c.req.query(\"kind\") || undefined;\n\t\t\tconst project = c.req.query(\"project\") || undefined;\n\t\t\tconst filters: Record<string, unknown> = {};\n\t\t\tif (kind) filters.kind = kind;\n\t\t\tif (project) filters.project = project;\n\t\t\tconst items = store.recent(limit, filters);\n\t\t\tconst asRecords = items as unknown as Record<string, unknown>[];\n\t\t\tattachSessionFields(store, asRecords);\n\t\t\treturn c.json({ items: asRecords });\n\t\t}\n\t});\n\n\t// GET /api/artifacts\n\tapp.get(\"/api/artifacts\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst sessionIdStr = c.req.query(\"session_id\");\n\t\t\tif (!sessionIdStr) {\n\t\t\t\treturn c.json({ error: \"session_id required\" }, 400);\n\t\t\t}\n\t\t\tconst sessionId = parseStrictInteger(sessionIdStr);\n\t\t\tif (sessionId == null) {\n\t\t\t\treturn c.json({ error: \"session_id must be int\" }, 400);\n\t\t\t}\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst rows = d\n\t\t\t\t.select()\n\t\t\t\t.from(schema.artifacts)\n\t\t\t\t.where(eq(schema.artifacts.session_id, sessionId))\n\t\t\t\t.all();\n\t\t\treturn c.json({ items: rows });\n\t\t}\n\t});\n\n\t// POST /api/memories/visibility\n\tapp.post(\"/api/memories/visibility\", async (c) => {\n\t\tconst store = getStore();\n\t\tlet body: Record<string, unknown>;\n\t\ttry {\n\t\t\tbody = await c.req.json<Record<string, unknown>>();\n\t\t} catch {\n\t\t\treturn c.json({ error: \"invalid JSON\" }, 400);\n\t\t}\n\t\tconst memoryId = parseStrictInteger(\n\t\t\ttypeof body.memory_id === \"string\" ? body.memory_id : String(body.memory_id ?? \"\"),\n\t\t);\n\t\tif (memoryId == null || memoryId <= 0) {\n\t\t\treturn c.json({ error: \"memory_id must be int\" }, 400);\n\t\t}\n\t\tconst visibility = String(body.visibility ?? \"\").trim();\n\t\tif (visibility !== \"private\" && visibility !== \"shared\") {\n\t\t\treturn c.json({ error: \"visibility must be private or shared\" }, 400);\n\t\t}\n\t\ttry {\n\t\t\tconst item = store.updateMemoryVisibility(memoryId, visibility);\n\t\t\treturn c.json({ item });\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tif (msg.includes(\"not found\")) return c.json({ error: msg }, 404);\n\t\t\tif (msg.includes(\"not owned\")) return c.json({ error: msg }, 403);\n\t\t\treturn c.json({ error: msg }, 400);\n\t\t}\n\t});\n\n\treturn app;\n}\n","/**\n * Observer status route — GET /api/observer-status.\n *\n * Ports Python's viewer_routes/observer_status.py.\n * Returns observer runtime info, credential availability, and queue status.\n */\n\nimport type { ObserverClient } from \"@codemem/core\";\nimport { type MemoryStore, probeAvailableCredentials, type RawEventSweeper } from \"@codemem/core\";\nimport { Hono } from \"hono\";\n\ntype StoreFactory = () => MemoryStore;\n\nexport interface ObserverStatusDeps {\n\tgetStore: StoreFactory;\n\tgetSweeper: () => RawEventSweeper | null;\n\tgetObserver?: () => ObserverClient | null;\n}\n\nfunction normalizeActiveObserver(active: ReturnType<ObserverClient[\"getStatus\"]> | null) {\n\tif (!active) return null;\n\treturn {\n\t\t...active,\n\t\tauth: {\n\t\t\t...active.auth,\n\t\t\tmethod: active.auth.type,\n\t\t\ttoken_present: active.auth.hasToken,\n\t\t},\n\t};\n}\n\nfunction buildFailureImpact(\n\tlatestFailure: Record<string, unknown> | null,\n\tqueueTotals: { pending: number; sessions: number },\n\tauthBackoff: { active: boolean; remainingS: number },\n): string | null {\n\tif (!latestFailure) return null;\n\tif (authBackoff.active) {\n\t\treturn `Queue retries paused for ~${authBackoff.remainingS}s after an observer auth failure.`;\n\t}\n\tif (queueTotals.pending > 0) {\n\t\treturn `${queueTotals.pending} queued raw events across ${queueTotals.sessions} session(s) are waiting on a successful flush.`;\n\t}\n\treturn \"Failed flush batches are pending retry.\";\n}\n\nexport function observerStatusRoutes(deps?: ObserverStatusDeps) {\n\tconst app = new Hono();\n\n\tapp.get(\"/api/observer-status\", (c) => {\n\t\tconst store = deps?.getStore();\n\t\tconst sweeper = deps?.getSweeper();\n\t\tconst observer = deps?.getObserver?.() ?? null;\n\n\t\t// Stub fallback when store doesn't have the required methods (e.g. tests with mock store)\n\t\tif (!store || typeof store.rawEventBacklogTotals !== \"function\") {\n\t\t\treturn c.json({\n\t\t\t\tactive: null,\n\t\t\t\tavailable_credentials: {},\n\t\t\t\tlatest_failure: null,\n\t\t\t\tqueue: {\n\t\t\t\t\tpending: 0,\n\t\t\t\t\tsessions: 0,\n\t\t\t\t\tauth_backoff_active: false,\n\t\t\t\t\tauth_backoff_remaining_s: 0,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\tconst queueTotals = store.rawEventBacklogTotals();\n\t\tconst authBackoff = sweeper?.authBackoffStatus() ?? { active: false, remainingS: 0 };\n\t\tconst latestFailure = store.latestRawEventFlushFailure();\n\t\tconst active = normalizeActiveObserver(observer?.getStatus() ?? null);\n\t\tconst availableCredentials = probeAvailableCredentials();\n\t\tconst shouldShowFailure =\n\t\t\tlatestFailure != null && (authBackoff.active || queueTotals.pending > 0);\n\n\t\tconst failureWithImpact =\n\t\t\tshouldShowFailure && latestFailure\n\t\t\t\t? { ...latestFailure, impact: buildFailureImpact(latestFailure, queueTotals, authBackoff) }\n\t\t\t\t: null;\n\n\t\treturn c.json({\n\t\t\tactive,\n\t\t\tavailable_credentials: availableCredentials,\n\t\t\tlatest_failure: failureWithImpact,\n\t\t\tqueue: {\n\t\t\t\t...queueTotals,\n\t\t\t\tauth_backoff_active: authBackoff.active,\n\t\t\t\tauth_backoff_remaining_s: authBackoff.remainingS,\n\t\t\t},\n\t\t});\n\t});\n\n\treturn app;\n}\n","/**\n * Raw events routes — GET & POST /api/raw-events, GET /api/raw-events/status,\n * POST /api/claude-hooks.\n */\n\nimport { createHash } from \"node:crypto\";\nimport type { MemoryStore, RawEventSweeper } from \"@codemem/core\";\nimport { buildRawEventEnvelopeFromHook, schema, stripPrivateObj } from \"@codemem/core\";\nimport { desc } from \"drizzle-orm\";\nimport { drizzle } from \"drizzle-orm/better-sqlite3\";\nimport { Hono } from \"hono\";\nimport { queryInt } from \"../helpers.js\";\n\ntype StoreFactory = () => MemoryStore;\n\nconst MAX_RAW_EVENTS_BODY_BYTES =\n\tNumber.parseInt(process.env.CODEMEM_RAW_EVENTS_MAX_BODY_BYTES ?? \"\", 10) || 1048576;\n\n/** Keys to check (in priority order) when resolving a session stream id. */\nconst SESSION_ID_KEYS = [\n\t\"session_stream_id\",\n\t\"session_id\",\n\t\"stream_id\",\n\t\"opencode_session_id\",\n] as const;\n\n/**\n * Resolve a session stream id from a payload object.\n * Checks multiple field aliases. Throws on conflicting values.\n */\nfunction resolveSessionStreamId(payload: Record<string, unknown>): string | null {\n\tconst values = new Map<string, string>();\n\tfor (const key of SESSION_ID_KEYS) {\n\t\tconst value = payload[key];\n\t\tif (value == null) continue;\n\t\tif (typeof value !== \"string\") throw new Error(`${key} must be string`);\n\t\tconst text = value.trim();\n\t\tif (text) values.set(key, text);\n\t}\n\tif (values.size === 0) return null;\n\tconst unique = new Set(values.values());\n\tif (unique.size > 1) throw new Error(\"conflicting session id fields\");\n\t// Return the first matching key's value (preserves priority order)\n\tfor (const key of SESSION_ID_KEYS) {\n\t\tconst v = values.get(key);\n\t\tif (v) return v;\n\t}\n\treturn null;\n}\n\n/**\n * Parse and validate a JSON object body, enforcing size limits.\n * Returns the parsed payload or a Hono Response on error.\n */\nasync function parseJsonObjectBody(\n\tc: {\n\t\treq: { header: (name: string) => string | undefined; text: () => Promise<string> };\n\t\tjson: (data: unknown, status?: number) => Response;\n\t},\n\tmaxBytes: number,\n): Promise<Record<string, unknown> | Response> {\n\tconst contentLength = Number.parseInt(c.req.header(\"content-length\") ?? \"0\", 10);\n\tif (Number.isNaN(contentLength) || contentLength < 0) {\n\t\treturn c.json({ error: \"invalid content-length\" }, 400);\n\t}\n\tif (contentLength > maxBytes) {\n\t\treturn c.json({ error: \"payload too large\", max_bytes: maxBytes }, 413);\n\t}\n\tlet raw: string;\n\ttry {\n\t\traw = await c.req.text();\n\t} catch {\n\t\treturn c.json({ error: \"invalid json\" }, 400);\n\t}\n\tif (Buffer.byteLength(raw, \"utf-8\") > maxBytes) {\n\t\treturn c.json({ error: \"payload too large\", max_bytes: maxBytes }, 413);\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = raw ? JSON.parse(raw) : {};\n\t} catch {\n\t\treturn c.json({ error: \"invalid json\" }, 400);\n\t}\n\tif (parsed == null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n\t\treturn c.json({ error: \"payload must be an object\" }, 400);\n\t}\n\treturn parsed as Record<string, unknown>;\n}\n\n/** Nudge the sweeper safely — never crashes the caller. */\nfunction nudgeSweeper(\n\tsweeper: RawEventSweeper | null | undefined,\n\tsessionIds: Iterable<string>,\n\tsource = \"opencode\",\n): void {\n\ttry {\n\t\tfor (const sessionId of sessionIds) {\n\t\t\tsweeper?.nudge(sessionId, source);\n\t\t}\n\t} catch {\n\t\t// never crash the request if sweeper nudge fails\n\t}\n}\n\nexport function rawEventsRoutes(getStore: StoreFactory, sweeper?: RawEventSweeper | null) {\n\tconst app = new Hono();\n\n\t// GET /api/raw-events (compat endpoint for stats panel)\n\tapp.get(\"/api/raw-events\", (c) => {\n\t\tconst store = getStore();\n\t\tconst totals = store.rawEventBacklogTotals();\n\t\treturn c.json(totals);\n\t});\n\n\t// GET /api/raw-events/status\n\tapp.get(\"/api/raw-events/status\", (c) => {\n\t\tconst store = getStore();\n\t\tconst limit = queryInt(c.req.query(\"limit\"), 25);\n\t\tconst d = drizzle(store.db, { schema });\n\t\tconst rows = d\n\t\t\t.select({\n\t\t\t\tsource: schema.rawEventSessions.source,\n\t\t\t\tstream_id: schema.rawEventSessions.stream_id,\n\t\t\t\topencode_session_id: schema.rawEventSessions.opencode_session_id,\n\t\t\t\tcwd: schema.rawEventSessions.cwd,\n\t\t\t\tproject: schema.rawEventSessions.project,\n\t\t\t\tstarted_at: schema.rawEventSessions.started_at,\n\t\t\t\tlast_seen_ts_wall_ms: schema.rawEventSessions.last_seen_ts_wall_ms,\n\t\t\t\tlast_received_event_seq: schema.rawEventSessions.last_received_event_seq,\n\t\t\t\tlast_flushed_event_seq: schema.rawEventSessions.last_flushed_event_seq,\n\t\t\t\tupdated_at: schema.rawEventSessions.updated_at,\n\t\t\t})\n\t\t\t.from(schema.rawEventSessions)\n\t\t\t.orderBy(desc(schema.rawEventSessions.updated_at))\n\t\t\t.limit(limit)\n\t\t\t.all();\n\t\tconst items = rows.map((row) => {\n\t\t\tconst streamId = String(row.stream_id ?? row.opencode_session_id ?? \"\");\n\t\t\treturn {\n\t\t\t\t...row,\n\t\t\t\tsession_stream_id: streamId,\n\t\t\t\tsession_id: streamId,\n\t\t\t};\n\t\t});\n\t\tconst totals = store.rawEventBacklogTotals();\n\t\treturn c.json({\n\t\t\titems,\n\t\t\ttotals,\n\t\t\tingest: {\n\t\t\t\tavailable: true,\n\t\t\t\tmode: \"stream_queue\",\n\t\t\t\tmax_body_bytes: MAX_RAW_EVENTS_BODY_BYTES,\n\t\t\t},\n\t\t});\n\t});\n\n\t// POST /api/raw-events — ingest raw events from plugin\n\tapp.post(\"/api/raw-events\", async (c) => {\n\t\tconst result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);\n\t\tif (result instanceof Response) return result;\n\t\tconst payload = result;\n\n\t\tconst store = getStore();\n\t\ttry {\n\t\t\t// Validate top-level string fields\n\t\t\tconst cwd = payload.cwd;\n\t\t\tif (cwd != null && typeof cwd !== \"string\") {\n\t\t\t\treturn c.json({ error: \"cwd must be string\" }, 400);\n\t\t\t}\n\t\t\tconst project = payload.project;\n\t\t\tif (project != null && typeof project !== \"string\") {\n\t\t\t\treturn c.json({ error: \"project must be string\" }, 400);\n\t\t\t}\n\t\t\tconst startedAt = payload.started_at;\n\t\t\tif (startedAt != null && typeof startedAt !== \"string\") {\n\t\t\t\treturn c.json({ error: \"started_at must be string\" }, 400);\n\t\t\t}\n\n\t\t\t// Determine event list: batch (events array) or single-event payload\n\t\t\tlet items = payload.events;\n\t\t\tif (items == null) {\n\t\t\t\titems = [payload];\n\t\t\t}\n\t\t\tif (!Array.isArray(items)) {\n\t\t\t\treturn c.json({ error: \"events must be a list\" }, 400);\n\t\t\t}\n\n\t\t\t// Resolve default session id from top-level payload\n\t\t\tlet defaultSessionId: string;\n\t\t\ttry {\n\t\t\t\tdefaultSessionId = resolveSessionStreamId(payload) ?? \"\";\n\t\t\t} catch (err) {\n\t\t\t\treturn c.json({ error: (err as Error).message }, 400);\n\t\t\t}\n\t\t\tif (defaultSessionId.startsWith(\"msg_\")) {\n\t\t\t\treturn c.json({ error: \"invalid session id\" }, 400);\n\t\t\t}\n\n\t\t\tlet inserted = 0;\n\t\t\tconst lastSeenBySession = new Map<string, number>();\n\t\t\tconst metaBySession = new Map<string, Record<string, string>>();\n\t\t\tconst sessionIds = new Set<string>();\n\t\t\tconst batchBySession = new Map<string, Record<string, unknown>[]>();\n\n\t\t\tfor (const item of items) {\n\t\t\t\tif (item == null || typeof item !== \"object\" || Array.isArray(item)) {\n\t\t\t\t\treturn c.json({ error: \"event must be an object\" }, 400);\n\t\t\t\t}\n\t\t\t\tconst itemObj = item as Record<string, unknown>;\n\n\t\t\t\tlet itemSessionId: string | null;\n\t\t\t\ttry {\n\t\t\t\t\titemSessionId = resolveSessionStreamId(itemObj);\n\t\t\t\t} catch (err) {\n\t\t\t\t\treturn c.json({ error: (err as Error).message }, 400);\n\t\t\t\t}\n\t\t\t\tconst opencodeSessionId = String(itemSessionId ?? defaultSessionId ?? \"\");\n\t\t\t\tif (!opencodeSessionId) {\n\t\t\t\t\treturn c.json({ error: \"session id required\" }, 400);\n\t\t\t\t}\n\t\t\t\tif (opencodeSessionId.startsWith(\"msg_\")) {\n\t\t\t\t\treturn c.json({ error: \"invalid session id\" }, 400);\n\t\t\t\t}\n\n\t\t\t\tlet eventId = String(itemObj.event_id ?? \"\");\n\t\t\t\tconst eventType = String(itemObj.event_type ?? \"\");\n\t\t\t\tif (!eventType) {\n\t\t\t\t\treturn c.json({ error: \"event_type required\" }, 400);\n\t\t\t\t}\n\n\t\t\t\tconst eventSeqValue = itemObj.event_seq;\n\t\t\t\tif (eventSeqValue != null) {\n\t\t\t\t\tconst parsed = Number(eventSeqValue);\n\t\t\t\t\tif (!Number.isFinite(parsed) || parsed !== Math.floor(parsed)) {\n\t\t\t\t\t\treturn c.json({ error: \"event_seq must be int\" }, 400);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tlet tsWallMs = itemObj.ts_wall_ms;\n\t\t\t\tif (tsWallMs != null) {\n\t\t\t\t\tconst parsed = Number(tsWallMs);\n\t\t\t\t\tif (!Number.isFinite(parsed)) {\n\t\t\t\t\t\treturn c.json({ error: \"ts_wall_ms must be int\" }, 400);\n\t\t\t\t\t}\n\t\t\t\t\ttsWallMs = Math.floor(parsed);\n\t\t\t\t\tconst prev = lastSeenBySession.get(opencodeSessionId) ?? (tsWallMs as number);\n\t\t\t\t\tlastSeenBySession.set(opencodeSessionId, Math.max(prev, tsWallMs as number));\n\t\t\t\t}\n\n\t\t\t\tlet tsMonoMs = itemObj.ts_mono_ms;\n\t\t\t\tif (tsMonoMs != null) {\n\t\t\t\t\tconst parsed = Number(tsMonoMs);\n\t\t\t\t\tif (!Number.isFinite(parsed)) {\n\t\t\t\t\t\treturn c.json({ error: \"ts_mono_ms must be number\" }, 400);\n\t\t\t\t\t}\n\t\t\t\t\ttsMonoMs = parsed;\n\t\t\t\t}\n\n\t\t\t\tlet eventPayload = itemObj.payload;\n\t\t\t\tif (eventPayload == null) eventPayload = {};\n\t\t\t\tif (typeof eventPayload !== \"object\" || Array.isArray(eventPayload)) {\n\t\t\t\t\treturn c.json({ error: \"payload must be an object\" }, 400);\n\t\t\t\t}\n\n\t\t\t\t// Per-item meta fields\n\t\t\t\tconst itemCwd = itemObj.cwd;\n\t\t\t\tif (itemCwd != null && typeof itemCwd !== \"string\") {\n\t\t\t\t\treturn c.json({ error: \"cwd must be string\" }, 400);\n\t\t\t\t}\n\t\t\t\tconst itemProject = itemObj.project;\n\t\t\t\tif (itemProject != null && typeof itemProject !== \"string\") {\n\t\t\t\t\treturn c.json({ error: \"project must be string\" }, 400);\n\t\t\t\t}\n\t\t\t\tconst itemStartedAt = itemObj.started_at;\n\t\t\t\tif (itemStartedAt != null && typeof itemStartedAt !== \"string\") {\n\t\t\t\t\treturn c.json({ error: \"started_at must be string\" }, 400);\n\t\t\t\t}\n\n\t\t\t\t// Sanitize payload\n\t\t\t\teventPayload = stripPrivateObj(eventPayload) as Record<string, unknown>;\n\n\t\t\t\t// Generate stable event_id for legacy senders.\n\t\t\t\t// Python uses json.dumps(sort_keys=True) which recursively sorts all keys.\n\t\t\t\t// We replicate with a recursive key-sorting replacer.\n\t\t\t\tif (!eventId) {\n\t\t\t\t\tconst sortedStringify = (obj: unknown): string =>\n\t\t\t\t\t\tJSON.stringify(obj, (_key, value) => {\n\t\t\t\t\t\t\tif (value != null && typeof value === \"object\" && !Array.isArray(value)) {\n\t\t\t\t\t\t\t\tconst sorted: Record<string, unknown> = {};\n\t\t\t\t\t\t\t\tfor (const k of Object.keys(value as Record<string, unknown>).sort()) {\n\t\t\t\t\t\t\t\t\tsorted[k] = (value as Record<string, unknown>)[k];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn sorted;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t});\n\t\t\t\t\tif (eventSeqValue != null) {\n\t\t\t\t\t\tconst rawId = sortedStringify({\n\t\t\t\t\t\t\ts: eventSeqValue,\n\t\t\t\t\t\t\tt: eventType,\n\t\t\t\t\t\t\tp: eventPayload,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tconst hash = createHash(\"sha256\").update(rawId, \"utf-8\").digest(\"hex\").slice(0, 16);\n\t\t\t\t\t\teventId = `legacy-seq-${eventSeqValue}-${hash}`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst rawId = sortedStringify({\n\t\t\t\t\t\t\tm: tsMonoMs ?? null,\n\t\t\t\t\t\t\tp: eventPayload,\n\t\t\t\t\t\t\tt: eventType,\n\t\t\t\t\t\t\tw: tsWallMs ?? null,\n\t\t\t\t\t\t});\n\t\t\t\t\t\teventId = `legacy-${createHash(\"sha256\").update(rawId, \"utf-8\").digest(\"hex\").slice(0, 16)}`;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst eventEntry: Record<string, unknown> = {\n\t\t\t\t\tevent_id: eventId,\n\t\t\t\t\tevent_type: eventType,\n\t\t\t\t\tpayload: eventPayload,\n\t\t\t\t\tts_wall_ms: tsWallMs ?? null,\n\t\t\t\t\tts_mono_ms: tsMonoMs ?? null,\n\t\t\t\t};\n\n\t\t\t\tsessionIds.add(opencodeSessionId);\n\t\t\t\tconst list = batchBySession.get(opencodeSessionId) ?? [];\n\t\t\t\tlist.push({ ...eventEntry });\n\t\t\t\tbatchBySession.set(opencodeSessionId, list);\n\n\t\t\t\tif (itemCwd || itemProject || itemStartedAt) {\n\t\t\t\t\tconst perSession = metaBySession.get(opencodeSessionId) ?? {};\n\t\t\t\t\tif (itemCwd) perSession.cwd = itemCwd as string;\n\t\t\t\t\tif (itemProject) perSession.project = itemProject as string;\n\t\t\t\t\tif (itemStartedAt) perSession.started_at = itemStartedAt as string;\n\t\t\t\t\tmetaBySession.set(opencodeSessionId, perSession);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Insert events\n\t\t\tif (sessionIds.size === 1) {\n\t\t\t\tconst singleSessionId = sessionIds.values().next().value as string;\n\t\t\t\tconst batch = batchBySession.get(singleSessionId) ?? [];\n\t\t\t\tconst result = store.recordRawEventsBatch(singleSessionId, batch);\n\t\t\t\tinserted = result.inserted;\n\t\t\t} else {\n\t\t\t\tfor (const [sid, sidEvents] of batchBySession) {\n\t\t\t\t\tconst result = store.recordRawEventsBatch(sid, sidEvents);\n\t\t\t\t\tinserted += result.inserted;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Update session metadata\n\t\t\tfor (const metaSessionId of sessionIds) {\n\t\t\t\tconst sessionMeta = metaBySession.get(metaSessionId) ?? {};\n\t\t\t\tconst applyRequestMeta = sessionIds.size === 1 || metaSessionId === defaultSessionId;\n\t\t\t\tstore.updateRawEventSessionMeta({\n\t\t\t\t\topencodeSessionId: metaSessionId,\n\t\t\t\t\tcwd:\n\t\t\t\t\t\tsessionMeta.cwd ?? (applyRequestMeta ? (cwd as string | undefined) : undefined) ?? null,\n\t\t\t\t\tproject:\n\t\t\t\t\t\tsessionMeta.project ??\n\t\t\t\t\t\t(applyRequestMeta ? (project as string | undefined) : undefined) ??\n\t\t\t\t\t\tnull,\n\t\t\t\t\tstartedAt:\n\t\t\t\t\t\tsessionMeta.started_at ??\n\t\t\t\t\t\t(applyRequestMeta ? (startedAt as string | undefined) : undefined) ??\n\t\t\t\t\t\tnull,\n\t\t\t\t\tlastSeenTsWallMs: lastSeenBySession.get(metaSessionId) ?? null,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Note per-session activity for optional debounced auto-flush.\n\t\t\tnudgeSweeper(sweeper, sessionIds);\n\n\t\t\treturn c.json({ inserted, received: (items as unknown[]).length });\n\t\t} catch (err) {\n\t\t\tconst response: Record<string, unknown> = { error: \"internal server error\" };\n\t\t\tif (process.env.CODEMEM_VIEWER_DEBUG === \"1\") {\n\t\t\t\tresponse.detail = (err as Error).message;\n\t\t\t}\n\t\t\treturn c.json(response, 500);\n\t\t}\n\t});\n\n\t// POST /api/claude-hooks — ingest Claude Code hook events\n\tapp.post(\"/api/claude-hooks\", async (c) => {\n\t\tconst result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);\n\t\tif (result instanceof Response) return result;\n\t\tconst payload = result;\n\n\t\t// Map hook payload → raw event envelope\n\t\tconst envelope = buildRawEventEnvelopeFromHook(payload);\n\t\tif (envelope === null) {\n\t\t\t// Unsupported event type or missing required fields — skip gracefully\n\t\t\treturn c.json({ inserted: 0, skipped: 1 });\n\t\t}\n\n\t\tconst store = getStore();\n\t\ttry {\n\t\t\tconst opencodeSessionId = envelope.opencode_session_id;\n\t\t\tconst source = envelope.source;\n\t\t\tconst strippedPayload = stripPrivateObj(envelope.payload) as Record<string, unknown>;\n\n\t\t\tconst inserted = store.recordRawEvent({\n\t\t\t\topencodeSessionId,\n\t\t\t\tsource,\n\t\t\t\teventId: envelope.event_id,\n\t\t\t\teventType: \"claude.hook\",\n\t\t\t\tpayload: strippedPayload,\n\t\t\t\ttsWallMs: envelope.ts_wall_ms,\n\t\t\t});\n\n\t\t\tstore.updateRawEventSessionMeta({\n\t\t\t\topencodeSessionId,\n\t\t\t\tsource,\n\t\t\t\tcwd: envelope.cwd,\n\t\t\t\tproject: envelope.project,\n\t\t\t\tstartedAt: envelope.started_at,\n\t\t\t\tlastSeenTsWallMs: envelope.ts_wall_ms,\n\t\t\t});\n\n\t\t\t// Note activity for optional debounced auto-flush.\n\t\t\tnudgeSweeper(sweeper, [opencodeSessionId], source);\n\n\t\t\treturn c.json({ inserted: inserted ? 1 : 0, skipped: 0 });\n\t\t} catch (err) {\n\t\t\tconst response: Record<string, unknown> = { error: \"internal server error\" };\n\t\t\tif (process.env.CODEMEM_VIEWER_DEBUG === \"1\") {\n\t\t\t\tresponse.detail = (err as Error).message;\n\t\t\t}\n\t\t\treturn c.json(response, 500);\n\t\t}\n\t});\n\n\treturn app;\n}\n","/**\n * Stats routes — GET /api/stats, GET /api/usage.\n *\n * Ports Python's viewer_routes/stats.py.\n */\n\nimport type { MemoryStore } from \"@codemem/core\";\nimport { Hono } from \"hono\";\n\n/**\n * Create stats routes. The store factory is called per-request to get a\n * fresh connection (matching the Python viewer pattern).\n */\nexport function statsRoutes(getStore: () => MemoryStore) {\n\tconst app = new Hono();\n\n\tapp.get(\"/api/stats\", (c) => {\n\t\tconst store = getStore();\n\t\treturn c.json({\n\t\t\t...store.stats(),\n\t\t\tviewer_pid: process.pid,\n\t\t});\n\t});\n\n\tapp.get(\"/api/usage\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst projectFilter = c.req.query(\"project\") || null;\n\t\t\tconst eventsGlobal = store.db\n\t\t\t\t.prepare(\n\t\t\t\t\t`SELECT event,\n\t\t\t\t\t\tSUM(tokens_read) AS total_tokens_read,\n\t\t\t\t\t\tSUM(tokens_written) AS total_tokens_written,\n\t\t\t\t\t\tSUM(tokens_saved) AS total_tokens_saved,\n\t\t\t\t\t\tCOUNT(*) AS count\n\t\t\t\t\t FROM usage_events GROUP BY event ORDER BY event`,\n\t\t\t\t)\n\t\t\t\t.all() as Record<string, unknown>[];\n\t\t\tconst totalsGlobal = store.db\n\t\t\t\t.prepare(\n\t\t\t\t\t`SELECT COALESCE(SUM(tokens_read),0) AS tokens_read,\n\t\t\t\t\t\tCOALESCE(SUM(tokens_written),0) AS tokens_written,\n\t\t\t\t\t\tCOALESCE(SUM(tokens_saved),0) AS tokens_saved,\n\t\t\t\t\t\tCOUNT(*) AS count\n\t\t\t\t\t FROM usage_events`,\n\t\t\t\t)\n\t\t\t\t.get() as Record<string, unknown>;\n\t\t\tlet eventsFiltered: Record<string, unknown>[] | null = null;\n\t\t\tlet totalsFiltered: Record<string, unknown> | null = null;\n\t\t\tif (projectFilter) {\n\t\t\t\teventsFiltered = store.db\n\t\t\t\t\t.prepare(\n\t\t\t\t\t\t`SELECT event,\n\t\t\t\t\t\t\tSUM(tokens_read) AS total_tokens_read,\n\t\t\t\t\t\t\tSUM(tokens_written) AS total_tokens_written,\n\t\t\t\t\t\t\tSUM(tokens_saved) AS total_tokens_saved,\n\t\t\t\t\t\t\tCOUNT(*) AS count\n\t\t\t\t\t\t FROM usage_events\n\t\t\t\t\t\t JOIN sessions ON sessions.id = usage_events.session_id\n\t\t\t\t\t\t WHERE sessions.project = ?\n\t\t\t\t\t\t GROUP BY event ORDER BY event`,\n\t\t\t\t\t)\n\t\t\t\t\t.all(projectFilter) as Record<string, unknown>[];\n\t\t\t\ttotalsFiltered = store.db\n\t\t\t\t\t.prepare(\n\t\t\t\t\t\t`SELECT COALESCE(SUM(tokens_read),0) AS tokens_read,\n\t\t\t\t\t\t\tCOALESCE(SUM(tokens_written),0) AS tokens_written,\n\t\t\t\t\t\t\tCOALESCE(SUM(tokens_saved),0) AS tokens_saved,\n\t\t\t\t\t\t\tCOUNT(*) AS count\n\t\t\t\t\t\t FROM usage_events\n\t\t\t\t\t\t JOIN sessions ON sessions.id = usage_events.session_id\n\t\t\t\t\t\t WHERE sessions.project = ?`,\n\t\t\t\t\t)\n\t\t\t\t\t.get(projectFilter) as Record<string, unknown>;\n\t\t\t}\n\t\t\treturn c.json({\n\t\t\t\tproject: projectFilter,\n\t\t\t\tevents: projectFilter ? eventsFiltered : eventsGlobal,\n\t\t\t\ttotals: projectFilter ? totalsFiltered : totalsGlobal,\n\t\t\t\tevents_global: eventsGlobal,\n\t\t\t\ttotals_global: totalsGlobal,\n\t\t\t\tevents_filtered: eventsFiltered,\n\t\t\t\ttotals_filtered: totalsFiltered,\n\t\t\t\trecent_packs: [],\n\t\t\t});\n\t\t}\n\t});\n\n\treturn app;\n}\n","/**\n * Sync routes — status, peers, actors, attempts, pairing, mutations.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport net from \"node:net\";\nimport { dirname, join } from \"node:path\";\nimport type { MemoryStore, ReplicationOp } from \"@codemem/core\";\nimport {\n\tapplyReplicationOps,\n\tcleanupNonces,\n\tcoordinatorCreateInviteAction,\n\tcoordinatorImportInviteAction,\n\tcoordinatorReviewJoinRequestAction,\n\tcoordinatorStatusSnapshot,\n\tDEFAULT_TIME_WINDOW_S,\n\tensureDeviceIdentity,\n\textractReplicationOps,\n\tfingerprintPublicKey,\n\tgetSyncResetState,\n\tlistCoordinatorJoinRequests,\n\tloadMemorySnapshotPageForPeer,\n\tloadReplicationOpsForPeer,\n\treadCoordinatorSyncConfig,\n\trecordNonce,\n\tschema,\n\tverifySignature,\n} from \"@codemem/core\";\nimport { count, desc, eq, max, ne } from \"drizzle-orm\";\nimport { drizzle } from \"drizzle-orm/better-sqlite3\";\nimport { Hono } from \"hono\";\nimport { queryBool, queryInt, safeJsonList } from \"../helpers.js\";\n\ntype StoreFactory = () => MemoryStore;\ntype SyncRuntimeStatus = {\n\tphase:\n\t\t| \"starting\"\n\t\t| \"running\"\n\t\t| \"stopping\"\n\t\t| \"error\"\n\t\t| \"disabled\"\n\t\t| \"rebootstrapping\"\n\t\t| \"needs_attention\"\n\t\t| null;\n\tdetail?: string | null;\n};\n\nconst SYNC_STALE_AFTER_SECONDS = 10 * 60;\nconst SYNC_PROTOCOL_VERSION = \"2\";\n\nfunction intEnvOr(name: string, fallback: number): number {\n\tconst value = Number.parseInt(process.env[name] ?? \"\", 10);\n\treturn Number.isFinite(value) ? value : fallback;\n}\n\nconst MAX_SYNC_BODY_BYTES = intEnvOr(\"CODEMEM_SYNC_MAX_BODY_BYTES\", 1_048_576);\nconst MAX_SYNC_OPS = intEnvOr(\"CODEMEM_SYNC_MAX_OPS\", 2000);\n\nconst PAIRING_FILTER_HINT =\n\t\"Run this on another device with codemem sync pair --accept '<payload>'. \" +\n\t\"On the accepting device, --include/--exclude control both what it sends and what it accepts from that peer.\";\n\nfunction pathWithQuery(url: string): string {\n\tconst parsed = new URL(url);\n\treturn parsed.search ? `${parsed.pathname}${parsed.search}` : parsed.pathname;\n}\n\nfunction unauthorizedPayload(reason: string): Record<string, string> {\n\tif (process.env.CODEMEM_SYNC_AUTH_DIAGNOSTICS === \"1\") {\n\t\treturn { error: \"unauthorized\", reason };\n\t}\n\treturn { error: \"unauthorized\" };\n}\n\nfunction authorizeSyncRequest(\n\tstore: MemoryStore,\n\trequest: { method: string; url: string; header(name: string): string | undefined },\n\tbody: Buffer,\n): { ok: boolean; reason: string; deviceId: string } {\n\tconst deviceId = (request.header(\"X-Opencode-Device\") ?? \"\").trim();\n\tconst signature = request.header(\"X-Opencode-Signature\") ?? \"\";\n\tconst timestamp = request.header(\"X-Opencode-Timestamp\") ?? \"\";\n\tconst nonce = request.header(\"X-Opencode-Nonce\") ?? \"\";\n\tif (!deviceId || !signature || !timestamp || !nonce) {\n\t\treturn { ok: false, reason: \"missing_headers\", deviceId };\n\t}\n\n\tconst peerRow = store.db\n\t\t.prepare(\n\t\t\t\"SELECT pinned_fingerprint, public_key FROM sync_peers WHERE peer_device_id = ? LIMIT 1\",\n\t\t)\n\t\t.get(deviceId) as { pinned_fingerprint: string | null; public_key: string | null } | undefined;\n\tif (!peerRow) {\n\t\treturn { ok: false, reason: \"unknown_peer\", deviceId };\n\t}\n\n\tconst pinnedFingerprint = String(peerRow.pinned_fingerprint ?? \"\").trim();\n\tconst publicKey = String(peerRow.public_key ?? \"\").trim();\n\tif (!pinnedFingerprint || !publicKey) {\n\t\treturn { ok: false, reason: \"peer_record_incomplete\", deviceId };\n\t}\n\tif (fingerprintPublicKey(publicKey) !== pinnedFingerprint) {\n\t\treturn { ok: false, reason: \"fingerprint_mismatch\", deviceId };\n\t}\n\n\tlet valid = false;\n\ttry {\n\t\tvalid = verifySignature({\n\t\t\tmethod: request.method,\n\t\t\tpathWithQuery: pathWithQuery(request.url),\n\t\t\tbodyBytes: body,\n\t\t\ttimestamp,\n\t\t\tnonce,\n\t\t\tsignature,\n\t\t\tpublicKey,\n\t\t\tdeviceId,\n\t\t});\n\t} catch {\n\t\treturn { ok: false, reason: \"signature_verification_error\", deviceId };\n\t}\n\n\tif (!valid) {\n\t\treturn { ok: false, reason: \"invalid_signature\", deviceId };\n\t}\n\n\tconst createdAt = new Date().toISOString();\n\tif (!recordNonce(store.db, deviceId, nonce, createdAt)) {\n\t\treturn { ok: false, reason: \"nonce_replay\", deviceId };\n\t}\n\n\tconst cutoff = new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1000).toISOString();\n\tcleanupNonces(store.db, cutoff);\n\treturn { ok: true, reason: \"ok\", deviceId };\n}\n\nfunction projectBasename(value: string | null | undefined): string {\n\tconst project = String(value ?? \"\")\n\t\t.trim()\n\t\t.replaceAll(\"\\\\\", \"/\");\n\tif (!project) return \"\";\n\tconst parts = project.split(\"/\").filter(Boolean);\n\treturn parts.length > 0 ? (parts[parts.length - 1] ?? \"\") : \"\";\n}\n\nfunction parseJsonList(value: unknown): string[] {\n\tif (value == null) return [];\n\tif (typeof value === \"string\") {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(value) as unknown;\n\t\t\tif (!Array.isArray(parsed)) return [];\n\t\t\treturn parsed.map((entry) => String(entry ?? \"\").trim()).filter(Boolean);\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\tif (!Array.isArray(value)) return [];\n\treturn value.map((entry) => String(entry ?? \"\").trim()).filter(Boolean);\n}\n\nfunction readPeerProjectFilters(\n\tstore: MemoryStore,\n\tpeerDeviceId: string,\n): { include: string[]; exclude: string[] } {\n\tconst globalConfig = readCoordinatorSyncConfig();\n\tconst row = store.db\n\t\t.prepare(\n\t\t\t\"SELECT projects_include_json, projects_exclude_json FROM sync_peers WHERE peer_device_id = ? LIMIT 1\",\n\t\t)\n\t\t.get(peerDeviceId) as\n\t\t| { projects_include_json: string | null; projects_exclude_json: string | null }\n\t\t| undefined;\n\tif (!row) {\n\t\treturn {\n\t\t\tinclude: globalConfig.syncProjectsInclude,\n\t\t\texclude: globalConfig.syncProjectsExclude,\n\t\t};\n\t}\n\tconst hasOverride = row.projects_include_json != null || row.projects_exclude_json != null;\n\tif (!hasOverride) {\n\t\treturn {\n\t\t\tinclude: globalConfig.syncProjectsInclude,\n\t\t\texclude: globalConfig.syncProjectsExclude,\n\t\t};\n\t}\n\treturn {\n\t\tinclude: parseJsonList(row.projects_include_json),\n\t\texclude: parseJsonList(row.projects_exclude_json),\n\t};\n}\n\nfunction peerClaimedLocalActor(store: MemoryStore, peerDeviceId: string): boolean {\n\tconst row = store.db\n\t\t.prepare(\"SELECT claimed_local_actor FROM sync_peers WHERE peer_device_id = ? LIMIT 1\")\n\t\t.get(peerDeviceId) as { claimed_local_actor: number | null } | undefined;\n\treturn Boolean(row?.claimed_local_actor);\n}\n\nfunction parseOpPayload(op: { payload_json: string | null }): Record<string, unknown> | null {\n\tif (!op.payload_json || !String(op.payload_json).trim()) return null;\n\ttry {\n\t\tconst parsed = JSON.parse(op.payload_json) as unknown;\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n\t\treturn parsed as Record<string, unknown>;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction isSharedVisibility(payload: Record<string, unknown> | null): boolean {\n\tif (!payload) return false;\n\tlet visibility = String(payload.visibility ?? \"\")\n\t\t.trim()\n\t\t.toLowerCase();\n\tconst metadata =\n\t\tpayload.metadata_json &&\n\t\ttypeof payload.metadata_json === \"object\" &&\n\t\t!Array.isArray(payload.metadata_json)\n\t\t\t? (payload.metadata_json as Record<string, unknown>)\n\t\t\t: {};\n\tconst metadataVisibility = String(metadata.visibility ?? \"\")\n\t\t.trim()\n\t\t.toLowerCase();\n\tif (!visibility && metadataVisibility) visibility = metadataVisibility;\n\tif (!visibility) {\n\t\tlet workspaceKind = String(payload.workspace_kind ?? \"\")\n\t\t\t.trim()\n\t\t\t.toLowerCase();\n\t\tlet workspaceId = String(payload.workspace_id ?? \"\")\n\t\t\t.trim()\n\t\t\t.toLowerCase();\n\t\tif (!workspaceKind)\n\t\t\tworkspaceKind = String(metadata.workspace_kind ?? \"\")\n\t\t\t\t.trim()\n\t\t\t\t.toLowerCase();\n\t\tif (!workspaceId)\n\t\t\tworkspaceId = String(metadata.workspace_id ?? \"\")\n\t\t\t\t.trim()\n\t\t\t\t.toLowerCase();\n\t\tif (workspaceKind === \"shared\" || workspaceId.startsWith(\"shared:\")) {\n\t\t\tvisibility = \"shared\";\n\t\t} else {\n\t\t\treturn true;\n\t\t}\n\t}\n\treturn visibility === \"shared\";\n}\n\nfunction projectAllowed(\n\tprojectValue: string | null,\n\tfilters: { include: string[]; exclude: string[] },\n): boolean {\n\tconst value = String(projectValue ?? \"\").trim();\n\tconst valueBase = projectBasename(value);\n\tfor (const blocked of filters.exclude) {\n\t\tif (blocked === value || blocked === valueBase) return false;\n\t}\n\tif (filters.include.length === 0) return true;\n\tfor (const allowed of filters.include) {\n\t\tif (allowed === value || allowed === valueBase) return true;\n\t}\n\treturn false;\n}\n\nfunction filterOpsForPeer(\n\tstore: MemoryStore,\n\tpeerDeviceId: string,\n\tops: ReplicationOp[],\n): { allowed: ReplicationOp[]; skipped: number } {\n\tconst filters = readPeerProjectFilters(store, peerDeviceId);\n\tconst allowPrivate = peerClaimedLocalActor(store, peerDeviceId);\n\tconst allowed: ReplicationOp[] = [];\n\tlet skipped = 0;\n\tfor (const op of ops) {\n\t\tif (op.entity_type !== \"memory_item\") {\n\t\t\tallowed.push(op);\n\t\t\tcontinue;\n\t\t}\n\t\tconst payload = parseOpPayload(op);\n\t\tif (!allowPrivate && !isSharedVisibility(payload)) {\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\tconst project = payload && typeof payload.project === \"string\" ? payload.project : null;\n\t\tif (!projectAllowed(project, filters)) {\n\t\t\tskipped++;\n\t\t\tcontinue;\n\t\t}\n\t\tallowed.push(op);\n\t}\n\treturn { allowed, skipped };\n}\n\n// ---------------------------------------------------------------------------\n// Peer row mapping — deduplicated helper (fix #4)\n// ---------------------------------------------------------------------------\n\n/**\n * Map a raw sync_peers DB row to the API response shape.\n * When showDiag is false, sensitive fields (fingerprint, last_error, addresses)\n * are redacted.\n */\nfunction mapPeerRow(row: Record<string, unknown>, showDiag: boolean): Record<string, unknown> {\n\treturn {\n\t\tpeer_device_id: row.peer_device_id,\n\t\tname: row.name,\n\t\tfingerprint: showDiag ? row.pinned_fingerprint : null,\n\t\tpinned: Boolean(row.pinned_fingerprint),\n\t\taddresses: showDiag ? safeJsonList(row.addresses_json as string | null) : [],\n\t\tlast_seen_at: row.last_seen_at,\n\t\tlast_sync_at: row.last_sync_at,\n\t\tlast_error: showDiag ? row.last_error : null,\n\t\thas_error: Boolean(row.last_error),\n\t\tclaimed_local_actor: Boolean(row.claimed_local_actor),\n\t\tactor_id: row.actor_id ?? null,\n\t\tactor_display_name: row.actor_display_name ?? null,\n\t\tproject_scope: {\n\t\t\tinclude: safeJsonList(row.projects_include_json as string | null),\n\t\t\texclude: safeJsonList(row.projects_exclude_json as string | null),\n\t\t\teffective_include: safeJsonList(row.projects_include_json as string | null),\n\t\t\teffective_exclude: safeJsonList(row.projects_exclude_json as string | null),\n\t\t\tinherits_global: row.projects_include_json == null && row.projects_exclude_json == null,\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Peer status helpers\n// ---------------------------------------------------------------------------\n\nfunction isRecentIso(value: unknown, windowS = SYNC_STALE_AFTER_SECONDS): boolean {\n\tconst raw = String(value ?? \"\").trim();\n\tif (!raw) return false;\n\tconst normalized = raw.replace(\"Z\", \"+00:00\");\n\tconst hasOffset = /(?:Z|[+-]\\d{2}:?\\d{2})$/i.test(raw);\n\tconst ts = new Date(hasOffset ? normalized : `${normalized}+00:00`);\n\tif (Number.isNaN(ts.getTime())) return false;\n\tconst ageS = (Date.now() - ts.getTime()) / 1000;\n\treturn ageS >= 0 && ageS <= windowS;\n}\n\nfunction peerStatus(peer: Record<string, unknown>): Record<string, unknown> {\n\tconst lastSyncAt = peer.last_sync_at;\n\tconst lastPingAt = peer.last_seen_at;\n\tconst hasError = Boolean(peer.has_error);\n\n\tconst syncFresh = isRecentIso(lastSyncAt);\n\tconst pingFresh = isRecentIso(lastPingAt);\n\n\tlet peerState: string;\n\tif (hasError && !(syncFresh || pingFresh)) peerState = \"offline\";\n\telse if (hasError) peerState = \"degraded\";\n\telse if (syncFresh || pingFresh) peerState = \"online\";\n\telse if (lastSyncAt || lastPingAt) peerState = \"stale\";\n\telse peerState = \"unknown\";\n\n\tconst syncStatus = hasError ? \"error\" : syncFresh ? \"ok\" : lastSyncAt ? \"stale\" : \"unknown\";\n\tconst pingStatus = pingFresh ? \"ok\" : lastPingAt ? \"stale\" : \"unknown\";\n\n\treturn {\n\t\tsync_status: syncStatus,\n\t\tping_status: pingStatus,\n\t\tpeer_state: peerState,\n\t\tfresh: syncFresh || pingFresh,\n\t\tlast_sync_at: lastSyncAt,\n\t\tlast_ping_at: lastPingAt,\n\t};\n}\n\nfunction attemptStatus(attempt: Record<string, unknown>): string {\n\tif (attempt.ok) return \"ok\";\n\tif (attempt.error) return \"error\";\n\treturn \"unknown\";\n}\n\nfunction readViewerBinding(dbPath: string): { host: string; port: number } | null {\n\ttry {\n\t\tconst raw = readFileSync(join(dirname(dbPath), \"viewer.pid\"), \"utf8\");\n\t\tconst parsed = JSON.parse(raw) as Partial<{ host: string; port: number }>;\n\t\tif (typeof parsed.host === \"string\" && typeof parsed.port === \"number\") {\n\t\t\treturn { host: parsed.host, port: parsed.port };\n\t\t}\n\t} catch {\n\t\t// ignore missing/malformed pidfile\n\t}\n\treturn null;\n}\n\nasync function portOpen(host: string, port: number): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tconst socket = net.createConnection({ host, port });\n\t\tconst done = (ok: boolean) => {\n\t\t\tsocket.removeAllListeners();\n\t\t\tsocket.destroy();\n\t\t\tresolve(ok);\n\t\t};\n\t\tsocket.setTimeout(300);\n\t\tsocket.once(\"connect\", () => done(true));\n\t\tsocket.once(\"timeout\", () => done(false));\n\t\tsocket.once(\"error\", () => done(false));\n\t});\n}\n\nconst PEERS_QUERY = `\n\tSELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,\n\t p.last_seen_at, p.last_sync_at, p.last_error,\n\t p.projects_include_json, p.projects_exclude_json, p.claimed_local_actor,\n\t p.actor_id, a.display_name AS actor_display_name\n\tFROM sync_peers AS p\n\tLEFT JOIN actors AS a ON a.actor_id = p.actor_id\n\tORDER BY name, peer_device_id\n`;\n\n// ---------------------------------------------------------------------------\n// Route factory\n// ---------------------------------------------------------------------------\n\n/**\n * Peer-to-peer sync protocol routes (/v1/*).\n *\n * These are mounted on the sync listener (0.0.0.0:7337) and are\n * network-accessible. All requests are auth-gated via signature\n * verification so unauthenticated callers are rejected.\n */\nexport function syncProtocolRoutes(getStore: StoreFactory) {\n\tconst app = new Hono();\n\n\t// GET /v1/status (peer sync protocol)\n\tapp.get(\"/v1/status\", (c) => {\n\t\tconst store = getStore();\n\t\tconst auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));\n\t\tif (!auth.ok) return c.json(unauthorizedPayload(auth.reason), 401);\n\n\t\ttry {\n\t\t\tlet device = store.db\n\t\t\t\t.prepare(\"SELECT device_id, fingerprint FROM sync_device LIMIT 1\")\n\t\t\t\t.get() as { device_id: string; fingerprint: string } | undefined;\n\t\t\tif (!device) {\n\t\t\t\tconst [deviceId, fingerprint] = ensureDeviceIdentity(store.db);\n\t\t\t\tdevice = { device_id: deviceId, fingerprint };\n\t\t\t}\n\t\t\tconst syncReset = getSyncResetState(store.db);\n\t\t\treturn c.json({\n\t\t\t\tdevice_id: device.device_id,\n\t\t\t\tprotocol_version: SYNC_PROTOCOL_VERSION,\n\t\t\t\tfingerprint: device.fingerprint,\n\t\t\t\tsync_reset: syncReset,\n\t\t\t});\n\t\t} catch {\n\t\t\treturn c.json({ error: \"internal_error\" }, 500);\n\t\t}\n\t});\n\n\t// GET /v1/ops (peer sync protocol)\n\tapp.get(\"/v1/ops\", (c) => {\n\t\tconst store = getStore();\n\t\tconst auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));\n\t\tif (!auth.ok) return c.json(unauthorizedPayload(auth.reason), 401);\n\t\tconst peerDeviceId = auth.deviceId;\n\n\t\ttry {\n\t\t\tconst since = c.req.query(\"since\") ?? null;\n\t\t\tconst rawLimit = Number.parseInt(c.req.query(\"limit\") ?? \"200\", 10);\n\t\t\tconst rawGeneration = c.req.query(\"generation\");\n\t\t\tconst generation =\n\t\t\t\trawGeneration != null && rawGeneration.trim().length > 0\n\t\t\t\t\t? Number.parseInt(rawGeneration, 10)\n\t\t\t\t\t: null;\n\t\t\tconst snapshotId = c.req.query(\"snapshot_id\") ?? null;\n\t\t\tconst baselineCursor = c.req.query(\"baseline_cursor\") ?? null;\n\t\t\tconst limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 1000)) : 200;\n\t\t\tlet localDeviceId = store.db.prepare(\"SELECT device_id FROM sync_device LIMIT 1\").get() as\n\t\t\t\t| { device_id: string }\n\t\t\t\t| undefined;\n\t\t\tif (!localDeviceId) {\n\t\t\t\tconst [deviceId] = ensureDeviceIdentity(store.db);\n\t\t\t\tlocalDeviceId = { device_id: deviceId };\n\t\t\t}\n\t\t\tconst result = loadReplicationOpsForPeer(store.db, {\n\t\t\t\tsince,\n\t\t\t\tlimit,\n\t\t\t\tdeviceId: localDeviceId.device_id,\n\t\t\t\tgeneration: Number.isFinite(generation) ? generation : null,\n\t\t\t\tsnapshotId,\n\t\t\t\tbaselineCursor,\n\t\t\t});\n\t\t\tif (result.reset_required) {\n\t\t\t\treturn c.json(\n\t\t\t\t\t{\n\t\t\t\t\t\terror: \"reset_required\",\n\t\t\t\t\t\t...result.reset,\n\t\t\t\t\t},\n\t\t\t\t\t409,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst { ops, nextCursor, boundary } = result;\n\t\t\tconst filtered = filterOpsForPeer(store, peerDeviceId, ops);\n\t\t\treturn c.json({\n\t\t\t\treset_required: false,\n\t\t\t\tgeneration: boundary.generation,\n\t\t\t\tsnapshot_id: boundary.snapshot_id,\n\t\t\t\tbaseline_cursor: boundary.baseline_cursor,\n\t\t\t\tretained_floor_cursor: boundary.retained_floor_cursor,\n\t\t\t\tops: filtered.allowed,\n\t\t\t\tnext_cursor: nextCursor,\n\t\t\t\tskipped: filtered.skipped,\n\t\t\t});\n\t\t} catch {\n\t\t\treturn c.json({ error: \"internal_error\" }, 500);\n\t\t}\n\t});\n\n\t// POST /v1/ops (peer sync protocol)\n\tapp.post(\"/v1/ops\", async (c) => {\n\t\tconst store = getStore();\n\t\tconst raw = Buffer.from(await c.req.arrayBuffer());\n\t\tif (raw.length > MAX_SYNC_BODY_BYTES) {\n\t\t\treturn c.json({ error: \"payload_too_large\" }, 413);\n\t\t}\n\n\t\tconst auth = authorizeSyncRequest(store, c.req, raw);\n\t\tif (!auth.ok) return c.json(unauthorizedPayload(auth.reason), 401);\n\t\tconst peerDeviceId = auth.deviceId;\n\n\t\tlet body: Record<string, unknown>;\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(raw.toString(\"utf-8\")) as unknown;\n\t\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n\t\t\t\treturn c.json({ error: \"invalid_json\" }, 400);\n\t\t\t}\n\t\t\tbody = parsed as Record<string, unknown>;\n\t\t} catch {\n\t\t\treturn c.json({ error: \"invalid_json\" }, 400);\n\t\t}\n\n\t\tif (!Array.isArray(body.ops)) {\n\t\t\treturn c.json({ error: \"invalid_ops\" }, 400);\n\t\t}\n\t\tif (body.ops.length > MAX_SYNC_OPS) {\n\t\t\treturn c.json({ error: \"too_many_ops\" }, 413);\n\t\t}\n\n\t\tconst normalizedOps = extractReplicationOps(body);\n\t\tfor (const op of normalizedOps) {\n\t\t\tif (op.device_id !== peerDeviceId || op.clock_device_id !== peerDeviceId) {\n\t\t\t\treturn c.json(\n\t\t\t\t\t{\n\t\t\t\t\t\terror: \"invalid_op_device\",\n\t\t\t\t\t\treason: \"device_id_mismatch\",\n\t\t\t\t\t\top_id: op.op_id,\n\t\t\t\t\t},\n\t\t\t\t\t400,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tlet localDeviceId = store.db.prepare(\"SELECT device_id FROM sync_device LIMIT 1\").get() as\n\t\t\t| { device_id: string }\n\t\t\t| undefined;\n\t\tif (!localDeviceId) {\n\t\t\tconst [deviceId] = ensureDeviceIdentity(store.db);\n\t\t\tlocalDeviceId = { device_id: deviceId };\n\t\t}\n\n\t\tconst filteredInbound = filterOpsForPeer(store, peerDeviceId, normalizedOps);\n\t\tconst result = applyReplicationOps(store.db, filteredInbound.allowed, localDeviceId.device_id);\n\t\treturn c.json({\n\t\t\t...result,\n\t\t\tskipped: result.skipped + filteredInbound.skipped,\n\t\t});\n\t});\n\n\t// GET /v1/bootstrap/memories (peer snapshot bootstrap protocol)\n\tapp.get(\"/v1/bootstrap/memories\", (c) => {\n\t\tconst store = getStore();\n\t\tconst auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));\n\t\tif (!auth.ok) return c.json(unauthorizedPayload(auth.reason), 401);\n\t\tconst peerDeviceId = auth.deviceId;\n\n\t\ttry {\n\t\t\tconst rawLimit = Number.parseInt(c.req.query(\"limit\") ?? \"200\", 10);\n\t\t\tconst rawGeneration = c.req.query(\"generation\");\n\t\t\tconst generation =\n\t\t\t\trawGeneration != null && rawGeneration.trim().length > 0\n\t\t\t\t\t? Number.parseInt(rawGeneration, 10)\n\t\t\t\t\t: null;\n\t\t\tconst snapshotId = c.req.query(\"snapshot_id\") ?? null;\n\t\t\tconst baselineCursor = c.req.query(\"baseline_cursor\") ?? null;\n\t\t\tconst pageToken = c.req.query(\"page_token\") ?? null;\n\t\t\tconst limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 1000)) : 200;\n\t\t\tconst page = loadMemorySnapshotPageForPeer(store.db, {\n\t\t\t\tlimit,\n\t\t\t\tpageToken,\n\t\t\t\tpeerDeviceId,\n\t\t\t\tgeneration: Number.isFinite(generation) ? generation : null,\n\t\t\t\tsnapshotId,\n\t\t\t\tbaselineCursor,\n\t\t\t});\n\t\t\treturn c.json({\n\t\t\t\tgeneration: page.boundary.generation,\n\t\t\t\tsnapshot_id: page.boundary.snapshot_id,\n\t\t\t\tbaseline_cursor: page.boundary.baseline_cursor,\n\t\t\t\tretained_floor_cursor: page.boundary.retained_floor_cursor,\n\t\t\t\titems: page.items,\n\t\t\t\tnext_page_token: page.nextPageToken,\n\t\t\t\thas_more: page.hasMore,\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tif (\n\t\t\t\terr instanceof Error &&\n\t\t\t\t(err.message === \"generation_mismatch\" || err.message === \"boundary_mismatch\")\n\t\t\t) {\n\t\t\t\tconst boundary = getSyncResetState(store.db);\n\t\t\t\treturn c.json(\n\t\t\t\t\t{ error: \"reset_required\", reset_required: true, reason: err.message, ...boundary },\n\t\t\t\t\t409,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn c.json({ error: \"internal_error\" }, 500);\n\t\t}\n\t});\n\n\treturn app;\n}\n\n/**\n * Viewer-facing sync management routes (/api/sync/*).\n *\n * These are mounted on the viewer listener (127.0.0.1:38888) and\n * provide sync status, peer management, and coordinator UI for the\n * local viewer.\n */\nexport function syncRoutes(\n\tgetStore: StoreFactory,\n\tgetSyncRuntimeStatus?: () => SyncRuntimeStatus | null,\n) {\n\tconst app = new Hono();\n\n\t// GET /api/sync/status\n\tapp.get(\"/api/sync/status\", async (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst showDiag = queryBool(c.req.query(\"includeDiagnostics\"));\n\t\t\tconst includeJoinRequests = queryBool(c.req.query(\"includeJoinRequests\"));\n\t\t\tconst project = c.req.query(\"project\") || null;\n\t\t\tconst config = readCoordinatorSyncConfig();\n\t\t\tconst syncReset = getSyncResetState(store.db);\n\n\t\t\tconst d = drizzle(store.db, { schema });\n\n\t\t\tconst deviceRow = d\n\t\t\t\t.select({\n\t\t\t\t\tdevice_id: schema.syncDevice.device_id,\n\t\t\t\t\tfingerprint: schema.syncDevice.fingerprint,\n\t\t\t\t})\n\t\t\t\t.from(schema.syncDevice)\n\t\t\t\t.limit(1)\n\t\t\t\t.get();\n\n\t\t\tconst daemonState = d\n\t\t\t\t.select()\n\t\t\t\t.from(schema.syncDaemonState)\n\t\t\t\t.where(eq(schema.syncDaemonState.id, 1))\n\t\t\t\t.get();\n\n\t\t\tconst peerCountRow = d.select({ total: count() }).from(schema.syncPeers).get();\n\t\t\tlet retentionState:\n\t\t\t\t| {\n\t\t\t\t\t\tlast_run_at?: string | null;\n\t\t\t\t\t\tlast_duration_ms?: number | null;\n\t\t\t\t\t\tlast_deleted_ops?: number | null;\n\t\t\t\t\t\tlast_estimated_bytes_before?: number | null;\n\t\t\t\t\t\tlast_estimated_bytes_after?: number | null;\n\t\t\t\t\t\tretained_floor_cursor?: string | null;\n\t\t\t\t\t\tlast_error?: string | null;\n\t\t\t\t\t\tlast_error_at?: string | null;\n\t\t\t\t }\n\t\t\t\t| undefined;\n\t\t\ttry {\n\t\t\t\tretentionState = d\n\t\t\t\t\t.select()\n\t\t\t\t\t.from(schema.syncRetentionState)\n\t\t\t\t\t.where(eq(schema.syncRetentionState.id, 1))\n\t\t\t\t\t.get();\n\t\t\t} catch {\n\t\t\t\tretentionState = undefined;\n\t\t\t}\n\n\t\t\tconst lastSyncRow = d\n\t\t\t\t.select({ last_sync_at: max(schema.syncPeers.last_sync_at) })\n\t\t\t\t.from(schema.syncPeers)\n\t\t\t\t.get();\n\n\t\t\tconst lastError = daemonState?.last_error as string | null;\n\t\t\tconst lastErrorAt = daemonState?.last_error_at as string | null;\n\t\t\tconst lastOkAt = daemonState?.last_ok_at as string | null;\n\t\t\tconst viewerBinding = readViewerBinding(store.dbPath);\n\t\t\tconst daemonRunning = viewerBinding\n\t\t\t\t? await portOpen(viewerBinding.host, viewerBinding.port)\n\t\t\t\t: false;\n\t\t\tconst daemonDetail = viewerBinding\n\t\t\t\t? daemonRunning\n\t\t\t\t\t? `viewer pidfile at ${viewerBinding.host}:${viewerBinding.port}`\n\t\t\t\t\t: `pidfile present but ${viewerBinding.host}:${viewerBinding.port} is unreachable`\n\t\t\t\t: null;\n\n\t\t\tlet daemonStateValue = \"ok\";\n\t\t\tif (!config.syncEnabled) {\n\t\t\t\tdaemonStateValue = \"disabled\";\n\t\t\t} else if (lastError && (!lastOkAt || String(lastOkAt) < String(lastErrorAt ?? \"\"))) {\n\t\t\t\tdaemonStateValue = \"error\";\n\t\t\t} else if (!daemonRunning) {\n\t\t\t\tdaemonStateValue = \"stopped\";\n\t\t\t}\n\n\t\t\tconst statusPayload: Record<string, unknown> = {\n\t\t\t\tenabled: config.syncEnabled,\n\t\t\t\tinterval_s: config.syncIntervalS,\n\t\t\t\tretention: {\n\t\t\t\t\tenabled: config.syncRetentionEnabled,\n\t\t\t\t\tmax_age_days: config.syncRetentionMaxAgeDays,\n\t\t\t\t\tmax_size_mb: config.syncRetentionMaxSizeMb,\n\t\t\t\t\tretained_floor_cursor:\n\t\t\t\t\t\tsyncReset.retained_floor_cursor ??\n\t\t\t\t\t\t(retentionState?.retained_floor_cursor as string | null) ??\n\t\t\t\t\t\tnull,\n\t\t\t\t\tlast_run_at: (retentionState?.last_run_at as string | null) ?? null,\n\t\t\t\t\tlast_duration_ms:\n\t\t\t\t\t\ttypeof retentionState?.last_duration_ms === \"number\"\n\t\t\t\t\t\t\t? retentionState.last_duration_ms\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\tlast_deleted_ops:\n\t\t\t\t\t\ttypeof retentionState?.last_deleted_ops === \"number\"\n\t\t\t\t\t\t\t? retentionState.last_deleted_ops\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\tlast_estimated_bytes_before:\n\t\t\t\t\t\ttypeof retentionState?.last_estimated_bytes_before === \"number\"\n\t\t\t\t\t\t\t? retentionState.last_estimated_bytes_before\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\tlast_estimated_bytes_after:\n\t\t\t\t\t\ttypeof retentionState?.last_estimated_bytes_after === \"number\"\n\t\t\t\t\t\t\t? retentionState.last_estimated_bytes_after\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\tlast_error: (retentionState?.last_error as string | null) ?? null,\n\t\t\t\t\tlast_error_at: (retentionState?.last_error_at as string | null) ?? null,\n\t\t\t\t},\n\t\t\t\tpeer_count: Number(peerCountRow?.total ?? 0),\n\t\t\t\tlast_sync_at: lastSyncRow?.last_sync_at ?? null,\n\t\t\t\tdaemon_state: daemonStateValue,\n\t\t\t\tdaemon_running: daemonRunning,\n\t\t\t\tdaemon_detail: daemonDetail,\n\t\t\t\tproject_filter_active:\n\t\t\t\t\tconfig.syncProjectsInclude.length > 0 || config.syncProjectsExclude.length > 0,\n\t\t\t\tproject_filter: {\n\t\t\t\t\tinclude: config.syncProjectsInclude,\n\t\t\t\t\texclude: config.syncProjectsExclude,\n\t\t\t\t},\n\t\t\t\tredacted: !showDiag,\n\t\t\t};\n\n\t\t\tif (showDiag) {\n\t\t\t\tstatusPayload.device_id = deviceRow?.device_id ?? null;\n\t\t\t\tstatusPayload.fingerprint = deviceRow?.fingerprint ?? null;\n\t\t\t\tstatusPayload.bind = `${config.syncHost}:${config.syncPort}`;\n\t\t\t\tstatusPayload.daemon_last_error = lastError;\n\t\t\t\tstatusPayload.daemon_last_error_at = lastErrorAt;\n\t\t\t\tstatusPayload.daemon_last_ok_at = lastOkAt;\n\t\t\t}\n\n\t\t\tconst runtimeStatus = getSyncRuntimeStatus?.() ?? null;\n\t\t\tif (runtimeStatus?.phase && runtimeStatus.phase !== \"running\") {\n\t\t\t\tdaemonStateValue = runtimeStatus.phase;\n\t\t\t\tstatusPayload.daemon_state = daemonStateValue;\n\t\t\t\tstatusPayload.daemon_running = runtimeStatus.phase === \"starting\" || daemonRunning;\n\t\t\t\tstatusPayload.daemon_detail = runtimeStatus.detail ?? daemonDetail;\n\t\t\t}\n\n\t\t\t// Build peers list using deduplicated mapPeerRow\n\t\t\tconst peerRows = store.db.prepare(PEERS_QUERY).all() as Record<string, unknown>[];\n\t\t\tconst peersItems = peerRows.map((row) => {\n\t\t\t\tconst peer = mapPeerRow(row, showDiag);\n\t\t\t\tpeer.status = peerStatus(peer);\n\t\t\t\treturn peer;\n\t\t\t});\n\n\t\t\tconst peersMap: Record<string, unknown> = {};\n\t\t\tfor (const peer of peersItems) {\n\t\t\t\tpeersMap[String(peer.peer_device_id)] = peer.status;\n\t\t\t}\n\n\t\t\t// Attempts\n\t\t\tconst attemptRows = d\n\t\t\t\t.select({\n\t\t\t\t\tpeer_device_id: schema.syncAttempts.peer_device_id,\n\t\t\t\t\tok: schema.syncAttempts.ok,\n\t\t\t\t\terror: schema.syncAttempts.error,\n\t\t\t\t\tstarted_at: schema.syncAttempts.started_at,\n\t\t\t\t\tfinished_at: schema.syncAttempts.finished_at,\n\t\t\t\t\tops_in: schema.syncAttempts.ops_in,\n\t\t\t\t\tops_out: schema.syncAttempts.ops_out,\n\t\t\t\t})\n\t\t\t\t.from(schema.syncAttempts)\n\t\t\t\t.orderBy(desc(schema.syncAttempts.finished_at))\n\t\t\t\t.limit(25)\n\t\t\t\t.all();\n\t\t\tconst attemptsItems = attemptRows.map((row) => ({\n\t\t\t\t...row,\n\t\t\t\tstatus: attemptStatus(row),\n\t\t\t\taddress: null,\n\t\t\t}));\n\t\t\tconst latestAttemptError = String(attemptsItems[0]?.error || \"\").trim();\n\n\t\t\tconst statusBlock: Record<string, unknown> = {\n\t\t\t\t...statusPayload,\n\t\t\t\tpeers: peersMap,\n\t\t\t\tpending: 0,\n\t\t\t\tsync: {},\n\t\t\t\tping: {},\n\t\t\t};\n\t\t\tconst legacyDevices = store.claimableLegacyDeviceIds();\n\t\t\tconst sharingReview = store.sharingReviewSummary(project);\n\t\t\tconst coordinator = await coordinatorStatusSnapshot(store, config);\n\t\t\tlet joinRequests: Record<string, unknown>[] = [];\n\t\t\tif (includeJoinRequests && config.syncCoordinatorAdminSecret) {\n\t\t\t\ttry {\n\t\t\t\t\tjoinRequests = await listCoordinatorJoinRequests(config);\n\t\t\t\t} catch {\n\t\t\t\t\tjoinRequests = [];\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (daemonStateValue === \"ok\" && latestAttemptError.startsWith(\"needs_attention:\")) {\n\t\t\t\tdaemonStateValue = \"needs_attention\";\n\t\t\t\tstatusPayload.daemon_state = daemonStateValue;\n\t\t\t\tstatusPayload.daemon_detail = latestAttemptError.replace(/^needs_attention:/, \"\");\n\t\t\t\tstatusBlock.daemon_state = daemonStateValue;\n\t\t\t\tstatusBlock.daemon_detail = latestAttemptError.replace(/^needs_attention:/, \"\");\n\t\t\t}\n\n\t\t\tif (daemonStateValue === \"ok\") {\n\t\t\t\tconst peerStates = new Set(\n\t\t\t\t\tpeersItems.map((peer) =>\n\t\t\t\t\t\tString((peer.status as Record<string, unknown> | undefined)?.peer_state ?? \"\"),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tconst latestFailedRecently = Boolean(\n\t\t\t\t\tattemptsItems[0] &&\n\t\t\t\t\t\tattemptsItems[0].status === \"error\" &&\n\t\t\t\t\t\tisRecentIso(attemptsItems[0].finished_at),\n\t\t\t\t);\n\t\t\t\tconst allOffline =\n\t\t\t\t\tpeersItems.length > 0 &&\n\t\t\t\t\tpeersItems.every(\n\t\t\t\t\t\t(peer) =>\n\t\t\t\t\t\t\tString((peer.status as Record<string, unknown>)?.peer_state ?? \"\") === \"offline\",\n\t\t\t\t\t);\n\t\t\t\tif (latestFailedRecently) {\n\t\t\t\t\tconst hasLivePeer = peerStates.has(\"online\") || peerStates.has(\"degraded\");\n\t\t\t\t\tif (hasLivePeer) daemonStateValue = \"degraded\";\n\t\t\t\t\telse if (allOffline) daemonStateValue = \"offline-peers\";\n\t\t\t\t\telse if (peersItems.length > 0) daemonStateValue = \"stale\";\n\t\t\t\t} else if (peerStates.has(\"degraded\")) {\n\t\t\t\t\tdaemonStateValue = \"degraded\";\n\t\t\t\t} else if (allOffline) {\n\t\t\t\t\tdaemonStateValue = \"offline-peers\";\n\t\t\t\t} else if (peersItems.length > 0 && !peerStates.has(\"online\")) {\n\t\t\t\t\tdaemonStateValue = \"stale\";\n\t\t\t\t}\n\t\t\t\tstatusPayload.daemon_state = daemonStateValue;\n\t\t\t\tstatusBlock.daemon_state = daemonStateValue;\n\t\t\t}\n\n\t\t\tconst responsePayload: Record<string, unknown> = {\n\t\t\t\t...statusPayload,\n\t\t\t\tstatus: statusBlock,\n\t\t\t\tpeers: peersItems,\n\t\t\t\tattempts: attemptsItems.slice(0, 5),\n\t\t\t\tlegacy_devices: legacyDevices,\n\t\t\t\tsharing_review: sharingReview,\n\t\t\t\tcoordinator,\n\t\t\t};\n\t\t\tif (includeJoinRequests) {\n\t\t\t\tresponsePayload.join_requests = joinRequests;\n\t\t\t}\n\t\t\treturn c.json(responsePayload);\n\t\t}\n\t});\n\n\t// GET /api/sync/peers\n\tapp.get(\"/api/sync/peers\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst showDiag = queryBool(c.req.query(\"includeDiagnostics\"));\n\t\t\tconst rows = store.db.prepare(PEERS_QUERY).all() as Record<string, unknown>[];\n\t\t\t// Use deduplicated mapPeerRow helper (fix #4)\n\t\t\tconst peers = rows.map((row) => mapPeerRow(row, showDiag));\n\t\t\treturn c.json({ items: peers, redacted: !showDiag });\n\t\t}\n\t});\n\n\t// GET /api/sync/actors\n\tapp.get(\"/api/sync/actors\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst includeMerged = queryBool(c.req.query(\"includeMerged\"));\n\t\t\tconst query = d.select().from(schema.actors);\n\t\t\tconst rows = includeMerged\n\t\t\t\t? query.orderBy(schema.actors.display_name).all()\n\t\t\t\t: query.where(ne(schema.actors.status, \"merged\")).orderBy(schema.actors.display_name).all();\n\t\t\treturn c.json({ items: rows });\n\t\t}\n\t});\n\n\t// GET /api/sync/attempts\n\tapp.get(\"/api/sync/attempts\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tlet limit = queryInt(c.req.query(\"limit\"), 25);\n\t\t\tif (limit <= 0) return c.json({ error: \"invalid_limit\" }, 400);\n\t\t\tlimit = Math.min(limit, 500);\n\t\t\tconst rows = d\n\t\t\t\t.select({\n\t\t\t\t\tpeer_device_id: schema.syncAttempts.peer_device_id,\n\t\t\t\t\tok: schema.syncAttempts.ok,\n\t\t\t\t\terror: schema.syncAttempts.error,\n\t\t\t\t\tstarted_at: schema.syncAttempts.started_at,\n\t\t\t\t\tfinished_at: schema.syncAttempts.finished_at,\n\t\t\t\t\tops_in: schema.syncAttempts.ops_in,\n\t\t\t\t\tops_out: schema.syncAttempts.ops_out,\n\t\t\t\t})\n\t\t\t\t.from(schema.syncAttempts)\n\t\t\t\t.orderBy(desc(schema.syncAttempts.finished_at))\n\t\t\t\t.limit(limit)\n\t\t\t\t.all();\n\t\t\treturn c.json({ items: rows });\n\t\t}\n\t});\n\n\t// GET /api/sync/pairing — uses ensureDeviceIdentity from core (fix #5 context)\n\tapp.get(\"/api/sync/pairing\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst showDiag = queryBool(c.req.query(\"includeDiagnostics\"));\n\t\t\tif (!showDiag) {\n\t\t\t\treturn c.json({\n\t\t\t\t\tredacted: true,\n\t\t\t\t\tpairing_filter_hint: PAIRING_FILTER_HINT,\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst deviceRow = d\n\t\t\t\t.select({\n\t\t\t\t\tdevice_id: schema.syncDevice.device_id,\n\t\t\t\t\tpublic_key: schema.syncDevice.public_key,\n\t\t\t\t\tfingerprint: schema.syncDevice.fingerprint,\n\t\t\t\t})\n\t\t\t\t.from(schema.syncDevice)\n\t\t\t\t.limit(1)\n\t\t\t\t.get();\n\n\t\t\tlet deviceId: string | undefined;\n\t\t\tlet publicKey: string | undefined;\n\t\t\tlet fingerprint: string | undefined;\n\n\t\t\tif (deviceRow) {\n\t\t\t\tdeviceId = String(deviceRow.device_id);\n\t\t\t\tpublicKey = String(deviceRow.public_key);\n\t\t\t\tfingerprint = String(deviceRow.fingerprint);\n\t\t\t} else {\n\t\t\t\t// Fall back to ensureDeviceIdentity if no row exists\n\t\t\t\ttry {\n\t\t\t\t\tconst [id, fp] = ensureDeviceIdentity(store.db);\n\t\t\t\t\tdeviceId = id;\n\t\t\t\t\tfingerprint = fp;\n\t\t\t\t\tconst newRow = d\n\t\t\t\t\t\t.select({ public_key: schema.syncDevice.public_key })\n\t\t\t\t\t\t.from(schema.syncDevice)\n\t\t\t\t\t\t.where(eq(schema.syncDevice.device_id, id))\n\t\t\t\t\t\t.get();\n\t\t\t\t\tpublicKey = newRow?.public_key ?? \"\";\n\t\t\t\t} catch {\n\t\t\t\t\treturn c.json({ error: \"device identity unavailable\" }, 500);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!deviceId || !fingerprint) {\n\t\t\t\treturn c.json({ error: \"public key missing\" }, 500);\n\t\t\t}\n\n\t\t\treturn c.json({\n\t\t\t\tdevice_id: deviceId,\n\t\t\t\tfingerprint,\n\t\t\t\tpublic_key: publicKey ?? null,\n\t\t\t\tpairing_filter_hint: PAIRING_FILTER_HINT,\n\t\t\t\taddresses: [],\n\t\t\t});\n\t\t}\n\t});\n\n\t// ------------------------------------------------------------------\n\t// POST mutations\n\t// ------------------------------------------------------------------\n\n\t// POST /api/sync/peers/rename\n\tapp.post(\"/api/sync/peers/rename\", async (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst body = await c.req.json<Record<string, unknown>>();\n\t\t\tconst peerDeviceId = String(body.peer_device_id ?? \"\").trim();\n\t\t\tconst name = String(body.name ?? \"\").trim();\n\t\t\tif (!peerDeviceId) return c.json({ error: \"peer_device_id required\" }, 400);\n\t\t\tif (!name) return c.json({ error: \"name required\" }, 400);\n\t\t\tconst exists = d\n\t\t\t\t.select({ peer_device_id: schema.syncPeers.peer_device_id })\n\t\t\t\t.from(schema.syncPeers)\n\t\t\t\t.where(eq(schema.syncPeers.peer_device_id, peerDeviceId))\n\t\t\t\t.get();\n\t\t\tif (!exists) return c.json({ error: \"peer not found\" }, 404);\n\t\t\td.update(schema.syncPeers)\n\t\t\t\t.set({ name })\n\t\t\t\t.where(eq(schema.syncPeers.peer_device_id, peerDeviceId))\n\t\t\t\t.run();\n\t\t\treturn c.json({ ok: true });\n\t\t}\n\t});\n\n\tapp.post(\"/api/sync/invites/create\", async (c) => {\n\t\tlet body: Record<string, unknown>;\n\t\ttry {\n\t\t\tbody = await c.req.json<Record<string, unknown>>();\n\t\t} catch {\n\t\t\treturn c.json({ error: \"invalid json\" }, 400);\n\t\t}\n\t\tconst groupId = String(body.group_id ?? \"\").trim();\n\t\tconst coordinatorUrl = body.coordinator_url == null ? null : String(body.coordinator_url ?? \"\");\n\t\tconst policy = String(body.policy ?? \"auto_admit\").trim();\n\t\tconst ttlHours = Number.parseInt(String(body.ttl_hours ?? 24), 10);\n\t\tif (!groupId) return c.json({ error: \"group_id required\" }, 400);\n\t\tif (body.coordinator_url != null && typeof body.coordinator_url !== \"string\") {\n\t\t\treturn c.json({ error: \"coordinator_url must be string\" }, 400);\n\t\t}\n\t\tif (![\"auto_admit\", \"approval_required\"].includes(policy)) {\n\t\t\treturn c.json({ error: \"policy must be auto_admit or approval_required\" }, 400);\n\t\t}\n\t\tif (!Number.isFinite(ttlHours)) return c.json({ error: \"ttl_hours must be int\" }, 400);\n\t\ttry {\n\t\t\tconst config = readCoordinatorSyncConfig();\n\t\t\tconst result = await coordinatorCreateInviteAction({\n\t\t\t\tgroupId,\n\t\t\t\tcoordinatorUrl,\n\t\t\t\tpolicy,\n\t\t\t\tttlHours,\n\t\t\t\tcreatedBy: null,\n\t\t\t\tremoteUrl: config.syncCoordinatorUrl || null,\n\t\t\t\tadminSecret: config.syncCoordinatorAdminSecret || null,\n\t\t\t});\n\t\t\treturn c.json(result);\n\t\t} catch (error) {\n\t\t\treturn c.json({ error: error instanceof Error ? error.message : String(error) }, 400);\n\t\t}\n\t});\n\n\tapp.post(\"/api/sync/invites/import\", async (c) => {\n\t\tconst store = getStore();\n\t\tlet body: Record<string, unknown>;\n\t\ttry {\n\t\t\tbody = await c.req.json<Record<string, unknown>>();\n\t\t} catch {\n\t\t\treturn c.json({ error: \"invalid json\" }, 400);\n\t\t}\n\t\tconst inviteValue = String(body.invite ?? \"\").trim();\n\t\tif (!inviteValue) return c.json({ error: \"invite required\" }, 400);\n\t\ttry {\n\t\t\tconst result = await coordinatorImportInviteAction({ inviteValue, dbPath: store.dbPath });\n\t\t\treturn c.json(result);\n\t\t} catch (error) {\n\t\t\treturn c.json({ error: error instanceof Error ? error.message : String(error) }, 400);\n\t\t}\n\t});\n\n\tapp.post(\"/api/sync/join-requests/review\", async (c) => {\n\t\tlet body: Record<string, unknown>;\n\t\ttry {\n\t\t\tbody = await c.req.json<Record<string, unknown>>();\n\t\t} catch {\n\t\t\treturn c.json({ error: \"invalid json\" }, 400);\n\t\t}\n\t\tconst requestId = String(body.request_id ?? \"\").trim();\n\t\tconst action = String(body.action ?? \"\").trim();\n\t\tif (!requestId) return c.json({ error: \"request_id required\" }, 400);\n\t\tif (![\"approve\", \"deny\"].includes(action)) {\n\t\t\treturn c.json({ error: \"action must be approve or deny\" }, 400);\n\t\t}\n\t\ttry {\n\t\t\tconst config = readCoordinatorSyncConfig();\n\t\t\tconst result = await coordinatorReviewJoinRequestAction({\n\t\t\t\trequestId,\n\t\t\t\tapprove: action === \"approve\",\n\t\t\t\treviewedBy: null,\n\t\t\t\tremoteUrl: config.syncCoordinatorUrl || null,\n\t\t\t\tadminSecret: config.syncCoordinatorAdminSecret || null,\n\t\t\t});\n\t\t\tif (!result) return c.json({ error: \"join request not found\" }, 404);\n\t\t\treturn c.json({ ok: true, request: result });\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\treturn c.json(\n\t\t\t\t{ error: message },\n\t\t\t\tmessage.includes(\"request_not_found\") || message.includes(\"not found\") ? 404 : 400,\n\t\t\t);\n\t\t}\n\t});\n\n\t// DELETE /api/sync/peers/:peer_device_id\n\tapp.delete(\"/api/sync/peers/:peer_device_id\", (c) => {\n\t\tconst store = getStore();\n\t\t{\n\t\t\tconst d = drizzle(store.db, { schema });\n\t\t\tconst peerDeviceId = c.req.param(\"peer_device_id\")?.trim();\n\t\t\tif (!peerDeviceId) return c.json({ error: \"peer_device_id required\" }, 400);\n\t\t\tconst exists = d\n\t\t\t\t.select({ peer_device_id: schema.syncPeers.peer_device_id })\n\t\t\t\t.from(schema.syncPeers)\n\t\t\t\t.where(eq(schema.syncPeers.peer_device_id, peerDeviceId))\n\t\t\t\t.get();\n\t\t\tif (!exists) return c.json({ error: \"peer not found\" }, 404);\n\t\t\td.delete(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).run();\n\t\t\treturn c.json({ ok: true });\n\t\t}\n\t});\n\n\treturn app;\n}\n","/**\n * @codemem/server — HTTP server (viewer, sync, API).\n *\n * Single HTTP server handling viewer routes and sync daemon.\n * Shares one better-sqlite3 connection between viewer and sync.\n * Embedding inference runs in a worker_thread (lazy-started).\n *\n * Entry: `codemem serve`\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { ObserverClient } from \"@codemem/core\";\nimport { MemoryStore, type RawEventSweeper, resolveDbPath, VERSION } from \"@codemem/core\";\nimport { serveStatic } from \"@hono/node-server/serve-static\";\nimport { Hono } from \"hono\";\nimport { originGuard, preflightHandler } from \"./middleware.js\";\nimport { configRoutes } from \"./routes/config.js\";\nimport { memoryRoutes } from \"./routes/memory.js\";\nimport { observerStatusRoutes } from \"./routes/observer-status.js\";\nimport { rawEventsRoutes } from \"./routes/raw-events.js\";\nimport { statsRoutes } from \"./routes/stats.js\";\nimport { syncProtocolRoutes, syncRoutes } from \"./routes/sync.js\";\n\nexport { VERSION };\n\n/** Shared store instance — SQLite WAL mode handles concurrent reads safely. */\nlet sharedStore: MemoryStore | null = null;\n\n/** Get (or create) the shared store instance. Exported so the sweeper can share it. */\nexport function getStore(): MemoryStore {\n\tif (!sharedStore) {\n\t\tsharedStore = new MemoryStore(resolveDbPath());\n\t}\n\treturn sharedStore;\n}\n\n/** Close the shared store (called on shutdown). */\nexport function closeStore(): void {\n\tsharedStore?.close();\n\tsharedStore = null;\n}\n\n/**\n * Create the Hono app with all viewer routes.\n * Exported for testing — pass a custom store factory to inject test DBs.\n */\nexport interface AppOptions {\n\tstoreFactory?: () => MemoryStore;\n\tsweeper?: RawEventSweeper | null;\n\tobserver?: ObserverClient | null;\n\tgetSyncRuntimeStatus?: () => {\n\t\tphase:\n\t\t\t| \"starting\"\n\t\t\t| \"running\"\n\t\t\t| \"stopping\"\n\t\t\t| \"error\"\n\t\t\t| \"disabled\"\n\t\t\t| \"rebootstrapping\"\n\t\t\t| \"needs_attention\"\n\t\t\t| null;\n\t\tdetail?: string | null;\n\t} | null;\n}\n\nexport function createApp(opts?: AppOptions) {\n\tconst storeFactory = opts?.storeFactory ?? getStore;\n\tconst sweeper = opts?.sweeper ?? null;\n\tconst observer = opts?.observer ?? null;\n\tconst getSyncRuntimeStatus = opts?.getSyncRuntimeStatus ?? (() => null);\n\tconst app = new Hono();\n\n\t// CORS / origin guard\n\tapp.use(\"*\", preflightHandler());\n\tapp.use(\"*\", originGuard());\n\n\t// API routes\n\tapp.route(\"/\", statsRoutes(storeFactory));\n\tapp.route(\"/\", memoryRoutes(storeFactory));\n\tapp.route(\n\t\t\"/\",\n\t\tobserverStatusRoutes({\n\t\t\tgetStore: storeFactory,\n\t\t\tgetSweeper: () => sweeper,\n\t\t\tgetObserver: () => observer,\n\t\t}),\n\t);\n\tapp.route(\"/\", configRoutes({ getSweeper: () => sweeper }));\n\tapp.route(\"/\", rawEventsRoutes(storeFactory, sweeper));\n\tapp.route(\"/\", syncRoutes(storeFactory, getSyncRuntimeStatus));\n\n\t// Static assets — serve under /assets/*\n\t// Resolves to packages/viewer-server/static/ both in dev and when installed from npm.\n\tconst staticRoot =\n\t\tprocess.env.CODEMEM_VIEWER_STATIC_DIR ?? join(import.meta.dirname ?? \".\", \"../static\");\n\n\tapp.use(\n\t\t\"/assets/*\",\n\t\tserveStatic({\n\t\t\troot: staticRoot,\n\t\t\trewriteRequestPath: (path) => path.replace(/^\\/assets/, \"\"),\n\t\t}),\n\t);\n\n\t// SPA — serve index.html for root and all client-side routes\n\tconst indexHtml = readFileSync(join(staticRoot, \"index.html\"), \"utf-8\");\n\tapp.get(\"*\", (c) => {\n\t\tif (c.req.path.startsWith(\"/api/\")) {\n\t\t\treturn c.json({ error: \"not found\" }, 404);\n\t\t}\n\t\treturn c.html(indexHtml);\n\t});\n\n\treturn app;\n}\n\n/**\n * Create the Hono app for peer-to-peer sync protocol routes only.\n *\n * This app is bound to a separate network-accessible listener\n * (default 0.0.0.0:7337) so remote peers can reach the sync\n * endpoints without exposing the viewer UI or local API routes.\n *\n * No CORS/origin guard is applied — sync requests are authenticated\n * via cryptographic signature verification instead.\n */\nexport function createSyncApp(opts?: AppOptions) {\n\tconst storeFactory = opts?.storeFactory ?? getStore;\n\tconst app = new Hono();\n\tapp.route(\"/\", syncProtocolRoutes(storeFactory));\n\n\t// Catch-all — return generic 404 to avoid fingerprinting the server.\n\tapp.all(\"*\", (c) => c.json({ error: \"not found\" }, 404));\n\n\treturn app;\n}\n\n// No auto-start — the CLI's `serve` command owns server startup.\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/routes/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EAKN,KAAK,eAAe,EAKpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAuD5B,MAAM,WAAW,kBAAkB;IAClC,UAAU,CAAC,EAAE,MAAM,eAAe,GAAG,IAAI,CAAC;CAC1C;AAgLD,wBAAgB,YAAY,CAAC,IAAI,GAAE,kBAAuB,8EAkGzD"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/routes/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EAKN,KAAK,eAAe,EAKpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AA6D5B,MAAM,WAAW,kBAAkB;IAClC,UAAU,CAAC,EAAE,MAAM,eAAe,GAAG,IAAI,CAAC;CAC1C;AAgLD,wBAAgB,YAAY,CAAC,IAAI,GAAE,kBAAuB,8EAkGzD"}
@@ -5,7 +5,7 @@ import type { MemoryStore } from "@codemem/core";
5
5
  import { Hono } from "hono";
6
6
  type StoreFactory = () => MemoryStore;
7
7
  type SyncRuntimeStatus = {
8
- phase: "starting" | "running" | "stopping" | "error" | "disabled" | null;
8
+ phase: "starting" | "running" | "stopping" | "error" | "disabled" | "rebootstrapping" | "needs_attention" | null;
9
9
  detail?: string | null;
10
10
  };
11
11
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/routes/sync.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,eAAe,CAAC;AAqBhE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,KAAK,YAAY,GAAG,MAAM,WAAW,CAAC;AACtC,KAAK,iBAAiB,GAAG;IACxB,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,IAAI,CAAC;IACzE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAmXF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,YAAY,8EA0HxD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACzB,QAAQ,EAAE,YAAY,EACtB,oBAAoB,CAAC,EAAE,MAAM,iBAAiB,GAAG,IAAI,8EA0brD"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/routes/sync.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,eAAe,CAAC;AAuBhE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,KAAK,YAAY,GAAG,MAAM,WAAW,CAAC;AACtC,KAAK,iBAAiB,GAAG;IACxB,KAAK,EACF,UAAU,GACV,SAAS,GACT,UAAU,GACV,OAAO,GACP,UAAU,GACV,iBAAiB,GACjB,iBAAiB,GACjB,IAAI,CAAC;IACR,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAmXF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,YAAY,8EAsMxD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACzB,QAAQ,EAAE,YAAY,EACtB,oBAAoB,CAAC,EAAE,MAAM,iBAAiB,GAAG,IAAI,8EAqfrD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemem/server",
3
- "version": "0.20.9",
3
+ "version": "0.21.0",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "exports": {
@@ -18,7 +18,7 @@
18
18
  "@hono/node-server": "^1.14.3",
19
19
  "drizzle-orm": "^0.45.1",
20
20
  "hono": "^4.7.10",
21
- "@codemem/core": "^0.20.9"
21
+ "@codemem/core": "^0.21.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/better-sqlite3": "^7.6.13",
package/static/app.js CHANGED
@@ -1414,7 +1414,7 @@
1414
1414
  const reductionPercent = parsePercentValue(reductionLabel);
1415
1415
  const tagCoverage = Number(dbStats.tags_coverage || 0);
1416
1416
  const syncState = String(syncStatus.daemon_state || "unknown");
1417
- const syncStateLabel = syncState === "offline-peers" ? "Offline peers" : titleCase(syncState);
1417
+ const syncStateLabel = syncState === "offline-peers" ? "Offline peers" : syncState === "needs_attention" ? "Needs attention" : syncState === "rebootstrapping" ? "Rebootstrapping" : titleCase(syncState);
1418
1418
  const peerCount = Array.isArray(state.lastSyncPeers) ? state.lastSyncPeers.length : 0;
1419
1419
  const syncDisabled = syncState === "disabled" || syncStatus.enabled === false;
1420
1420
  const syncOfflinePeers = syncState === "offline-peers";
@@ -1452,6 +1452,9 @@
1452
1452
  if (syncState === "error") {
1453
1453
  riskScore += 36;
1454
1454
  drivers.push("sync daemon reports errors");
1455
+ } else if (syncState === "needs_attention") {
1456
+ riskScore += 40;
1457
+ drivers.push("sync needs manual attention");
1455
1458
  } else if (syncState === "stopped") {
1456
1459
  riskScore += 22;
1457
1460
  drivers.push("sync daemon stopped");
@@ -1926,7 +1929,12 @@
1926
1929
  const pending = Number(status.pending || 0);
1927
1930
  const daemonDetail = String(status.daemon_detail || "");
1928
1931
  const daemonState = String(status.daemon_state || "unknown");
1929
- const daemonStateLabel = daemonState === "offline-peers" ? "Offline peers" : titleCase(daemonState);
1932
+ const retention = status.retention || {};
1933
+ const retentionEnabled = retention.enabled === true;
1934
+ const retentionDeleted = Number(retention.last_deleted_ops || 0);
1935
+ const retentionLastRunAt = retention.last_run_at || null;
1936
+ const retentionLastError = String(retention.last_error || "");
1937
+ const daemonStateLabel = daemonState === "offline-peers" ? "Offline peers" : daemonState === "needs_attention" ? "Needs attention" : daemonState === "rebootstrapping" ? "Rebootstrapping" : titleCase(daemonState);
1930
1938
  const syncDisabled = daemonState === "disabled" || status.enabled === false;
1931
1939
  const peerCount = Object.keys(peers).length;
1932
1940
  const syncNoPeers = !syncDisabled && peerCount === 0;
@@ -1938,6 +1946,8 @@
1938
1946
  ];
1939
1947
  if (daemonState === "offline-peers") parts.push("All peers are currently offline; sync will resume automatically");
1940
1948
  if (daemonDetail && daemonState === "stopped") parts.push(`Detail: ${daemonDetail}`);
1949
+ if (daemonDetail && (daemonState === "needs_attention" || daemonState === "rebootstrapping")) parts.push(`Detail: ${daemonDetail}`);
1950
+ if (retentionEnabled) parts.push(retentionLastRunAt ? `Retention last ran ${formatAgeShort(secondsSince(retentionLastRunAt))} ago (approx oldest-first)` : "Retention enabled");
1941
1951
  syncMeta.textContent = parts.join(" · ");
1942
1952
  }
1943
1953
  (syncDisabled ? [
@@ -1990,6 +2000,10 @@
1990
2000
  {
1991
2001
  label: "Last ping",
1992
2002
  value: lastPing ? `${formatAgeShort(secondsSince(lastPing))} ago` : "never"
2003
+ },
2004
+ {
2005
+ label: "Retention",
2006
+ value: retentionEnabled ? retentionLastRunAt ? `${retentionDeleted.toLocaleString()} ops last run (approx)` : "Enabled" : "Disabled"
1993
2007
  }
1994
2008
  ]).forEach((item) => {
1995
2009
  const block = el("div", "stat");
@@ -2019,6 +2033,13 @@
2019
2033
  block.appendChild(content);
2020
2034
  syncStatusGrid.appendChild(block);
2021
2035
  }
2036
+ if (!syncDisabled && retentionEnabled && retentionLastError) {
2037
+ const block = el("div", "stat");
2038
+ const content = el("div", "stat-content");
2039
+ content.append(el("div", "value", "Retention"), el("div", "label", retentionLastError));
2040
+ block.appendChild(content);
2041
+ syncStatusGrid.appendChild(block);
2042
+ }
2022
2043
  const actions = [];
2023
2044
  if (syncNoPeers) {} else if (daemonState === "offline-peers") {} else if (daemonState === "stopped") {
2024
2045
  actions.push({
@@ -2029,7 +2050,15 @@
2029
2050
  label: "Then run one immediate sync pass.",
2030
2051
  command: "codemem sync once"
2031
2052
  });
2032
- } else if (syncError || pingError || daemonState === "error") {
2053
+ } else if (daemonState === "needs_attention") actions.push({
2054
+ label: "Sync needs manual attention before reset can continue.",
2055
+ command: "codemem sync doctor"
2056
+ });
2057
+ else if (daemonState === "rebootstrapping") actions.push({
2058
+ label: "Sync is rebuilding state in the background.",
2059
+ command: "codemem sync status"
2060
+ });
2061
+ else if (syncError || pingError || daemonState === "error") {
2033
2062
  actions.push({
2034
2063
  label: "Sync reports errors. Restart now.",
2035
2064
  command: "codemem sync restart && codemem sync once"