+21

![opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



![opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



James Long
Brendan Allan
Kit Langton
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Affan Ali
affanali2k3
Frank
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
Aiden Cline
Jay V
Dax Raad
Aarav Sareen
OpeOginni
Luke Parker
Ben Guthrie
Dax
Filip
Max Anderson
Brendan Allan
Jack
Shoubhit Dash
Dustin Deus
starptech
Aiden Cline
usrnk1
Jay
runvip
opencode
Julian Coy
Vladimir Glafirov
8c94e9005f
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
147 lines
5.6 KiB
TypeScript
147 lines
5.6 KiB
TypeScript
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
import { Effect, Layer, Context, Schema } from "effect"
|
|
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
|
import { EventV2Bridge } from "@/event-v2-bridge"
|
|
import { Snapshot } from "../snapshot"
|
|
import { Storage } from "@/storage/storage"
|
|
import { Session } from "./session"
|
|
import { MessageV2 } from "./message-v2"
|
|
import { SessionID, MessageID, PartID } from "./schema"
|
|
import { SessionRunState } from "./run-state"
|
|
import { SessionSummary } from "./summary"
|
|
|
|
export const RevertInput = Schema.Struct({
|
|
sessionID: SessionID,
|
|
messageID: MessageID,
|
|
partID: Schema.optional(PartID),
|
|
})
|
|
export type RevertInput = Schema.Schema.Type<typeof RevertInput>
|
|
|
|
export interface Interface {
|
|
readonly revert: (input: RevertInput) => Effect.Effect<Session.Info, Session.BusyError>
|
|
readonly unrevert: (input: { sessionID: SessionID }) => Effect.Effect<Session.Info, Session.BusyError>
|
|
readonly cleanup: (session: Session.Info) => Effect.Effect<void>
|
|
}
|
|
|
|
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRevert") {}
|
|
|
|
const layer = Layer.effect(
|
|
Service,
|
|
Effect.gen(function* () {
|
|
const sessions = yield* Session.Service
|
|
const snap = yield* Snapshot.Service
|
|
const storage = yield* Storage.Service
|
|
const events = yield* EventV2Bridge.Service
|
|
const summary = yield* SessionSummary.Service
|
|
const state = yield* SessionRunState.Service
|
|
|
|
const revert = Effect.fn("SessionRevert.revert")(function* (input: RevertInput) {
|
|
yield* state.assertNotBusy(input.sessionID)
|
|
const all = yield* sessions.messages({ sessionID: input.sessionID }).pipe(Effect.orDie)
|
|
let lastUser: SessionV1.User | undefined
|
|
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
|
|
|
|
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)
|
|
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* events.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).pipe(Effect.orDie)
|
|
})
|
|
|
|
const unrevert = Effect.fn("SessionRevert.unrevert")(function* (input: { sessionID: SessionID }) {
|
|
yield* Effect.logInfo("unreverting", { sessionID: input.sessionID })
|
|
yield* state.assertNotBusy(input.sessionID)
|
|
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
|
|
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).pipe(Effect.orDie)
|
|
})
|
|
|
|
const cleanup = Effect.fn("SessionRevert.cleanup")(function* (session: Session.Info) {
|
|
if (!session.revert) return
|
|
const sessionID = session.id
|
|
const msgs = yield* sessions.messages({ sessionID }).pipe(Effect.orDie)
|
|
const messageID = session.revert.messageID
|
|
const remove = [] as SessionV1.WithParts[]
|
|
let target: SessionV1.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* sessions.removeMessage({ 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* sessions.removePart({ sessionID, messageID: target.info.id, partID: part.id })
|
|
}
|
|
}
|
|
}
|
|
yield* sessions.clearRevert(sessionID)
|
|
})
|
|
|
|
return Service.of({ revert, unrevert, cleanup })
|
|
}),
|
|
)
|
|
|
|
export const node = LayerNode.make({
|
|
service: Service,
|
|
layer: layer,
|
|
deps: [Session.node, Snapshot.node, Storage.node, EventV2Bridge.node, SessionSummary.node, SessionRunState.node],
|
|
})
|
|
|
|
export * as SessionRevert from "./revert"
|