fix(core): transfer only settled history (#44008)

This commit is contained in:
Kit Langton
2026-08-21 19:28:04 -04:00
committed by GitHub
Unverified
parent 2937f0e635
commit fb703ede73
2 changed files with 136 additions and 2 deletions
+8 -2
View File
@@ -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
}
+128
View File
@@ -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