Skip to content

Commit fd3d027

Browse files
Apply PR #20142: refactor(session): effectify SessionSummary service
2 parents 1ad4396 + 9b8dbed commit fd3d027

File tree

3 files changed

+118
-104
lines changed

3 files changed

+118
-104
lines changed

packages/opencode/src/server/routes/session.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,13 +436,13 @@ export const SessionRoutes = lazy(() =>
436436
validator(
437437
"param",
438438
z.object({
439-
sessionID: SessionSummary.diff.schema.shape.sessionID,
439+
sessionID: SessionSummary.DiffInput.shape.sessionID,
440440
}),
441441
),
442442
validator(
443443
"query",
444444
z.object({
445-
messageID: SessionSummary.diff.schema.shape.messageID,
445+
messageID: SessionSummary.DiffInput.shape.messageID,
446446
}),
447447
),
448448
async (c) => {

packages/opencode/src/session/processor.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -294,12 +294,10 @@ export namespace SessionProcessor {
294294
}
295295
ctx.snapshot = undefined
296296
}
297-
yield* Effect.promise(() =>
298-
SessionSummary.summarize({
299-
sessionID: ctx.sessionID,
300-
messageID: ctx.assistantMessage.parentID,
301-
}),
302-
).pipe(Effect.ignoreCause({ log: true, message: "session summary failed" }), Effect.forkDetach)
297+
SessionSummary.summarize({
298+
sessionID: ctx.sessionID,
299+
messageID: ctx.assistantMessage.parentID,
300+
})
303301
if (
304302
!ctx.assistantMessage.summary &&
305303
isOverflow({ cfg: yield* config.get(), tokens: usage.tokens, model: ctx.model })
Lines changed: 112 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import { fn } from "@/util/fn"
21
import z from "zod"
2+
import { Effect, Layer, ServiceMap } from "effect"
3+
import { makeRuntime } from "@/effect/run-service"
4+
import { Bus } from "@/bus"
5+
import { Snapshot } from "@/snapshot"
6+
import { Storage } from "@/storage/storage"
37
import { Session } from "."
4-
58
import { MessageV2 } from "./message-v2"
69
import { SessionID, MessageID } from "./schema"
7-
import { Snapshot } from "@/snapshot"
8-
9-
import { Storage } from "@/storage/storage"
10-
import { Bus } from "@/bus"
11-
import { NotFoundError } from "@/storage/db"
1210

1311
export namespace SessionSummary {
1412
function unquoteGitPath(input: string) {
@@ -67,103 +65,121 @@ export namespace SessionSummary {
6765
return Buffer.from(bytes).toString()
6866
}
6967

70-
export const summarize = fn(
71-
z.object({
72-
sessionID: SessionID.zod,
73-
messageID: MessageID.zod,
74-
}),
75-
async (input) => {
76-
await Session.messages({ sessionID: input.sessionID })
77-
.then((all) =>
78-
Promise.all([
79-
summarizeSession({ sessionID: input.sessionID, messages: all }),
80-
summarizeMessage({ messageID: input.messageID, messages: all }),
81-
]),
82-
)
83-
.catch((err) => {
84-
if (NotFoundError.isInstance(err)) return
85-
throw err
68+
export interface Interface {
69+
readonly summarize: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect<void>
70+
readonly diff: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Snapshot.FileDiff[]>
71+
readonly computeDiff: (input: { messages: MessageV2.WithParts[] }) => Effect.Effect<Snapshot.FileDiff[]>
72+
}
73+
74+
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionSummary") {}
75+
76+
export const layer = Layer.effect(
77+
Service,
78+
Effect.gen(function* () {
79+
const sessions = yield* Session.Service
80+
const snapshot = yield* Snapshot.Service
81+
const storage = yield* Storage.Service
82+
const bus = yield* Bus.Service
83+
84+
const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: {
85+
messages: MessageV2.WithParts[]
86+
}) {
87+
let from: string | undefined
88+
let to: string | undefined
89+
for (const item of input.messages) {
90+
if (!from) {
91+
for (const part of item.parts) {
92+
if (part.type === "step-start" && part.snapshot) {
93+
from = part.snapshot
94+
break
95+
}
96+
}
97+
}
98+
for (const part of item.parts) {
99+
if (part.type === "step-finish" && part.snapshot) to = part.snapshot
100+
}
101+
}
102+
if (from && to) return yield* snapshot.diffFull(from, to)
103+
return []
104+
})
105+
106+
const summarize = Effect.fn("SessionSummary.summarize")(function* (input: {
107+
sessionID: SessionID
108+
messageID: MessageID
109+
}) {
110+
const all = yield* sessions.messages({ sessionID: input.sessionID })
111+
if (!all.length) return
112+
113+
const diffs = yield* computeDiff({ messages: all })
114+
yield* sessions.setSummary({
115+
sessionID: input.sessionID,
116+
summary: {
117+
additions: diffs.reduce((sum, x) => sum + x.additions, 0),
118+
deletions: diffs.reduce((sum, x) => sum + x.deletions, 0),
119+
files: diffs.length,
120+
},
86121
})
87-
},
88-
)
122+
yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore)
123+
yield* bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs })
89124

90-
async function summarizeSession(input: { sessionID: SessionID; messages: MessageV2.WithParts[] }) {
91-
const diffs = await computeDiff({ messages: input.messages })
92-
await Session.setSummary({
93-
sessionID: input.sessionID,
94-
summary: {
95-
additions: diffs.reduce((sum, x) => sum + x.additions, 0),
96-
deletions: diffs.reduce((sum, x) => sum + x.deletions, 0),
97-
files: diffs.length,
98-
},
99-
})
100-
await Storage.write(["session_diff", input.sessionID], diffs)
101-
Bus.publish(Session.Event.Diff, {
102-
sessionID: input.sessionID,
103-
diff: diffs,
104-
})
105-
}
125+
const messages = all.filter(
126+
(m) =>
127+
m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
128+
)
129+
const target = messages.find((m) => m.info.id === input.messageID)
130+
if (!target || target.info.role !== "user") return
131+
const msgDiffs = yield* computeDiff({ messages })
132+
target.info.summary = { ...target.info.summary, diffs: msgDiffs }
133+
yield* sessions.updateMessage(target.info)
134+
})
106135

107-
async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) {
108-
const messages = input.messages.filter(
109-
(m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
110-
)
111-
const msgWithParts = messages.find((m) => m.info.id === input.messageID)
112-
if (!msgWithParts || msgWithParts.info.role !== "user") return
113-
const userMsg = msgWithParts.info
114-
const diffs = await computeDiff({ messages })
115-
userMsg.summary = {
116-
...userMsg.summary,
117-
diffs,
118-
}
119-
await Session.updateMessage(userMsg)
120-
}
136+
const diff = Effect.fn("SessionSummary.diff")(function* (input: {
137+
sessionID: SessionID
138+
messageID?: MessageID
139+
}) {
140+
const diffs = yield* storage.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID]).pipe(
141+
Effect.catch(() => Effect.succeed([] as Snapshot.FileDiff[])),
142+
)
143+
const next = diffs.map((item) => {
144+
const file = unquoteGitPath(item.file)
145+
if (file === item.file) return item
146+
return { ...item, file }
147+
})
148+
const changed = next.some((item, i) => item.file !== diffs[i]?.file)
149+
if (changed) yield* storage.write(["session_diff", input.sessionID], next).pipe(Effect.ignore)
150+
return next
151+
})
121152

122-
export const diff = fn(
123-
z.object({
124-
sessionID: SessionID.zod,
125-
messageID: MessageID.zod.optional(),
153+
return Service.of({ summarize, diff, computeDiff })
126154
}),
127-
async (input) => {
128-
const diffs = await Storage.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID]).catch(() => [])
129-
const next = diffs.map((item) => {
130-
const file = unquoteGitPath(item.file)
131-
if (file === item.file) return item
132-
return {
133-
...item,
134-
file,
135-
}
136-
})
137-
const changed = next.some((item, i) => item.file !== diffs[i]?.file)
138-
if (changed) Storage.write(["session_diff", input.sessionID], next).catch(() => {})
139-
return next
140-
},
141155
)
142156

