diff --git a/apps/server/src/persistenceService.test.ts b/apps/server/src/persistenceService.test.ts index 88fd74444a4c..dbac2eae8c07 100644 --- a/apps/server/src/persistenceService.test.ts +++ b/apps/server/src/persistenceService.test.ts @@ -102,7 +102,7 @@ describe("PersistenceService", () => { sessionId: "sess-1", }; - service.ingestProviderEvent({ + const event1 = service.ingestProviderEvent({ ...baseEvent, id: "evt-1", createdAt: iso(10), @@ -115,8 +115,9 @@ describe("PersistenceService", () => { }, }, }); + expect(event1?.seq).toBeGreaterThan(0); - service.ingestProviderEvent({ + const event2 = service.ingestProviderEvent({ ...baseEvent, id: "evt-2", createdAt: iso(20), @@ -128,8 +129,9 @@ describe("PersistenceService", () => { delta: "hi", }, }); + expect((event2?.seq ?? 0) > (event1?.seq ?? 0)).toBe(true); - service.ingestProviderEvent({ + const event3 = service.ingestProviderEvent({ ...baseEvent, id: "evt-3", createdAt: iso(30), @@ -142,8 +144,9 @@ describe("PersistenceService", () => { }, }, }); + expect((event3?.seq ?? 0) > (event2?.seq ?? 0)).toBe(true); - service.ingestProviderEvent({ + const event4 = service.ingestProviderEvent({ ...baseEvent, id: "evt-4", createdAt: iso(40), @@ -156,6 +159,7 @@ describe("PersistenceService", () => { }, }, }); + expect((event4?.seq ?? 0) > (event3?.seq ?? 0)).toBe(true); const snapshot = service.loadSnapshot(); expect(snapshot.projects).toHaveLength(1); @@ -179,6 +183,24 @@ describe("PersistenceService", () => { expect(catchUp.events.some((event) => event.eventType === "project.upsert")).toBe(true); expect(catchUp.events.some((event) => event.eventType === "thread.upsert")).toBe(true); expect(catchUp.events.some((event) => event.eventType === "message.upsert")).toBe(true); + + const providerCatchUp = service.providerCatchUp({ afterSeq: 0 }); + expect(providerCatchUp.lastProviderSeq).toBe(event4?.seq ?? 0); + expect(providerCatchUp.events.map((event) => event.id)).toEqual([ + "evt-1", + "evt-2", + "evt-3", + "evt-4", + ]); + const providerSeqs = providerCatchUp.events.map((event) => event.seq ?? 0); + for (let index = 1; index < providerSeqs.length; index += 1) { + expect(providerSeqs[index]).toBeGreaterThan(providerSeqs[index - 1] ?? Number.NEGATIVE_INFINITY); + } + + const providerCatchUpAfterSecond = service.providerCatchUp({ + afterSeq: event2?.seq ?? 0, + }); + expect(providerCatchUpAfterSecond.events.map((event) => event.id)).toEqual(["evt-3", "evt-4"]); } finally { service.close(); } diff --git a/apps/server/src/persistenceService.ts b/apps/server/src/persistenceService.ts index 1a2e444b441f..0ddffce7df04 100644 --- a/apps/server/src/persistenceService.ts +++ b/apps/server/src/persistenceService.ts @@ -16,6 +16,8 @@ import { type ProjectRemoveInput, type ProjectUpdateScriptsInput, type ProjectUpdateScriptsResult, + type ProviderCatchUpInput, + type ProviderCatchUpResult, type ProviderEvent, type ProviderSendTurnInput, type StateBootstrapResult, @@ -44,6 +46,9 @@ import { projectRemoveInputSchema, projectScriptsSchema, projectUpdateScriptsInputSchema, + providerCatchUpInputSchema, + providerCatchUpResultSchema, + providerEventSchema, stateBootstrapResultSchema, stateCatchUpInputSchema, stateCatchUpResultSchema, @@ -94,6 +99,25 @@ interface StateEventRow { interface ProviderEventInsertResult { inserted: boolean; runtimeThreadId: string | null; + seq: number; +} + +interface ProviderEventRow { + seq: number; + id: string; + session_id: string; + provider: string; + kind: string; + method: string; + thread_id: string | null; + turn_id: string | null; + item_id: string | null; + request_id: string | null; + request_kind: string | null; + text_delta: string | null; + message: string | null; + payload_json: string | null; + created_at: string; } export interface PersistenceServiceOptions { @@ -772,6 +796,43 @@ export class PersistenceService extends EventEmitter { }); } + providerCatchUp(raw: ProviderCatchUpInput): ProviderCatchUpResult { + const input = providerCatchUpInputSchema.parse(raw); + const rows = this.db + .prepare( + `SELECT + seq, + id, + session_id, + provider, + kind, + method, + thread_id, + turn_id, + item_id, + request_id, + request_kind, + text_delta, + message, + payload_json, + created_at + FROM provider_events + WHERE seq > ? + ORDER BY seq ASC + LIMIT ?;`, + ) + .all(input.afterSeq, input.limit) as unknown as ProviderEventRow[]; + + const events = rows + .map((row) => this.parseProviderEventRow(row)) + .filter((event): event is ProviderEvent => event !== null); + + return providerCatchUpResultSchema.parse({ + events, + lastProviderSeq: this.readLastProviderSeq(), + }); + } + listMessages(raw: StateListMessagesInput): StateListMessagesResult { const input = stateListMessagesInputSchema.parse(raw); const rows = this.db @@ -867,12 +928,18 @@ export class PersistenceService extends EventEmitter { }); } - ingestProviderEvent(event: ProviderEvent): void { + ingestProviderEvent(event: ProviderEvent): ProviderEvent | null { + let persistedEvent: ProviderEvent | null = null; this.withTransaction((pendingEvents) => { const insertResult = this.insertProviderEvent(event); if (!insertResult.inserted) { return; } + persistedEvent = providerEventSchema.parse({ + ...event, + seq: insertResult.seq, + ...(insertResult.runtimeThreadId ? { threadId: insertResult.runtimeThreadId } : {}), + }); const localThreadId = this.resolveThreadIdForEvent(event, insertResult.runtimeThreadId); if (!localThreadId) { @@ -1138,6 +1205,7 @@ export class PersistenceService extends EventEmitter { } } }); + return persistedEvent; } persistTurnDiffSummaryFromCheckpoint(input: { @@ -1360,6 +1428,13 @@ export class PersistenceService extends EventEmitter { return row?.seq ?? 0; } + private readLastProviderSeq(): number { + const row = this.db + .prepare("SELECT COALESCE(MAX(seq), 0) AS seq FROM provider_events;") + .get() as { seq: number } | undefined; + return row?.seq ?? 0; + } + private withTransaction(fn: (pendingEvents: StateEvent[]) => T): T { const pendingEvents: StateEvent[] = []; const result = this.stateDb.transaction(() => fn(pendingEvents)); @@ -1766,13 +1841,40 @@ export class PersistenceService extends EventEmitter { event.message ?? null, payloadJson, event.createdAt, - ) as { changes?: number | bigint }; + ) as { changes?: number | bigint; lastInsertRowid?: number | bigint }; + const inserted = toSafeInteger(result.changes, 0) > 0; return { - inserted: toSafeInteger(result.changes, 0) > 0, + inserted, runtimeThreadId: runtimeThreadId ?? null, + seq: inserted ? toSafeInteger(result.lastInsertRowid, 0) : 0, }; } + private parseProviderEventRow(row: ProviderEventRow): ProviderEvent | null { + const payload = row.payload_json ? this.tryParseJson(row.payload_json) : undefined; + const parsed = providerEventSchema.safeParse({ + seq: row.seq, + id: row.id, + kind: row.kind, + provider: row.provider, + sessionId: row.session_id, + createdAt: row.created_at, + method: row.method, + ...(row.thread_id ? { threadId: row.thread_id } : {}), + ...(row.turn_id ? { turnId: row.turn_id } : {}), + ...(row.item_id ? { itemId: row.item_id } : {}), + ...(row.request_id ? { requestId: row.request_id } : {}), + ...(row.request_kind ? { requestKind: row.request_kind } : {}), + ...(row.text_delta !== null ? { textDelta: row.text_delta } : {}), + ...(row.message ? { message: row.message } : {}), + ...(payload !== undefined ? { payload } : {}), + }); + if (!parsed.success) { + return null; + } + return parsed.data; + } + private parseJson(json: string, schema: SafeParseSchema): T | null { const candidate = this.tryParseJson(json); const parsed = schema.safeParse(candidate); diff --git a/apps/server/src/providerManager.test.ts b/apps/server/src/providerManager.test.ts index c26f34cb6ae5..c2924692c35e 100644 --- a/apps/server/src/providerManager.test.ts +++ b/apps/server/src/providerManager.test.ts @@ -3,6 +3,58 @@ import { describe, expect, it, vi } from "vitest"; import { ProviderManager } from "./providerManager"; describe("ProviderManager", () => { + it("emits persisted provider events with durable sequence metadata when available", () => { + const ingestProviderEvent = vi.fn((event: { + id: string; + kind: "notification"; + provider: "codex"; + sessionId: string; + createdAt: string; + method: string; + threadId: string; + }) => ({ + ...event, + seq: 11, + })); + const persistenceService = { + ingestProviderEvent, + unbindSession: vi.fn(), + }; + const manager = new ProviderManager({ + persistenceService: persistenceService as never, + }); + const events: Array<{ id: string; seq?: number }> = []; + manager.on("event", (event) => { + events.push({ id: event.id, seq: event.seq }); + }); + + const internals = manager as unknown as { + onCodexEvent: (event: { + id: string; + kind: "notification"; + provider: "codex"; + sessionId: string; + createdAt: string; + method: string; + threadId: string; + }) => void; + }; + + internals.onCodexEvent({ + id: "evt-seq-1", + kind: "notification", + provider: "codex", + sessionId: "sess-1", + createdAt: new Date().toISOString(), + method: "turn/started", + threadId: "thread-1", + }); + + expect(ingestProviderEvent).toHaveBeenCalledTimes(1); + expect(events).toEqual([{ id: "evt-seq-1", seq: 11 }]); + manager.dispose(); + }); + it("detaches provider event listener and ends thread log streams on dispose", () => { const manager = new ProviderManager(); const internals = manager as unknown as { diff --git a/apps/server/src/providerManager.ts b/apps/server/src/providerManager.ts index 8ce55ea7112c..ec3d23412362 100644 --- a/apps/server/src/providerManager.ts +++ b/apps/server/src/providerManager.ts @@ -180,18 +180,11 @@ export class ProviderManager extends EventEmitter { if (this.disposed) { return; } - - this.routeEventToThreadLog(event); - try { - this.persistenceService?.ingestProviderEvent(event); - } catch { - // Persistence failures should not break provider streaming. - } - if (event.method === "session/closed" || event.method === "session/exited") { - this.persistenceService?.unbindSession(event.sessionId); + const emittedEvent = this.publishProviderEvent(event); + if (!emittedEvent) { + return; } - this.emit("event", event); - this.maybeCaptureFilesystemCheckpoint(event); + this.maybeCaptureFilesystemCheckpoint(emittedEvent); }; constructor(options: ProviderManagerOptions = {}) { @@ -546,7 +539,7 @@ export class ProviderManager extends EventEmitter { } private emitCheckpointCaptured(sessionId: string, threadId: string, turnCount: number): void { - this.emit("event", { + this.publishProviderEvent({ id: randomUUID(), kind: "notification", provider: "codex", @@ -566,7 +559,7 @@ export class ProviderManager extends EventEmitter { message: string, threadId?: string, ): void { - this.emit("event", { + this.publishProviderEvent({ id: randomUUID(), kind: "error", provider: "codex", @@ -578,6 +571,26 @@ export class ProviderManager extends EventEmitter { }); } + private publishProviderEvent(event: ProviderEvent): ProviderEvent | null { + let emittedEvent = event; + try { + const persistedEvent = this.persistenceService?.ingestProviderEvent(event); + if (this.persistenceService && !persistedEvent) { + return null; + } + emittedEvent = persistedEvent ?? event; + } catch { + // Persistence failures should not break provider streaming. + } + + this.routeEventToThreadLog(emittedEvent); + if (emittedEvent.method === "session/closed" || emittedEvent.method === "session/exited") { + this.persistenceService?.unbindSession(emittedEvent.sessionId); + } + this.emit("event", emittedEvent); + return emittedEvent; + } + private withFilesystemLock(sessionId: string, fn: () => Promise): Promise { const previous = this.filesystemLocks.get(sessionId) ?? Promise.resolve(); const next = previous.catch(() => undefined).then(fn); diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts index 51f086a090e0..8e0eb3d0d77d 100644 --- a/apps/server/src/wsServer.test.ts +++ b/apps/server/src/wsServer.test.ts @@ -669,6 +669,58 @@ describe("WebSocket Server", () => { expect(response.result).toEqual([]); }); + it("returns durable provider catch-up events ordered by seq", async () => { + const stateDir = makeTempDir("t3code-ws-provider-catchup-"); + const persistenceService = new PersistenceService({ + dbPath: path.join(stateDir, "state.sqlite"), + legacyProjectsJsonPath: path.join(stateDir, "projects.json"), + }); + + const first = persistenceService.ingestProviderEvent({ + id: "provider-evt-1", + kind: "notification", + provider: "codex", + sessionId: "sess-1", + createdAt: "2026-02-20T00:00:00.000Z", + method: "turn/started", + turnId: "turn-1", + payload: { turn: { id: "turn-1" } }, + }); + const second = persistenceService.ingestProviderEvent({ + id: "provider-evt-2", + kind: "notification", + provider: "codex", + sessionId: "sess-1", + createdAt: "2026-02-20T00:00:01.000Z", + method: "turn/completed", + turnId: "turn-1", + payload: { turn: { id: "turn-1", status: "completed" } }, + }); + + server = createTestServer({ cwd: "/test", stateDir, persistenceService }); + await server.start(); + const addr = server.httpServer.address(); + const port = typeof addr === "object" && addr !== null ? addr.port : 0; + + const ws = await connectWs(port); + connections.push(ws); + await waitForMessage(ws); + + const response = await sendRequest(ws, WS_METHODS.providersCatchUp, { + afterSeq: first?.seq ?? 0, + }); + expect(response.error).toBeUndefined(); + expect(response.result).toEqual({ + events: [ + expect.objectContaining({ + id: "provider-evt-2", + seq: second?.seq, + }), + ], + lastProviderSeq: second?.seq ?? 0, + }); + }); + it("persists app settings through appSettings RPC methods", async () => { const stateDir = makeTempDir("t3code-ws-app-settings-"); server = createTestServer({ cwd: "/test", stateDir }); diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index 9e92c76e69bc..81ccf7ac879f 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -334,6 +334,9 @@ export function createServer(options: ServerOptions) { case WS_METHODS.providersListSessions: return providerManager.listSessions(); + case WS_METHODS.providersCatchUp: + return persistenceService.providerCatchUp(request.params as never); + case WS_METHODS.providersListCheckpoints: return providerManager.listCheckpoints(request.params as never); diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index d36cca5c02ae..012f371e6581 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -6,6 +6,7 @@ import { } from "@tanstack/react-router"; import { useEffect, useRef } from "react"; import { QueryClient, useQueryClient } from "@tanstack/react-query"; +import type { ProviderEvent } from "@t3tools/contracts"; import { APP_DISPLAY_NAME } from "../branding"; import { Button } from "../components/ui/button"; @@ -120,6 +121,14 @@ function errorDetails(error: unknown): string { } } +function readProviderSeq(event: ProviderEvent): number | null { + const seq = event.seq; + if (typeof seq !== "number" || !Number.isInteger(seq) || seq <= 0) { + return null; + } + return seq; +} + function StateSyncRouter() { const api = useNativeApi(); const { dispatch } = useStore(); @@ -131,6 +140,8 @@ function StateSyncRouter() { }); const lastStateSeqRef = useRef(0); const stateQueueRef = useRef(Promise.resolve()); + const lastProviderSeqRef = useRef(0); + const providerQueueRef = useRef(Promise.resolve()); useEffect(() => { if (!api) return; @@ -202,7 +213,7 @@ function StateSyncRouter() { useEffect(() => { if (!api) return; - return api.providers.onEvent((event) => { + const applyProviderEvent = (event: ProviderEvent) => { if (event.method === "turn/completed") { void invalidateGitQueries(queryClient); } @@ -217,14 +228,73 @@ function StateSyncRouter() { }, }); } - if (!activeThreadId) return; dispatch({ type: "APPLY_EVENT", event, activeAssistantItemRef, activeThreadId, }); + }; + + const replayProviderCatchUp = async (): Promise => { + const catchUp = await api.providers.catchUp({ + afterSeq: lastProviderSeqRef.current, + }); + if (catchUp.events.length === 0) { + lastProviderSeqRef.current = Math.max(lastProviderSeqRef.current, catchUp.lastProviderSeq); + return; + } + + for (const missingEvent of catchUp.events) { + const missingSeq = readProviderSeq(missingEvent); + if (!missingSeq || missingSeq <= lastProviderSeqRef.current) continue; + applyProviderEvent(missingEvent); + lastProviderSeqRef.current = missingSeq; + } + + if (lastProviderSeqRef.current < catchUp.lastProviderSeq) { + await replayProviderCatchUp(); + } + }; + + const enqueueProviderWork = (work: () => Promise) => { + providerQueueRef.current = providerQueueRef.current.then(work).catch(() => undefined); + }; + + const unsubscribeWelcome = onServerWelcome(() => { + if (lastProviderSeqRef.current <= 0) { + return; + } + enqueueProviderWork(async () => { + await replayProviderCatchUp(); + }); }); + + const unsubscribeProvider = api.providers.onEvent((event) => { + enqueueProviderWork(async () => { + const eventSeq = readProviderSeq(event); + if (!eventSeq) { + applyProviderEvent(event); + return; + } + if (eventSeq <= lastProviderSeqRef.current) { + return; + } + if (lastProviderSeqRef.current > 0 && eventSeq > lastProviderSeqRef.current + 1) { + await replayProviderCatchUp(); + } + if (eventSeq <= lastProviderSeqRef.current) { + return; + } + applyProviderEvent(event); + lastProviderSeqRef.current = eventSeq; + }); + }); + + return () => { + unsubscribeWelcome(); + unsubscribeProvider(); + }; }, [activeThreadId, api, dispatch, queryClient]); useEffect(() => { diff --git a/apps/web/src/wsNativeApi.ts b/apps/web/src/wsNativeApi.ts index c3b4845b17c6..adb07b5ad61b 100644 --- a/apps/web/src/wsNativeApi.ts +++ b/apps/web/src/wsNativeApi.ts @@ -93,6 +93,7 @@ export function createWsNativeApi(): NativeApi { listSessions: () => transport.request(WS_METHODS.providersListSessions), listCheckpoints: (input) => transport.request(WS_METHODS.providersListCheckpoints, input), getCheckpointDiff: (input) => transport.request(WS_METHODS.providersGetCheckpointDiff, input), + catchUp: (input) => transport.request(WS_METHODS.providersCatchUp, input), revertToCheckpoint: (input) => transport.request(WS_METHODS.providersRevertToCheckpoint, input), onEvent: (callback) => diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index 5ad33c19bb7e..b613a90fa136 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -17,6 +17,8 @@ import type { GitStatusResult, } from "./git"; import type { + ProviderCatchUpInput, + ProviderCatchUpResult, ProviderEvent, ProviderGetCheckpointDiffInput, ProviderGetCheckpointDiffResult, @@ -118,6 +120,7 @@ export interface NativeApi { getCheckpointDiff: ( input: ProviderGetCheckpointDiffInput, ) => Promise; + catchUp: (input: ProviderCatchUpInput) => Promise; revertToCheckpoint: ( input: ProviderRevertToCheckpointInput, ) => Promise; diff --git a/packages/contracts/src/provider.test.ts b/packages/contracts/src/provider.test.ts index df5d52e42a81..68e3cf019ff8 100644 --- a/packages/contracts/src/provider.test.ts +++ b/packages/contracts/src/provider.test.ts @@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest"; import { PROVIDER_SEND_TURN_MAX_ATTACHMENTS, + providerCatchUpInputSchema, + providerCatchUpResultSchema, providerCheckpointSchema, providerEventSchema, providerGetCheckpointDiffInputSchema, @@ -156,6 +158,19 @@ describe("providerEventSchema", () => { expect(parsed.method).toBe("item/agentMessage/delta"); }); + it("accepts optional durable sequence metadata", () => { + const parsed = providerEventSchema.parse({ + seq: 42, + id: "evt_seq_1", + kind: "notification", + provider: "codex", + sessionId: "sess_1", + createdAt: "2026-01-01T00:00:00.000Z", + method: "turn/completed", + }); + expect(parsed.seq).toBe(42); + }); + it("accepts request approval metadata", () => { const parsed = providerEventSchema.parse({ id: "evt_2", @@ -172,6 +187,33 @@ describe("providerEventSchema", () => { }); }); +describe("provider catch-up schemas", () => { + it("parses catch-up input defaults", () => { + const parsed = providerCatchUpInputSchema.parse({}); + expect(parsed.afterSeq).toBe(0); + expect(parsed.limit).toBe(1_000); + }); + + it("parses catch-up result payload", () => { + const parsed = providerCatchUpResultSchema.parse({ + events: [ + { + seq: 7, + id: "evt_7", + kind: "notification", + provider: "codex", + sessionId: "sess_1", + createdAt: "2026-01-01T00:00:00.000Z", + method: "turn/started", + }, + ], + lastProviderSeq: 9, + }); + expect(parsed.events[0]?.seq).toBe(7); + expect(parsed.lastProviderSeq).toBe(9); + }); +}); + describe("providerRespondToRequestInputSchema", () => { it("accepts valid decisions", () => { const parsed = providerRespondToRequestInputSchema.parse({ diff --git a/packages/contracts/src/provider.ts b/packages/contracts/src/provider.ts index 695805ff0419..7ab62694148c 100644 --- a/packages/contracts/src/provider.ts +++ b/packages/contracts/src/provider.ts @@ -170,6 +170,11 @@ export const providerGetCheckpointDiffResultSchema = z.object({ diff: z.string(), }); +export const providerCatchUpInputSchema = z.object({ + afterSeq: z.number().int().min(0).default(0), + limit: z.number().int().min(1).max(5_000).default(1_000), +}); + export const providerRespondToRequestInputSchema = z.object({ sessionId: z.string().min(1), requestId: z.string().min(1), @@ -179,6 +184,7 @@ export const providerRespondToRequestInputSchema = z.object({ export const providerEventKindSchema = z.enum(["session", "notification", "request", "error"]); export const providerEventSchema = z.object({ + seq: z.number().int().positive().optional(), id: z.string().min(1), kind: providerEventKindSchema, provider: providerKindSchema, @@ -195,6 +201,11 @@ export const providerEventSchema = z.object({ payload: z.unknown().optional(), }); +export const providerCatchUpResultSchema = z.object({ + events: z.array(providerEventSchema), + lastProviderSeq: z.number().int().min(0), +}); + export type ProviderKind = z.infer; export type ProviderApprovalPolicy = z.infer; export type ProviderSandboxMode = z.infer; @@ -219,6 +230,8 @@ export type ProviderRevertToCheckpointResult = z.infer< >; export type ProviderGetCheckpointDiffInput = z.input; export type ProviderGetCheckpointDiffResult = z.infer; +export type ProviderCatchUpInput = z.input; +export type ProviderCatchUpResult = z.infer; export type ProviderRespondToRequestInput = z.input; export type ProviderEventKind = z.infer; export type ProviderEvent = z.infer; diff --git a/packages/contracts/src/ws.test.ts b/packages/contracts/src/ws.test.ts index 374b006886b2..ea57814ef564 100644 --- a/packages/contracts/src/ws.test.ts +++ b/packages/contracts/src/ws.test.ts @@ -7,4 +7,8 @@ describe("WS thread methods", () => { expect(WS_METHODS.threadsUpdateTerminalState).toBe("threads.updateTerminalState"); expect("threadsUpdate" in WS_METHODS).toBe(false); }); + + it("includes provider catch-up method for durable stream resume", () => { + expect(WS_METHODS.providersCatchUp).toBe("providers.catchUp"); + }); }); diff --git a/packages/contracts/src/ws.ts b/packages/contracts/src/ws.ts index 54c6d8dfce3d..c781ba9bbf5c 100644 --- a/packages/contracts/src/ws.ts +++ b/packages/contracts/src/ws.ts @@ -28,6 +28,7 @@ export const WS_METHODS = { providersListSessions: "providers.listSessions", providersListCheckpoints: "providers.listCheckpoints", providersGetCheckpointDiff: "providers.getCheckpointDiff", + providersCatchUp: "providers.catchUp", providersRevertToCheckpoint: "providers.revertToCheckpoint", // Project registry methods