@lightupai/polaris 0.0.104 → 0.0.106
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/package.json +1 -1
- package/src/client/client.ts +32 -8
- package/src/daemon/daemon.ts +129 -5
- package/tests/e2e.test.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightupai/polaris",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.106",
|
|
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/client/client.ts
CHANGED
|
@@ -264,7 +264,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
264
264
|
...(agent ? { agent } : {}),
|
|
265
265
|
...(create ? { create: true } : {}),
|
|
266
266
|
});
|
|
267
|
-
const body = await res.json() as { status?: string; project?: string; session?: string; user?: string; agent?: string; error?: string; existing?: string[]; account?: string };
|
|
267
|
+
const body = await res.json() as { status?: string; project?: string; session?: string; user?: string; agent?: string; error?: string; existing?: string[]; account?: string; gap?: { events: number; since: string } | null };
|
|
268
268
|
if (res.status === 404 && body.error === "project_not_found") {
|
|
269
269
|
const accountLine = body.account ? ` in ${body.account}` : "";
|
|
270
270
|
const list = body.existing && body.existing.length > 0
|
|
@@ -278,7 +278,11 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
278
278
|
currentUser = user;
|
|
279
279
|
currentProfile = activeProfile;
|
|
280
280
|
await saveSessionState(currentProject, currentSession, currentUser, currentProfile);
|
|
281
|
-
|
|
281
|
+
let msg = `Connected to #${currentProject}/${currentSession} as ${user}.`;
|
|
282
|
+
if (body.gap && body.gap.events > 0) {
|
|
283
|
+
msg += `\n${body.gap.events} events since ${body.gap.since} not delivered — re-sending now.`;
|
|
284
|
+
}
|
|
285
|
+
return { content: [{ type: "text", text: msg }] };
|
|
282
286
|
}
|
|
283
287
|
return { content: [{ type: "text", text: `Failed to connect: ${body.error ?? "unknown error"}` }] };
|
|
284
288
|
} catch {
|
|
@@ -302,12 +306,23 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
302
306
|
|
|
303
307
|
if (name === "polaris_status") {
|
|
304
308
|
try {
|
|
305
|
-
const
|
|
306
|
-
|
|
309
|
+
const [statusRes, checkpointRes] = await Promise.all([
|
|
310
|
+
daemonGet(`/status/${CC_SESSION_ID}`),
|
|
311
|
+
daemonGet(`/checkpoint/${CC_SESSION_ID}`),
|
|
312
|
+
]);
|
|
313
|
+
const body = (await statusRes.json()) as { connected: boolean; project?: string; session?: string; user?: string; account?: string; slackChannel?: string };
|
|
307
314
|
if (body.connected) {
|
|
308
315
|
const accountTag = body.account ? ` [${body.account}]` : "";
|
|
309
316
|
const slackTag = body.slackChannel ? ` #${body.slackChannel}` : "";
|
|
310
|
-
|
|
317
|
+
let text = `Connected: ${body.project}/${body.session} as ${body.user}${accountTag}${slackTag}`;
|
|
318
|
+
try {
|
|
319
|
+
const cp = await checkpointRes.json() as { hasCheckpoint: boolean; lastEventTimestamp?: string; pending?: number };
|
|
320
|
+
if (cp.hasCheckpoint && cp.lastEventTimestamp) {
|
|
321
|
+
const ts = new Date(cp.lastEventTimestamp).toISOString().slice(11, 19) + "Z";
|
|
322
|
+
text += `\nLast delivered: ${ts}, ${cp.pending ?? 0} pending`;
|
|
323
|
+
}
|
|
324
|
+
} catch { /* best-effort */ }
|
|
325
|
+
return { content: [{ type: "text", text }] };
|
|
311
326
|
}
|
|
312
327
|
return { content: [{ type: "text", text: "Not connected to any Polaris session." }] };
|
|
313
328
|
} catch {
|
|
@@ -478,7 +493,7 @@ async function main() {
|
|
|
478
493
|
user: saved.user,
|
|
479
494
|
profile: reconnectProfile,
|
|
480
495
|
});
|
|
481
|
-
const body = await res.json() as { status?: string; project?: string; session?: string; user?: string; error?: string };
|
|
496
|
+
const body = await res.json() as { status?: string; project?: string; session?: string; user?: string; error?: string; gap?: { events: number; since: string } | null };
|
|
482
497
|
if (res.ok) {
|
|
483
498
|
currentProject = body.project ?? saved.project;
|
|
484
499
|
currentSession = body.session ?? saved.session;
|
|
@@ -486,7 +501,11 @@ async function main() {
|
|
|
486
501
|
currentProfile = reconnectProfile;
|
|
487
502
|
// Show email when it looks like one; fall back to profile key or "default"
|
|
488
503
|
const accountLabel = reconnectProfile.includes("@") ? reconnectProfile : (reconnectProfile || "default");
|
|
489
|
-
|
|
504
|
+
let reconnMsg = `Polaris auto-reconnected to #${currentProject}/${currentSession} [${accountLabel}]`;
|
|
505
|
+
if (body.gap && body.gap.events > 0) {
|
|
506
|
+
reconnMsg += `; re-sending ${body.gap.events} events since ${body.gap.since}`;
|
|
507
|
+
}
|
|
508
|
+
console.error(reconnMsg);
|
|
490
509
|
} else {
|
|
491
510
|
// Session or project no longer exists — clear stale state
|
|
492
511
|
await clearSessionState();
|
|
@@ -514,7 +533,12 @@ async function main() {
|
|
|
514
533
|
profile: currentProfile,
|
|
515
534
|
});
|
|
516
535
|
if (reconnectRes.ok) {
|
|
517
|
-
|
|
536
|
+
const reconnBody = await reconnectRes.json() as { gap?: { events: number; since: string } | null };
|
|
537
|
+
let reconnMsg = `Polaris: session dropped; auto-reconnected to #${currentProject}/${currentSession}`;
|
|
538
|
+
if (reconnBody.gap && reconnBody.gap.events > 0) {
|
|
539
|
+
reconnMsg += `; re-sending ${reconnBody.gap.events} events`;
|
|
540
|
+
}
|
|
541
|
+
console.error(reconnMsg);
|
|
518
542
|
} else {
|
|
519
543
|
console.error(`Polaris: session dropped and reconnect failed. Run /polaris to check status.`);
|
|
520
544
|
currentProject = "";
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Server, ServerWebSocket } from "bun";
|
|
|
2
2
|
import { readFile, writeFile, appendFile, mkdir, readdir, unlink } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
5
6
|
|
|
6
7
|
// --- Session registry ---
|
|
7
8
|
|
|
@@ -205,10 +206,13 @@ function ensureLogDir(): Promise<void> {
|
|
|
205
206
|
return logReady;
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
async function logEvent(endpoint: string, payload: unknown, response?: { status: number; body?: unknown }): Promise<void> {
|
|
209
|
+
async function logEvent(endpoint: string, payload: unknown, response?: { status: number; body?: unknown }, context?: { project?: string; session?: string; ccSessionId?: string }): Promise<void> {
|
|
209
210
|
try {
|
|
210
211
|
await ensureLogDir();
|
|
211
212
|
const entry: Record<string, unknown> = { t: new Date().toISOString(), endpoint, payload };
|
|
213
|
+
if (context?.project) entry.project = context.project;
|
|
214
|
+
if (context?.session) entry.session = context.session;
|
|
215
|
+
if (context?.ccSessionId) entry.ccSessionId = context.ccSessionId;
|
|
212
216
|
if (response) entry.response = response;
|
|
213
217
|
const file = join(LOG_DIR, `daemon-${new Date().toISOString().slice(0, 10)}.jsonl`);
|
|
214
218
|
await appendFile(file, JSON.stringify(entry) + "\n");
|
|
@@ -324,6 +328,43 @@ async function logSessionChange(action: "connect" | "disconnect" | "ws-drop", cc
|
|
|
324
328
|
} catch { /* best-effort */ }
|
|
325
329
|
}
|
|
326
330
|
|
|
331
|
+
// --- Delivery checkpoint ---
|
|
332
|
+
//
|
|
333
|
+
// After each successful event relay, the daemon writes a tiny checkpoint file
|
|
334
|
+
// next to the session state file: ~/.polaris/sessions/{hash}-checkpoint.json
|
|
335
|
+
// where {hash} is the same SHA-256 prefix used by the client. On reconnect,
|
|
336
|
+
// the checkpoint is used to auto-backfill the gap without any manual action.
|
|
337
|
+
|
|
338
|
+
const SESSIONS_DIR = join(homedir(), ".polaris", "sessions");
|
|
339
|
+
|
|
340
|
+
function checkpointPath(ccSessionId: string): string {
|
|
341
|
+
const hash = createHash("sha256").update(ccSessionId).digest("hex").slice(0, 16);
|
|
342
|
+
return join(SESSIONS_DIR, `${hash}-checkpoint.json`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
interface Checkpoint {
|
|
346
|
+
lastEventId: string;
|
|
347
|
+
lastEventTimestamp: string;
|
|
348
|
+
project: string;
|
|
349
|
+
session: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function writeCheckpoint(ccSessionId: string, eventId: string, eventTimestamp: string, project: string, session: string): Promise<void> {
|
|
353
|
+
try {
|
|
354
|
+
await mkdir(SESSIONS_DIR, { recursive: true });
|
|
355
|
+
const cp: Checkpoint = { lastEventId: eventId, lastEventTimestamp: eventTimestamp, project, session };
|
|
356
|
+
await writeFile(checkpointPath(ccSessionId), JSON.stringify(cp));
|
|
357
|
+
} catch { /* best-effort */ }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function readCheckpoint(ccSessionId: string): Promise<Checkpoint | null> {
|
|
361
|
+
try {
|
|
362
|
+
const data = JSON.parse(await readFile(checkpointPath(ccSessionId), "utf-8")) as Checkpoint;
|
|
363
|
+
if (data.lastEventId && data.lastEventTimestamp) return data;
|
|
364
|
+
} catch { /* none */ }
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
327
368
|
// --- Backfill: recover lost events from daemon log ---
|
|
328
369
|
|
|
329
370
|
async function backfill(mapping: SessionMapping, duration?: string, from?: string, dryRun = false): Promise<{ recovered: number; would_recover?: number; source: string; gaps: string[] }> {
|
|
@@ -361,8 +402,18 @@ async function backfill(mapping: SessionMapping, duration?: string, from?: strin
|
|
|
361
402
|
}
|
|
362
403
|
}
|
|
363
404
|
|
|
405
|
+
// Collect all CC session IDs that belong to this mapping (MCP UUID + any hook alias).
|
|
406
|
+
// Both the MCP client's UUID and the CC hook session_id (if they differ) must match.
|
|
407
|
+
const knownCcIds = new Set<string>();
|
|
408
|
+
knownCcIds.add(mapping.ccSessionId);
|
|
409
|
+
for (const [id, m] of sessions) {
|
|
410
|
+
if (m.project === mapping.project && m.session === mapping.session) {
|
|
411
|
+
knownCcIds.add(id);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
364
415
|
// Read daemon log files covering the time range
|
|
365
|
-
const logEntries: Array<{ t: string; endpoint: string; payload: Record<string, unknown
|
|
416
|
+
const logEntries: Array<{ t: string; endpoint: string; payload: Record<string, unknown>; project?: string; session?: string; ccSessionId?: string; response?: unknown }> = [];
|
|
366
417
|
const startDate = since.toISOString().slice(0, 10);
|
|
367
418
|
const endDate = now.toISOString().slice(0, 10);
|
|
368
419
|
|
|
@@ -385,6 +436,16 @@ async function backfill(mapping: SessionMapping, duration?: string, from?: strin
|
|
|
385
436
|
if (entry.endpoint !== "/events") continue;
|
|
386
437
|
if (new Date(entry.t) <= since) continue;
|
|
387
438
|
if (new Date(entry.t) > now) continue;
|
|
439
|
+
// Skip error-response entries — they're duplicates of the primary entry
|
|
440
|
+
// and would cause the same event to be replayed twice.
|
|
441
|
+
if (entry.response !== undefined) continue;
|
|
442
|
+
// Filter by origin. New entries (v0.0.105+) have explicit context fields;
|
|
443
|
+
// old entries can be matched via payload.session_id. Both must belong to
|
|
444
|
+
// this mapping's known CC session IDs to prevent cross-session pollution.
|
|
445
|
+
const entrySessionId: string | undefined = entry.ccSessionId ?? entry.payload.session_id;
|
|
446
|
+
if (entrySessionId !== undefined && !knownCcIds.has(entrySessionId)) continue;
|
|
447
|
+
if (entry.project !== undefined && entry.project !== mapping.project) continue;
|
|
448
|
+
if (entry.session !== undefined && entry.session !== mapping.session) continue;
|
|
388
449
|
logEntries.push(entry);
|
|
389
450
|
} catch { /* skip malformed lines */ }
|
|
390
451
|
}
|
|
@@ -424,6 +485,19 @@ async function backfill(mapping: SessionMapping, duration?: string, from?: strin
|
|
|
424
485
|
|
|
425
486
|
return { recovered, source: "daemon_log", gaps: [] };
|
|
426
487
|
}
|
|
488
|
+
|
|
489
|
+
// Auto-backfill from checkpoint after a reconnect. Called non-blocking (void)
|
|
490
|
+
// so the /connect response is returned immediately while replay runs in background.
|
|
491
|
+
async function autoBackfillAfterConnect(mapping: SessionMapping): Promise<void> {
|
|
492
|
+
const cp = await readCheckpoint(mapping.ccSessionId);
|
|
493
|
+
if (!cp) return;
|
|
494
|
+
if (cp.project !== mapping.project || cp.session !== mapping.session) return;
|
|
495
|
+
const result = await backfill(mapping, undefined, cp.lastEventTimestamp);
|
|
496
|
+
if (result.recovered > 0) {
|
|
497
|
+
console.error(`polaris daemon: gap detected — auto-backfilled ${result.recovered} events since ${cp.lastEventTimestamp}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
427
501
|
// --- HTTP Server ---
|
|
428
502
|
|
|
429
503
|
function json(data: unknown, status = 200): Response {
|
|
@@ -552,6 +626,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
552
626
|
// Retry with new name on 409 (collision with generated name)
|
|
553
627
|
let attempts = 0;
|
|
554
628
|
let created = false;
|
|
629
|
+
let isReconnect = false; // true only when the session already existed (409 + explicit name)
|
|
555
630
|
while (!created && attempts < 3) {
|
|
556
631
|
const sessionRes = await fetch(`${serviceUrl}/projects/${body.project}/sessions`, {
|
|
557
632
|
method: "POST",
|
|
@@ -569,6 +644,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
569
644
|
body: JSON.stringify({ driver: body.user }),
|
|
570
645
|
});
|
|
571
646
|
created = true;
|
|
647
|
+
isReconnect = true; // session pre-existed → this is a true reconnect
|
|
572
648
|
} else {
|
|
573
649
|
// Generated name collision — retry with new name
|
|
574
650
|
mapping.session = generateSessionName();
|
|
@@ -597,12 +673,33 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
597
673
|
connectCloudWs(mapping);
|
|
598
674
|
await logSessionChange("connect", mapping.ccSessionId, mapping.project, mapping.session, mapping.user, mapping.agent);
|
|
599
675
|
|
|
676
|
+
// Check for a delivery gap since the last checkpoint (local-only dry run — no network).
|
|
677
|
+
// Only meaningful on a true reconnect (existing session); a brand-new session has no
|
|
678
|
+
// history to recover. Clear any stale checkpoint when starting a fresh session.
|
|
679
|
+
let gap: { events: number; since: string } | null = null;
|
|
680
|
+
if (isReconnect) {
|
|
681
|
+
const cp = await readCheckpoint(mapping.ccSessionId);
|
|
682
|
+
if (cp && cp.project === mapping.project && cp.session === mapping.session) {
|
|
683
|
+
const dryResult = await backfill(mapping, undefined, cp.lastEventTimestamp, true);
|
|
684
|
+
const count = dryResult.would_recover ?? 0;
|
|
685
|
+
if (count > 0) {
|
|
686
|
+
gap = { events: count, since: cp.lastEventTimestamp };
|
|
687
|
+
void autoBackfillAfterConnect(mapping);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
// Fresh session — clear any checkpoint left over from a previous session
|
|
692
|
+
// on this CC session ID so stale data never triggers a false backfill.
|
|
693
|
+
void unlink(checkpointPath(mapping.ccSessionId)).catch(() => {});
|
|
694
|
+
}
|
|
695
|
+
|
|
600
696
|
return json({
|
|
601
697
|
status: "connected",
|
|
602
698
|
project: mapping.project,
|
|
603
699
|
session: mapping.session,
|
|
604
700
|
user: mapping.user,
|
|
605
701
|
agent: mapping.agent,
|
|
702
|
+
gap,
|
|
606
703
|
});
|
|
607
704
|
} catch {
|
|
608
705
|
return error("Invalid JSON", 400);
|
|
@@ -623,6 +720,8 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
623
720
|
mapping.session = "";
|
|
624
721
|
mapping.user = "";
|
|
625
722
|
}
|
|
723
|
+
// Clear checkpoint — a deliberate disconnect means there is nothing to recover
|
|
724
|
+
void unlink(checkpointPath(body.ccSessionId)).catch(() => {});
|
|
626
725
|
return json({ status: "disconnected" });
|
|
627
726
|
} catch {
|
|
628
727
|
return error("Invalid JSON", 400);
|
|
@@ -636,6 +735,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
636
735
|
mapping.project = "";
|
|
637
736
|
mapping.session = "";
|
|
638
737
|
mapping.user = "";
|
|
738
|
+
void unlink(checkpointPath(id)).catch(() => {});
|
|
639
739
|
}
|
|
640
740
|
sessions.clear();
|
|
641
741
|
injectQueues.clear();
|
|
@@ -646,7 +746,6 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
646
746
|
if (method === "POST" && pathname === "/events") {
|
|
647
747
|
try {
|
|
648
748
|
const body = (await req.json()) as { session_id?: string; [key: string]: unknown };
|
|
649
|
-
await logEvent("/events", body);
|
|
650
749
|
const ccSessionId = body.session_id;
|
|
651
750
|
if (!ccSessionId) return error("session_id required in hook payload", 400);
|
|
652
751
|
|
|
@@ -689,6 +788,9 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
689
788
|
}
|
|
690
789
|
}
|
|
691
790
|
|
|
791
|
+
// Log with session context so backfill can filter by exact origin
|
|
792
|
+
await logEvent("/events", body, undefined, { project: mapping.project, session: mapping.session, ccSessionId });
|
|
793
|
+
|
|
692
794
|
// Determine sender: human for prompts, agent for everything else
|
|
693
795
|
const hookEvent = body.hook_event_name as string | undefined;
|
|
694
796
|
const sender = hookEvent === "UserPromptSubmit" ? mapping.user : mapping.agent;
|
|
@@ -713,7 +815,7 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
713
815
|
|
|
714
816
|
if (!res || res.status >= 500) {
|
|
715
817
|
console.error(`polaris daemon: upstream relay failed (${res ? res.status : "network error"}) — queued to outbox`);
|
|
716
|
-
await logEvent("/events", body, { status: res?.status ?? 0, body: "queued to outbox" });
|
|
818
|
+
await logEvent("/events", body, { status: res?.status ?? 0, body: "queued to outbox" }, { project: mapping.project, session: mapping.session, ccSessionId });
|
|
717
819
|
await enqueueOutbox(mapping.project, mapping.session, mapping.profile, relayBody);
|
|
718
820
|
// The event is durably accepted (outbox), so still drain injects
|
|
719
821
|
if (hookEvent === "UserPromptSubmit") {
|
|
@@ -726,10 +828,18 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
726
828
|
|
|
727
829
|
if (!res.ok) {
|
|
728
830
|
const err = await res.text();
|
|
729
|
-
await logEvent("/events", body, { status: res.status, body: err });
|
|
831
|
+
await logEvent("/events", body, { status: res.status, body: err }, { project: mapping.project, session: mapping.session, ccSessionId });
|
|
730
832
|
return new Response(err, { status: res.status });
|
|
731
833
|
}
|
|
732
834
|
|
|
835
|
+
// Write delivery checkpoint so reconnects can auto-backfill the exact gap
|
|
836
|
+
try {
|
|
837
|
+
const delivered = await res.json() as { id?: string; timestamp?: string };
|
|
838
|
+
if (delivered.id && delivered.timestamp) {
|
|
839
|
+
void writeCheckpoint(ccSessionId, delivered.id, delivered.timestamp, mapping.project, mapping.session);
|
|
840
|
+
}
|
|
841
|
+
} catch { /* best-effort — don't block the response */ }
|
|
842
|
+
|
|
733
843
|
// UserPromptSubmit: drain queued advisor injects and hand them back
|
|
734
844
|
// to the hook, which surfaces them via additionalContext
|
|
735
845
|
if (hookEvent === "UserPromptSubmit") {
|
|
@@ -967,6 +1077,20 @@ export function startDaemon(port = Number(process.env.POLARIS_DAEMON_PORT ?? 432
|
|
|
967
1077
|
}
|
|
968
1078
|
}
|
|
969
1079
|
|
|
1080
|
+
// GET /checkpoint/:ccSessionId — checkpoint state + pending count for polaris_status
|
|
1081
|
+
if (method === "GET" && pathname.startsWith("/checkpoint/")) {
|
|
1082
|
+
const ccSessionId = pathname.slice("/checkpoint/".length);
|
|
1083
|
+
const cp = await readCheckpoint(ccSessionId);
|
|
1084
|
+
if (!cp) return json({ hasCheckpoint: false });
|
|
1085
|
+
const mapping = sessions.get(ccSessionId);
|
|
1086
|
+
let pending = 0;
|
|
1087
|
+
if (mapping?.project && cp.project === mapping.project && cp.session === mapping.session) {
|
|
1088
|
+
const dryResult = await backfill(mapping, undefined, cp.lastEventTimestamp, true);
|
|
1089
|
+
pending = dryResult.would_recover ?? 0;
|
|
1090
|
+
}
|
|
1091
|
+
return json({ hasCheckpoint: true, lastEventTimestamp: cp.lastEventTimestamp, pending });
|
|
1092
|
+
}
|
|
1093
|
+
|
|
970
1094
|
// POST /backfill — recover lost events from daemon log
|
|
971
1095
|
if (method === "POST" && pathname === "/backfill") {
|
|
972
1096
|
try {
|
package/tests/e2e.test.ts
CHANGED
|
@@ -32,6 +32,8 @@ afterAll(async () => {
|
|
|
32
32
|
|
|
33
33
|
beforeEach(async () => {
|
|
34
34
|
await resetTestData(sql);
|
|
35
|
+
// Disconnect all daemon sessions so checkpoint files are cleared between tests
|
|
36
|
+
await fetch(`${daemonUrl}/disconnect-all`, { method: "POST", headers: { "Content-Type": "application/json" } });
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
async function post(base: string, path: string, body: unknown) {
|