@lightupai/polaris 0.0.105 → 0.0.109
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/hooks/statusline.sh +41 -3
- package/package.json +1 -1
- package/src/cli/cli.ts +12 -4
- package/src/client/client.ts +43 -0
- package/src/daemon/daemon.ts +116 -8
package/hooks/statusline.sh
CHANGED
|
@@ -24,6 +24,44 @@ if [ -n "$CC_SESSION_ID" ]; then
|
|
|
24
24
|
CONNECTED=$(echo "$STATUS" | jq -r '.connected' 2>/dev/null || echo "false")
|
|
25
25
|
fi
|
|
26
26
|
|
|
27
|
+
# --- Update check (cached, non-blocking) ---
|
|
28
|
+
# Reads installed version from the MCP package, caches the latest npm version
|
|
29
|
+
# for up to 1 hour. Background curl refresh never blocks the status line.
|
|
30
|
+
UPDATE_MSG=""
|
|
31
|
+
INSTALLED_PKG="$HOME/.polaris/mcp/node_modules/@lightupai/polaris/package.json"
|
|
32
|
+
UPDATE_CACHE="$HOME/.polaris/update-check.json"
|
|
33
|
+
|
|
34
|
+
if [ -f "$INSTALLED_PKG" ]; then
|
|
35
|
+
INSTALLED_VERSION=$(jq -r '.version // ""' "$INSTALLED_PKG" 2>/dev/null || echo "")
|
|
36
|
+
|
|
37
|
+
CACHED_LATEST=""
|
|
38
|
+
if [ -f "$UPDATE_CACHE" ]; then
|
|
39
|
+
CACHED_LATEST=$(jq -r '.latest // ""' "$UPDATE_CACHE" 2>/dev/null || echo "")
|
|
40
|
+
CACHE_TS=$(jq -r '.checked // 0' "$UPDATE_CACHE" 2>/dev/null || echo "0")
|
|
41
|
+
NOW=$(date +%s 2>/dev/null || echo "0")
|
|
42
|
+
CACHE_AGE=$(( NOW - CACHE_TS )) 2>/dev/null || CACHE_AGE=9999
|
|
43
|
+
STALE=false
|
|
44
|
+
[ "$CACHE_AGE" -gt 3600 ] 2>/dev/null && STALE=true
|
|
45
|
+
else
|
|
46
|
+
STALE=true
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if [ "$STALE" = "true" ]; then
|
|
50
|
+
# Refresh in background — never blocks the status line
|
|
51
|
+
(
|
|
52
|
+
LATEST_V=$(curl -sf --max-time 5 "https://registry.npmjs.org/@lightupai/polaris/latest" 2>/dev/null \
|
|
53
|
+
| jq -r '.version // ""' 2>/dev/null)
|
|
54
|
+
if [ -n "$LATEST_V" ]; then
|
|
55
|
+
printf '{"latest":"%s","checked":%s}\n' "$LATEST_V" "$(date +%s)" > "$UPDATE_CACHE"
|
|
56
|
+
fi
|
|
57
|
+
) &
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [ -n "$CACHED_LATEST" ] && [ -n "$INSTALLED_VERSION" ] && [ "$CACHED_LATEST" != "$INSTALLED_VERSION" ]; then
|
|
61
|
+
UPDATE_MSG=" [update available: v${CACHED_LATEST}]"
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
|
|
27
65
|
if [ "$CONNECTED" = "true" ]; then
|
|
28
66
|
PROJECT=$(echo "$STATUS" | jq -r '.project' 2>/dev/null)
|
|
29
67
|
SESSION=$(echo "$STATUS" | jq -r '.session' 2>/dev/null)
|
|
@@ -35,10 +73,10 @@ if [ "$CONNECTED" = "true" ]; then
|
|
|
35
73
|
ACCOUNT_TAG="[${ACCOUNT}] "
|
|
36
74
|
fi
|
|
37
75
|
if [ -n "$SLACK" ]; then
|
|
38
|
-
echo "polaris: ${ACCOUNT_TAG}${PROJECT}/${SESSION} (${USER}) #${SLACK}"
|
|
76
|
+
echo "polaris: ${ACCOUNT_TAG}${PROJECT}/${SESSION} (${USER}) #${SLACK}${UPDATE_MSG}"
|
|
39
77
|
else
|
|
40
|
-
echo "polaris: ${ACCOUNT_TAG}${PROJECT}/${SESSION} (${USER})"
|
|
78
|
+
echo "polaris: ${ACCOUNT_TAG}${PROJECT}/${SESSION} (${USER})${UPDATE_MSG}"
|
|
41
79
|
fi
|
|
42
80
|
else
|
|
43
|
-
echo "polaris: not connected"
|
|
81
|
+
echo "polaris: not connected${UPDATE_MSG}"
|
|
44
82
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightupai/polaris",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.109",
|
|
4
4
|
"description": "Record and stream Claude Code sessions to Slack. Like Gong for AI coding — capture every prompt, response, and tool call. Multiplayer collaboration across AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
package/src/cli/cli.ts
CHANGED
|
@@ -428,8 +428,8 @@ async function install(participantId?: string) {
|
|
|
428
428
|
const skillContent = `---
|
|
429
429
|
name: polaris
|
|
430
430
|
description: Connect to a Polaris multiplayer collaboration session
|
|
431
|
-
allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename polaris_rename_session
|
|
432
|
-
argument-hint: [join #channel [session-name] | rename <new-name> | rename-session <name> | disconnect | (no args for status)]
|
|
431
|
+
allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename polaris_rename_session polaris_update
|
|
432
|
+
argument-hint: [join #channel [session-name] | rename <new-name> | rename-session <name> | disconnect | update | (no args for status)]
|
|
433
433
|
---
|
|
434
434
|
|
|
435
435
|
## Polaris — Multiplayer Collaboration
|
|
@@ -453,6 +453,10 @@ Based on the arguments provided, do ONE of the following:
|
|
|
453
453
|
1. Call \`polaris_rename_session\` with the new name
|
|
454
454
|
2. Report the result
|
|
455
455
|
|
|
456
|
+
**\`/polaris update\`** — Update Polaris to the latest version:
|
|
457
|
+
1. Call \`polaris_update\`
|
|
458
|
+
2. Report what changed and remind the user to restart Claude Code if the MCP server was updated
|
|
459
|
+
|
|
456
460
|
**\`/polaris disconnect\`** — Disconnect:
|
|
457
461
|
1. Call \`polaris_disconnect\`
|
|
458
462
|
2. Confirm disconnection
|
|
@@ -571,8 +575,8 @@ async function login(appUrl: string, profileName?: string, loginHint?: string) {
|
|
|
571
575
|
const skillContent = `---
|
|
572
576
|
name: polaris
|
|
573
577
|
description: Connect to a Polaris multiplayer collaboration session
|
|
574
|
-
allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename polaris_rename_session
|
|
575
|
-
argument-hint: [join #channel [session-name] | rename <new-name> | rename-session <name> | disconnect | (no args for status)]
|
|
578
|
+
allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename polaris_rename_session polaris_update
|
|
579
|
+
argument-hint: [join #channel [session-name] | rename <new-name> | rename-session <name> | disconnect | update | (no args for status)]
|
|
576
580
|
---
|
|
577
581
|
|
|
578
582
|
## Polaris — Multiplayer Collaboration
|
|
@@ -596,6 +600,10 @@ Based on the arguments provided, do ONE of the following:
|
|
|
596
600
|
1. Call \`polaris_rename_session\` with the new name
|
|
597
601
|
2. Report the result
|
|
598
602
|
|
|
603
|
+
**\`/polaris update\`** — Update Polaris to the latest version:
|
|
604
|
+
1. Call \`polaris_update\`
|
|
605
|
+
2. Report what changed and remind the user to restart Claude Code if the MCP server was updated
|
|
606
|
+
|
|
599
607
|
**\`/polaris disconnect\`** — Disconnect:
|
|
600
608
|
1. Call \`polaris_disconnect\`
|
|
601
609
|
2. Confirm disconnection
|
package/src/client/client.ts
CHANGED
|
@@ -195,6 +195,14 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
195
195
|
properties: {},
|
|
196
196
|
},
|
|
197
197
|
},
|
|
198
|
+
{
|
|
199
|
+
name: "polaris_update",
|
|
200
|
+
description: "Update Polaris to the latest version. Runs npm install -g then polaris install to refresh hooks and the MCP server.",
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: "object" as const,
|
|
203
|
+
properties: {},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
198
206
|
{
|
|
199
207
|
name: "polaris_backfill",
|
|
200
208
|
description: "Recover lost events from local daemon logs. Use when events were lost due to disconnection or API downtime.",
|
|
@@ -466,6 +474,41 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
466
474
|
}
|
|
467
475
|
}
|
|
468
476
|
|
|
477
|
+
if (name === "polaris_update") {
|
|
478
|
+
const installedPkgPath = `${homedir()}/.polaris/mcp/node_modules/@lightupai/polaris/package.json`;
|
|
479
|
+
let currentVersion = "";
|
|
480
|
+
try {
|
|
481
|
+
const pkg = JSON.parse(await readFile(installedPkgPath, "utf-8")) as { version: string };
|
|
482
|
+
currentVersion = pkg.version ?? "";
|
|
483
|
+
} catch { /* package not installed yet */ }
|
|
484
|
+
|
|
485
|
+
let latestVersion = "";
|
|
486
|
+
try {
|
|
487
|
+
const res = await fetch("https://registry.npmjs.org/@lightupai/polaris/latest", { signal: AbortSignal.timeout(5000) });
|
|
488
|
+
if (res.ok) ({ version: latestVersion } = await res.json() as { version: string });
|
|
489
|
+
} catch { /* network unavailable */ }
|
|
490
|
+
|
|
491
|
+
if (!latestVersion) {
|
|
492
|
+
return { content: [{ type: "text", text: "Could not reach npm to check for updates. Check your network connection." }] };
|
|
493
|
+
}
|
|
494
|
+
if (latestVersion === currentVersion) {
|
|
495
|
+
return { content: [{ type: "text", text: `Polaris is already up to date (v${currentVersion}).` }] };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Update global binary
|
|
499
|
+
const npmResult = Bun.spawnSync(["npm", "install", "-g", "@lightupai/polaris@latest"], { stdout: "pipe", stderr: "pipe" });
|
|
500
|
+
if (npmResult.exitCode !== 0) {
|
|
501
|
+
return { content: [{ type: "text", text: `Update failed:\n${npmResult.stderr.toString().trim()}` }] };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Refresh hooks, MCP package, and daemon registration
|
|
505
|
+
const installResult = Bun.spawnSync(["polaris", "install"], { stdout: "pipe", stderr: "pipe" });
|
|
506
|
+
const msg = installResult.exitCode === 0
|
|
507
|
+
? `Updated Polaris from v${currentVersion} to v${latestVersion}.\n\nRestart Claude Code for the new MCP server to take effect.`
|
|
508
|
+
: `Updated global binary to v${latestVersion}, but \`polaris install\` failed — run it manually.\n${installResult.stderr.toString().trim()}`;
|
|
509
|
+
return { content: [{ type: "text", text: msg }] };
|
|
510
|
+
}
|
|
511
|
+
|
|
469
512
|
throw new Error(`Unknown tool: ${name}`);
|
|
470
513
|
});
|
|
471
514
|
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -328,6 +328,93 @@ async function logSessionChange(action: "connect" | "disconnect" | "ws-drop", cc
|
|
|
328
328
|
} catch { /* best-effort */ }
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
// --- Session persistence ---
|
|
332
|
+
//
|
|
333
|
+
// The daemon writes its session map to ~/.polaris/sessions/daemon-state.json
|
|
334
|
+
// after every mutation, and reloads it on startup. This makes daemon restarts
|
|
335
|
+
// (e.g. during `polaris update`) invisible to connected sessions: the new daemon
|
|
336
|
+
// loads the map and re-opens cloud WebSocket connections before serving requests.
|
|
337
|
+
//
|
|
338
|
+
// Persistence is only active when the daemon is run as main (import.meta.main).
|
|
339
|
+
// Tests that call startDaemon() in-process never enable it, so developer state
|
|
340
|
+
// is never overwritten by the test suite.
|
|
341
|
+
|
|
342
|
+
const SESSIONS_DIR = join(homedir(), ".polaris", "sessions");
|
|
343
|
+
const DAEMON_STATE_FILE = join(SESSIONS_DIR, "daemon-state.json");
|
|
344
|
+
let persistenceEnabled = false;
|
|
345
|
+
|
|
346
|
+
interface PersistedSession {
|
|
347
|
+
ccSessionId: string;
|
|
348
|
+
project: string;
|
|
349
|
+
session: string;
|
|
350
|
+
user: string;
|
|
351
|
+
agent: string;
|
|
352
|
+
profile: string;
|
|
353
|
+
slackChannel?: string;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function persistSessions(): Promise<void> {
|
|
357
|
+
if (!persistenceEnabled) return;
|
|
358
|
+
try {
|
|
359
|
+
await mkdir(SESSIONS_DIR, { recursive: true });
|
|
360
|
+
const active: PersistedSession[] = [];
|
|
361
|
+
for (const m of sessions.values()) {
|
|
362
|
+
if (!m.project) continue; // skip empty stubs from /register
|
|
363
|
+
active.push({
|
|
364
|
+
ccSessionId: m.ccSessionId,
|
|
365
|
+
project: m.project,
|
|
366
|
+
session: m.session,
|
|
367
|
+
user: m.user,
|
|
368
|
+
agent: m.agent,
|
|
369
|
+
profile: m.profile,
|
|
370
|
+
...(m.slackChannel ? { slackChannel: m.slackChannel } : {}),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
await writeFile(DAEMON_STATE_FILE, JSON.stringify({ sessions: active }));
|
|
374
|
+
} catch { /* best-effort */ }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function loadPersistedSessions(): Promise<void> {
|
|
378
|
+
try {
|
|
379
|
+
const data = JSON.parse(await readFile(DAEMON_STATE_FILE, "utf-8")) as { sessions: PersistedSession[] };
|
|
380
|
+
if (!Array.isArray(data.sessions)) return;
|
|
381
|
+
|
|
382
|
+
// Populate the sessions map
|
|
383
|
+
for (const s of data.sessions) {
|
|
384
|
+
if (!s.ccSessionId || !s.project || !s.session) continue;
|
|
385
|
+
sessions.set(s.ccSessionId, {
|
|
386
|
+
ccSessionId: s.ccSessionId,
|
|
387
|
+
project: s.project,
|
|
388
|
+
session: s.session,
|
|
389
|
+
user: s.user,
|
|
390
|
+
agent: s.agent,
|
|
391
|
+
profile: s.profile || "",
|
|
392
|
+
slackChannel: s.slackChannel,
|
|
393
|
+
ws: null,
|
|
394
|
+
pendingMapping: false, // aliases are already known — no claiming needed
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Open one cloud WebSocket per (project, session) pair; alias entries share it
|
|
399
|
+
const primaryForPair = new Map<string, SessionMapping>();
|
|
400
|
+
for (const mapping of sessions.values()) {
|
|
401
|
+
if (!mapping.project) continue;
|
|
402
|
+
const key = `${mapping.project}/${mapping.session}`;
|
|
403
|
+
if (!primaryForPair.has(key)) {
|
|
404
|
+
primaryForPair.set(key, mapping);
|
|
405
|
+
connectCloudWs(mapping);
|
|
406
|
+
} else {
|
|
407
|
+
mapping.ws = primaryForPair.get(key)!.ws;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const count = Array.from(sessions.values()).filter((m) => m.project).length;
|
|
412
|
+
if (count > 0) {
|
|
413
|
+
console.error(`polaris daemon: restored ${count} mapping(s) across ${primaryForPair.size} session(s)`);
|
|
414
|
+
}
|
|
415
|
+
} catch { /* no state file — fresh start */ }
|
|
416
|
+
}
|
|
417
|
+
|
|
331
418
|
// --- Delivery checkpoint ---
|
|
332
419
|
//
|
|
333
420
|
// After each successful event relay, the daemon writes a tiny checkpoint file
|
|
@@ -335,8 +422,6 @@ async function logSessionChange(action: "connect" | "disconnect" | "ws-drop", cc
|
|
|
335
422
|
// where {hash} is the same SHA-256 prefix used by the client. On reconnect,
|
|
336
423
|
// the checkpoint is used to auto-backfill the gap without any manual action.
|
|
337
424
|
|
|
338
|
-
const SESSIONS_DIR = join(homedir(), ".polaris", "sessions");
|
|
339
|
-
|
|
340
425
|
function checkpointPath(ccSessionId: string): string {
|
|
341
426
|
const hash = createHash("sha256").update(ccSessionId).digest("hex").slice(0, 16);
|
|
342
427
|
return join(SESSIONS_DIR, `${hash}-checkpoint.json`);
|
|
@@ -402,6 +487,16 @@ async function backfill(mapping: SessionMapping, duration?: string, from?: strin
|
|
|
402
487
|
}
|
|
403
488
|
}
|
|
404
489
|
|
|
490
|
+
// Collect all CC session IDs that belong to this mapping (MCP UUID + any hook alias).
|
|
491
|
+
// Both the MCP client's UUID and the CC hook session_id (if they differ) must match.
|
|
492
|
+
const knownCcIds = new Set<string>();
|
|
493
|
+
knownCcIds.add(mapping.ccSessionId);
|
|
494
|
+
for (const [id, m] of sessions) {
|
|
495
|
+
if (m.project === mapping.project && m.session === mapping.session) {
|
|
496
|
+
knownCcIds.add(id);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
405
500
|
// Read daemon log files covering the time range
|
|
406
501
|
const logEntries: Array<{ t: string; endpoint: string; payload: Record<string, unknown>; project?: string; session?: string; ccSessionId?: string; response?: unknown }> = [];
|
|
407
502
|
const startDate = since.toISOString().slice(0, 10);
|
|
@@ -426,14 +521,16 @@ async function backfill(mapping: SessionMapping, duration?: string, from?: strin
|
|
|
426
521
|
if (entry.endpoint !== "/events") continue;
|
|
427
522
|
if (new Date(entry.t) <= since) continue;
|
|
428
523
|
if (new Date(entry.t) > now) continue;
|
|
429
|
-
// Filter by origin (added since v0.0.105). Old entries without these fields
|
|
430
|
-
// pass through for backwards compatibility with pre-105 logs.
|
|
431
|
-
if (entry.project !== undefined && entry.project !== mapping.project) continue;
|
|
432
|
-
if (entry.session !== undefined && entry.session !== mapping.session) continue;
|
|
433
|
-
if (entry.ccSessionId !== undefined && entry.ccSessionId !== mapping.ccSessionId) continue;
|
|
434
524
|
// Skip error-response entries — they're duplicates of the primary entry
|
|
435
525
|
// and would cause the same event to be replayed twice.
|
|
436
526
|
if (entry.response !== undefined) continue;
|
|
527
|
+
// Filter by origin. New entries (v0.0.105+) have explicit context fields;
|
|
528
|
+
// old entries can be matched via payload.session_id. Both must belong to
|
|
529
|
+
// this mapping's known CC session IDs to prevent cross-session pollution.
|
|
530
|
+
const entrySessionId: string | undefined = entry.ccSessionId ?? entry.payload.session_id;
|
|
531
|
+
if (entrySessionId !== undefined && !knownCcIds.has(entrySessionId)) continue;
|
|
532
|
+
if (entry.project !== undefined && entry.project !== mapping.project) continue;
|
|
533
|
+
if (entry.session !== undefined && entry.session !== mapping.session) continue;
|
|
437
534
|
logEntries.push(entry);
|
|
438
535
|
} catch { /* skip malformed lines */ }
|
|
439
536
|
}
|
|
@@ -681,6 +778,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
681
778
|
void unlink(checkpointPath(mapping.ccSessionId)).catch(() => {});
|
|
682
779
|
}
|
|
683
780
|
|
|
781
|
+
void persistSessions();
|
|
684
782
|
return json({
|
|
685
783
|
status: "connected",
|
|
686
784
|
project: mapping.project,
|
|
@@ -710,6 +808,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
710
808
|
}
|
|
711
809
|
// Clear checkpoint — a deliberate disconnect means there is nothing to recover
|
|
712
810
|
void unlink(checkpointPath(body.ccSessionId)).catch(() => {});
|
|
811
|
+
void persistSessions();
|
|
713
812
|
return json({ status: "disconnected" });
|
|
714
813
|
} catch {
|
|
715
814
|
return error("Invalid JSON", 400);
|
|
@@ -727,6 +826,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
727
826
|
}
|
|
728
827
|
sessions.clear();
|
|
729
828
|
injectQueues.clear();
|
|
829
|
+
void persistSessions();
|
|
730
830
|
return json({ status: "all_disconnected" });
|
|
731
831
|
}
|
|
732
832
|
|
|
@@ -761,6 +861,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
761
861
|
mapping.pendingMapping = false;
|
|
762
862
|
// Register under the real CC session ID for future events
|
|
763
863
|
sessions.set(ccSessionId, { ...mapping, ccSessionId });
|
|
864
|
+
void persistSessions();
|
|
764
865
|
console.error(`[daemon] Mapped CC session ${ccSessionId.slice(0, 8)} → ${mapping.project}/${mapping.session}`);
|
|
765
866
|
// Share the inject queue between the original ccSessionId (where the
|
|
766
867
|
// cloud WS enqueues) and this CC hook session_id alias, so injected
|
|
@@ -868,7 +969,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
868
969
|
m.slackChannel = body.newName;
|
|
869
970
|
}
|
|
870
971
|
}
|
|
871
|
-
|
|
972
|
+
void persistSessions();
|
|
872
973
|
return json({ status: "renamed", oldName: body.oldName, newName: body.newName });
|
|
873
974
|
} catch {
|
|
874
975
|
return error("Invalid JSON", 400);
|
|
@@ -899,6 +1000,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
899
1000
|
|
|
900
1001
|
const oldName = mapping.session;
|
|
901
1002
|
mapping.session = body.newName;
|
|
1003
|
+
void persistSessions();
|
|
902
1004
|
return json({ status: "renamed", oldName, newName: body.newName });
|
|
903
1005
|
} catch {
|
|
904
1006
|
return error("Invalid JSON", 400);
|
|
@@ -916,6 +1018,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
916
1018
|
m.slackChannel = body.slackChannel;
|
|
917
1019
|
}
|
|
918
1020
|
}
|
|
1021
|
+
void persistSessions();
|
|
919
1022
|
return json({ status: "updated" });
|
|
920
1023
|
} catch {
|
|
921
1024
|
return error("Invalid JSON", 400);
|
|
@@ -1123,6 +1226,11 @@ if (import.meta.main) {
|
|
|
1123
1226
|
// Load config before starting so getServiceUrl() has the active profile
|
|
1124
1227
|
// (and getDaemonSecret() the configured daemonSecret)
|
|
1125
1228
|
await loadConfig();
|
|
1229
|
+
// Enable session persistence (only when running as main, never in tests)
|
|
1230
|
+
persistenceEnabled = true;
|
|
1231
|
+
// Restore sessions from disk before opening the port — new daemon is ready
|
|
1232
|
+
// to serve hook events immediately, before clients notice the restart
|
|
1233
|
+
await loadPersistedSessions();
|
|
1126
1234
|
// Replay any events left in the outbox by a previous daemon run
|
|
1127
1235
|
await scanOutbox();
|
|
1128
1236
|
const { server } = startDaemon();
|