diff --git a/src/features/Org2Cloud/org2CloudSessionSync.ts b/src/features/Org2Cloud/org2CloudSessionSync.ts index 17727991b..b07561e8a 100644 --- a/src/features/Org2Cloud/org2CloudSessionSync.ts +++ b/src/features/Org2Cloud/org2CloudSessionSync.ts @@ -1,4 +1,5 @@ import { getImportedHistorySourceBySessionId } from "@src/api/tauri/externalHistory"; +import { rpc } from "@src/api/tauri/rpc"; import { eventStoreProxy } from "@src/engines/SessionCore/core/store/EventStoreProxy"; import type { SessionEvent } from "@src/engines/SessionCore/core/types"; import { processChunksRust } from "@src/engines/SessionCore/ingestion/rustBridge"; @@ -7,7 +8,11 @@ import { createLogger } from "@src/hooks/logger"; import { COLLAB_SESSION_ACCESS_MODE } from "@src/store/collaboration/types"; import type { RemoteTeammateSessionMetadata } from "@src/store/collaboration/types"; import type { Session } from "@src/store/session/sessionAtom/types"; -import { isImportedHistorySession } from "@src/util/session/sessionDispatch"; +import type { ActivityChunk } from "@src/types/session/session"; +import { + isCliSession, + isImportedHistorySession, +} from "@src/util/session/sessionDispatch"; import { sha256Hex, @@ -323,7 +328,16 @@ export class Org2CloudSessionSync extends Org2CloudSessionSyncState { if (!Array.isArray(chunks) || chunks.length === 0) return []; return processChunksRust(chunks, sessionId); } - return eventStoreProxy.getPersistedEvents(sessionId); + const persisted = await eventStoreProxy.getPersistedEvents(sessionId); + if (persisted.length > 0 || !isCliSession(sessionId)) return persisted; + // Live CLI sessions keep their transcript of record in the CLI's native + // store (account-profile aware) and never write the events cache, so a + // persisted read alone pushes a hollow session: metadata with no replay, + // and the pass then stamps the event plane clean. Load the full native + // transcript through the same command the session-resume path uses. + const chunks = (await rpc.cli.chunks({ sessionId })) as ActivityChunk[]; + if (!Array.isArray(chunks) || chunks.length === 0) return []; + return processChunksRust(chunks, sessionId); } private preparePushEventsForPass( diff --git a/src/features/Org2Cloud/org2CloudSyncEngine.sessions.test.ts b/src/features/Org2Cloud/org2CloudSyncEngine.sessions.test.ts index 72532f3f1..3ec8c519b 100644 --- a/src/features/Org2Cloud/org2CloudSyncEngine.sessions.test.ts +++ b/src/features/Org2Cloud/org2CloudSyncEngine.sessions.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { rpc } from "@src/api/tauri/rpc"; import type { SessionEvent } from "@src/engines/SessionCore/core/types"; import type { Session } from "@src/store/session/sessionAtom/types"; @@ -89,6 +90,46 @@ describe("Org2CloudSyncEngine session publishing", () => { "cursoride-thread-1" ); }); + + it("publishes live CLI sessions from the native transcript when the events cache is empty", async () => { + eventStoreMock.getPersistedEvents.mockResolvedValueOnce([]); + const chunks = [{ id: "cli-chunk" }] as never; + const chunksSpy = vi.spyOn(rpc.cli, "chunks").mockResolvedValue(chunks); + const converted = [makeEvent("cli-event")]; + processChunksRustMock.mockResolvedValueOnce(converted); + + const events = await ( + engine as unknown as { + loadPushEvents(sessionId: string): Promise; + } + ).loadPushEvents("cliagent-123-native"); + + expect(events).toEqual(converted); + expect(chunksSpy).toHaveBeenCalledWith({ + sessionId: "cliagent-123-native", + }); + expect(processChunksRustMock).toHaveBeenCalledWith( + chunks, + "cliagent-123-native" + ); + chunksSpy.mockRestore(); + }); + + it("prefers the persisted event cache for CLI sessions when it is populated", async () => { + const persisted = [makeEvent("persisted-cli-event")]; + eventStoreMock.getPersistedEvents.mockResolvedValueOnce(persisted); + const chunksSpy = vi.spyOn(rpc.cli, "chunks"); + + const events = await ( + engine as unknown as { + loadPushEvents(sessionId: string): Promise; + } + ).loadPushEvents("cliagent-123-native"); + + expect(events).toEqual(persisted); + expect(chunksSpy).not.toHaveBeenCalled(); + chunksSpy.mockRestore(); + }); it("pushes only scope-matched own sessions (metadata + epoch-1 rewrite)", async () => { store.set(sessionsAtom, [ SESSION,