@lightupai/polaris 0.0.106 → 0.0.110
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 +57 -4
- package/src/client/client.ts +51 -0
- package/src/daemon/daemon.ts +99 -3
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.110",
|
|
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
|
@@ -275,7 +275,52 @@ async function removeSystemdService(): Promise<void> {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
// Platform dispatcher
|
|
278
|
+
// --- Session state migration ---
|
|
279
|
+
//
|
|
280
|
+
// When upgrading from a pre-0.0.109 daemon (which never wrote daemon-state.json),
|
|
281
|
+
// the new daemon would start with an empty session map and drop every connected
|
|
282
|
+
// session. This function fetches the running daemon's session list via GET /status
|
|
283
|
+
// and writes daemon-state.json before the daemon is stopped, so the new daemon
|
|
284
|
+
// can restore the map and reconnect cloud WebSockets without any user action.
|
|
285
|
+
// Best-effort: failures are silent (daemon may not be running, or already migrated).
|
|
286
|
+
async function migrateSessionState(): Promise<void> {
|
|
287
|
+
const stateFile = join(POLARIS_DIR, "sessions", "daemon-state.json");
|
|
288
|
+
// If a 0.0.109+ daemon already wrote the file, its state is more complete
|
|
289
|
+
// (includes learned hook aliases) — don't overwrite it.
|
|
290
|
+
try { await readFile(stateFile); return; } catch { /* doesn't exist — proceed */ }
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const config = await loadConfig();
|
|
294
|
+
const secret = resolveDaemonSecret(config);
|
|
295
|
+
const headers: Record<string, string> = {};
|
|
296
|
+
if (secret) headers["x-polaris-daemon-secret"] = secret;
|
|
297
|
+
const res = await fetch("http://127.0.0.1:4322/status", {
|
|
298
|
+
headers,
|
|
299
|
+
signal: AbortSignal.timeout(2000),
|
|
300
|
+
});
|
|
301
|
+
if (!res.ok) return;
|
|
302
|
+
const data = await res.json() as { sessions?: Array<{ ccSessionId: string; project: string; session: string; user: string }> };
|
|
303
|
+
if (!data.sessions || data.sessions.length === 0) return;
|
|
304
|
+
|
|
305
|
+
const activeProfile = config?.active || "";
|
|
306
|
+
const sessions = data.sessions.map((s) => ({
|
|
307
|
+
ccSessionId: s.ccSessionId,
|
|
308
|
+
project: s.project,
|
|
309
|
+
session: s.session,
|
|
310
|
+
user: s.user,
|
|
311
|
+
agent: "agent:claude",
|
|
312
|
+
profile: activeProfile,
|
|
313
|
+
}));
|
|
314
|
+
|
|
315
|
+
await mkdir(join(POLARIS_DIR, "sessions"), { recursive: true });
|
|
316
|
+
await writeFile(stateFile, JSON.stringify({ sessions }));
|
|
317
|
+
console.log(` ✓ Migrated ${sessions.length} active session(s) to persistent state`);
|
|
318
|
+
} catch { /* best-effort — daemon not running or unreachable */ }
|
|
319
|
+
}
|
|
320
|
+
|
|
278
321
|
async function installDaemonService(daemonSecret?: string): Promise<void> {
|
|
322
|
+
// Migrate session state from older daemon before it is stopped
|
|
323
|
+
await migrateSessionState();
|
|
279
324
|
if (process.platform === "darwin") return installLaunchAgent(daemonSecret);
|
|
280
325
|
if (process.platform === "linux") return installSystemdService(daemonSecret);
|
|
281
326
|
// Windows and others: no managed service; daemon is started manually
|
|
@@ -428,8 +473,8 @@ async function install(participantId?: string) {
|
|
|
428
473
|
const skillContent = `---
|
|
429
474
|
name: polaris
|
|
430
475
|
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)]
|
|
476
|
+
allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename polaris_rename_session polaris_update
|
|
477
|
+
argument-hint: [join #channel [session-name] | rename <new-name> | rename-session <name> | disconnect | update | (no args for status)]
|
|
433
478
|
---
|
|
434
479
|
|
|
435
480
|
## Polaris — Multiplayer Collaboration
|
|
@@ -453,6 +498,10 @@ Based on the arguments provided, do ONE of the following:
|
|
|
453
498
|
1. Call \`polaris_rename_session\` with the new name
|
|
454
499
|
2. Report the result
|
|
455
500
|
|
|
501
|
+
**\`/polaris update\`** — Update Polaris to the latest version:
|
|
502
|
+
1. Call \`polaris_update\`
|
|
503
|
+
2. Report what changed and remind the user to restart Claude Code if the MCP server was updated
|
|
504
|
+
|
|
456
505
|
**\`/polaris disconnect\`** — Disconnect:
|
|
457
506
|
1. Call \`polaris_disconnect\`
|
|
458
507
|
2. Confirm disconnection
|
|
@@ -571,8 +620,8 @@ async function login(appUrl: string, profileName?: string, loginHint?: string) {
|
|
|
571
620
|
const skillContent = `---
|
|
572
621
|
name: polaris
|
|
573
622
|
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)]
|
|
623
|
+
allowed-tools: polaris_connect polaris_disconnect polaris_status polaris_reply polaris_context polaris_rename polaris_rename_session polaris_update
|
|
624
|
+
argument-hint: [join #channel [session-name] | rename <new-name> | rename-session <name> | disconnect | update | (no args for status)]
|
|
576
625
|
---
|
|
577
626
|
|
|
578
627
|
## Polaris — Multiplayer Collaboration
|
|
@@ -596,6 +645,10 @@ Based on the arguments provided, do ONE of the following:
|
|
|
596
645
|
1. Call \`polaris_rename_session\` with the new name
|
|
597
646
|
2. Report the result
|
|
598
647
|
|
|
648
|
+
**\`/polaris update\`** — Update Polaris to the latest version:
|
|
649
|
+
1. Call \`polaris_update\`
|
|
650
|
+
2. Report what changed and remind the user to restart Claude Code if the MCP server was updated
|
|
651
|
+
|
|
599
652
|
**\`/polaris disconnect\`** — Disconnect:
|
|
600
653
|
1. Call \`polaris_disconnect\`
|
|
601
654
|
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,49 @@ 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 the copy that's actually in use: ~/.polaris/mcp (no sudo required).
|
|
499
|
+
// This is the same location the version was read from, and where the daemon
|
|
500
|
+
// and MCP server binaries are resolved by run-daemon.sh and claude mcp add.
|
|
501
|
+
const mcpDir = `${homedir()}/.polaris/mcp`;
|
|
502
|
+
const npmResult = Bun.spawnSync(
|
|
503
|
+
["npm", "install", "--prefix", mcpDir, `@lightupai/polaris@${latestVersion}`],
|
|
504
|
+
{ stdout: "pipe", stderr: "pipe" }
|
|
505
|
+
);
|
|
506
|
+
if (npmResult.exitCode !== 0) {
|
|
507
|
+
return { content: [{ type: "text", text: `Update failed:\n${npmResult.stderr.toString().trim()}` }] };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Refresh hooks, migrate session state, and restart the daemon using the
|
|
511
|
+
// local polaris binary (not relying on PATH, which may not include it).
|
|
512
|
+
const polarisBin = `${mcpDir}/node_modules/.bin/polaris`;
|
|
513
|
+
const installResult = Bun.spawnSync([polarisBin, "install"], { stdout: "pipe", stderr: "pipe" });
|
|
514
|
+
const msg = installResult.exitCode === 0
|
|
515
|
+
? `Updated Polaris from v${currentVersion} to v${latestVersion}.\n\nRestart Claude Code for the new MCP server to take effect.`
|
|
516
|
+
: `Updated package to v${latestVersion}, but \`polaris install\` failed — run it manually.\n${installResult.stderr.toString().trim()}`;
|
|
517
|
+
return { content: [{ type: "text", text: msg }] };
|
|
518
|
+
}
|
|
519
|
+
|
|
469
520
|
throw new Error(`Unknown tool: ${name}`);
|
|
470
521
|
});
|
|
471
522
|
|
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`);
|
|
@@ -693,6 +778,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
693
778
|
void unlink(checkpointPath(mapping.ccSessionId)).catch(() => {});
|
|
694
779
|
}
|
|
695
780
|
|
|
781
|
+
void persistSessions();
|
|
696
782
|
return json({
|
|
697
783
|
status: "connected",
|
|
698
784
|
project: mapping.project,
|
|
@@ -722,6 +808,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
722
808
|
}
|
|
723
809
|
// Clear checkpoint — a deliberate disconnect means there is nothing to recover
|
|
724
810
|
void unlink(checkpointPath(body.ccSessionId)).catch(() => {});
|
|
811
|
+
void persistSessions();
|
|
725
812
|
return json({ status: "disconnected" });
|
|
726
813
|
} catch {
|
|
727
814
|
return error("Invalid JSON", 400);
|
|
@@ -739,6 +826,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
739
826
|
}
|
|
740
827
|
sessions.clear();
|
|
741
828
|
injectQueues.clear();
|
|
829
|
+
void persistSessions();
|
|
742
830
|
return json({ status: "all_disconnected" });
|
|
743
831
|
}
|
|
744
832
|
|
|
@@ -773,6 +861,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
773
861
|
mapping.pendingMapping = false;
|
|
774
862
|
// Register under the real CC session ID for future events
|
|
775
863
|
sessions.set(ccSessionId, { ...mapping, ccSessionId });
|
|
864
|
+
void persistSessions();
|
|
776
865
|
console.error(`[daemon] Mapped CC session ${ccSessionId.slice(0, 8)} → ${mapping.project}/${mapping.session}`);
|
|
777
866
|
// Share the inject queue between the original ccSessionId (where the
|
|
778
867
|
// cloud WS enqueues) and this CC hook session_id alias, so injected
|
|
@@ -880,7 +969,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
880
969
|
m.slackChannel = body.newName;
|
|
881
970
|
}
|
|
882
971
|
}
|
|
883
|
-
|
|
972
|
+
void persistSessions();
|
|
884
973
|
return json({ status: "renamed", oldName: body.oldName, newName: body.newName });
|
|
885
974
|
} catch {
|
|
886
975
|
return error("Invalid JSON", 400);
|
|
@@ -911,6 +1000,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
911
1000
|
|
|
912
1001
|
const oldName = mapping.session;
|
|
913
1002
|
mapping.session = body.newName;
|
|
1003
|
+
void persistSessions();
|
|
914
1004
|
return json({ status: "renamed", oldName, newName: body.newName });
|
|
915
1005
|
} catch {
|
|
916
1006
|
return error("Invalid JSON", 400);
|
|
@@ -928,6 +1018,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
928
1018
|
m.slackChannel = body.slackChannel;
|
|
929
1019
|
}
|
|
930
1020
|
}
|
|
1021
|
+
void persistSessions();
|
|
931
1022
|
return json({ status: "updated" });
|
|
932
1023
|
} catch {
|
|
933
1024
|
return error("Invalid JSON", 400);
|
|
@@ -1135,6 +1226,11 @@ if (import.meta.main) {
|
|
|
1135
1226
|
// Load config before starting so getServiceUrl() has the active profile
|
|
1136
1227
|
// (and getDaemonSecret() the configured daemonSecret)
|
|
1137
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();
|
|
1138
1234
|
// Replay any events left in the outbox by a previous daemon run
|
|
1139
1235
|
await scanOutbox();
|
|
1140
1236
|
const { server } = startDaemon();
|