From fb703ede73f51de9ac34db80998b82a85ab50e81 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 21 Aug 2026 19:28:04 -0400 Subject: [PATCH] fix(core): transfer only settled history (#44008) --- packages/core/src/session/transfer.ts | 10 +- packages/core/test/session-create.test.ts | 128 ++++++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) diff --git a/packages/core/src/session/transfer.ts b/packages/core/src/session/transfer.ts index 0e51c381266..3f7e403e60f 100644 --- a/packages/core/src/session/transfer.ts +++ b/packages/core/src/session/transfer.ts @@ -53,7 +53,7 @@ const layer = Layer.effect( export: Effect.fn("SessionTransfer.export")(function* (input) { const data = { info: yield* sessions.get(input.sessionID), - messages: yield* sessions.messages({ sessionID: input.sessionID, order: "asc" }), + messages: (yield* sessions.messages({ sessionID: input.sessionID, order: "asc" })).filter(isSettled), } return input.sanitize ? sanitize(data) : data }), @@ -68,7 +68,7 @@ const layer = Layer.effect( if (recorded) return yield* new ImportConflictError({ sessionID }) const project = yield* projects.resolve(input.location.directory) yield* upsertProject(db, project).pipe(Effect.orDie) - const messages = input.data.messages.map((message, index) => { + const messages = input.data.messages.filter(isSettled).map((message, index) => { const encoded = encodeMessage(message) const { id: _, type, ...data } = encoded return { @@ -144,6 +144,12 @@ export const node = makeGlobalNode({ deps: [App.node, Bus.node, Database.node, Project.node, Session.node], }) +function isSettled(message: SessionMessage.Info) { + if (message.type === "assistant") return message.time.completed !== undefined + if (message.type === "shell" || message.type === "compaction") return message.status !== "running" + return true +} + function redact(kind: string, id: string, value: string) { return value.trim() ? `[redacted:${kind}:${id}]` : value } diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index a9d60653801..432d3df1277 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -931,6 +931,134 @@ describe("Session.create", () => { }) describe("SessionTransfer", () => { + it.effect("exports only settled projected messages", () => + Effect.gen(function* () { + const session = yield* Session.Service + const transfer = yield* SessionTransfer.Service + const bus = yield* Bus.Service + const { db } = yield* Database.Service + const source = yield* session.create({ location }) + yield* session.prompt({ sessionID: source.id, text: "Settled", resume: false }) + yield* SessionInbox.promote(db, bus, source.id, "steer") + yield* bus.publish(SessionEvent.Step.Started, { + sessionID: source.id, + assistantMessageID: SessionMessage.ID.create(), + agent: Agent.ID.make("build"), + model: Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }), + }) + yield* bus.publish(SessionEvent.Shell.Started, { + sessionID: source.id, + shell: Shell.Info.make({ + id: Shell.ID.make("sh_transfer_export"), + status: "running", + command: "sleep 10", + cwd: location.directory, + shell: "/bin/sh", + file: "/tmp/sh_transfer_export.out", + metadata: {}, + time: { started: 0 }, + }), + }) + yield* bus.publish(SessionEvent.Compaction.Started, { + sessionID: source.id, + reason: "manual", + recent: "pending", + }) + + expect((yield* transfer.export({ sessionID: source.id })).messages).toMatchObject([ + { type: "user", text: "Settled" }, + ]) + }), + ) + + it.effect("imports only settled projected messages", () => + Effect.gen(function* () { + const session = yield* Session.Service + const transfer = yield* SessionTransfer.Service + const { db } = yield* Database.Service + const template = yield* session.create({ location, title: "Transfer source" }) + const sessionID = Session.ID.create() + const userID = SessionMessage.ID.create() + const runningAssistantID = SessionMessage.ID.create() + const completedAssistantID = SessionMessage.ID.create() + const runningShellID = SessionMessage.ID.create() + const completedShellID = SessionMessage.ID.create() + const runningCompactionID = SessionMessage.ID.create() + const completedCompactionID = SessionMessage.ID.create() + const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }) + + yield* transfer.import({ + data: { + info: { ...template, id: sessionID }, + messages: [ + { id: userID, type: "user", text: "Settled", time: { created: DateTime.makeUnsafe(1) } }, + { + id: runningAssistantID, + type: "assistant", + agent: Agent.ID.make("build"), + model, + content: [], + time: { created: DateTime.makeUnsafe(2) }, + }, + { + id: completedAssistantID, + type: "assistant", + agent: Agent.ID.make("build"), + model, + content: [], + time: { created: DateTime.makeUnsafe(3), completed: DateTime.makeUnsafe(4) }, + }, + { + id: runningShellID, + type: "shell", + shellID: Shell.ID.make("sh_transfer_running"), + command: "sleep 10", + status: "running", + time: { created: DateTime.makeUnsafe(5) }, + }, + { + id: completedShellID, + type: "shell", + shellID: Shell.ID.make("sh_transfer_completed"), + command: "pwd", + status: "exited", + exit: 0, + output: { output: "/project", cursor: 8, size: 8, truncated: false }, + time: { created: DateTime.makeUnsafe(6), completed: DateTime.makeUnsafe(7) }, + }, + { + id: runningCompactionID, + type: "compaction", + status: "running", + reason: "manual", + summary: "pending", + recent: "pending", + time: { created: DateTime.makeUnsafe(8) }, + }, + { + id: completedCompactionID, + type: "compaction", + status: "completed", + reason: "manual", + summary: "summary", + recent: "recent", + time: { created: DateTime.makeUnsafe(9) }, + }, + ], + }, + location, + }) + + expect((yield* session.messages({ sessionID, order: "asc" })).map((message) => message.id)).toEqual([ + userID, + completedAssistantID, + completedShellID, + completedCompactionID, + ]) + expect(yield* Bus.latestSequence(db, sessionID)).toBe(4) + }), + ) + it.effect("imports projected messages and reserves their aggregate sequence", () => Effect.gen(function* () { const session = yield* Session.Service