143-
export async function computeDiff(input: { messages: MessageV2.WithParts[] }) {
144-
let from: string | undefined
145-
let to: string | undefined
146-
147-
// scan assistant messages to find earliest from and latest to
148-
// snapshot
149-
for (const item of input.messages) {
150-
if (!from) {
151-
for (const part of item.parts) {
152-
if (part.type === "step-start" && part.snapshot) {
153-
from = part.snapshot
154-
break
155-
}
156-
}
157-
}
157+
export const defaultLayer = Layer.unwrap(
158+
Effect.sync(() =>
159+
layer.pipe(
160+
Layer.provide(Session.defaultLayer),
161+
Layer.provide(Snapshot.defaultLayer),
162+
Layer.provide(Storage.defaultLayer),
163+
Layer.provide(Bus.layer),
164+
),
165+
),
166+
)
158167

159-
for (const part of item.parts) {
160-
if (part.type === "step-finish" && part.snapshot) {
161-
to = part.snapshot
162-
}
163-
}
164-
}
168+
const { runPromise } = makeRuntime(Service, defaultLayer)
165169

166-
if (from && to) return Snapshot.diffFull(from, to)
167-
return []
170+
export const summarize = (input: { sessionID: SessionID; messageID: MessageID }) =>
171+
void runPromise((svc) => svc.summarize(input)).catch(() => {})
172+
173+
export const DiffInput = z.object({
174+
sessionID: SessionID.zod,
175+
messageID: MessageID.zod.optional(),
176+
})
177+
178+
export async function diff(input: z.infer<typeof DiffInput>) {
179+
return runPromise((svc) => svc.diff(input))
180+
}
181+
182+
export async function computeDiff(input: { messages: MessageV2.WithParts[] }) {
183+
return runPromise((svc) => svc.computeDiff(input))
168184
}
169185
}

0 commit comments

Comments
 (0)