saeeol 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/session/compaction-helpers.ts +1 -169
- package/src/session/compaction.ts +1 -712
- package/src/session/core/compaction/compaction-helpers.ts +169 -0
- package/src/session/core/compaction/compaction.ts +712 -0
- package/src/session/core/compaction/overflow.ts +28 -0
- package/src/session/core/instruction.ts +234 -0
- package/src/session/core/llm.ts +504 -0
- package/src/session/core/network.ts +392 -0
- package/src/session/core/processor.ts +731 -0
- package/src/session/core/projectors.ts +139 -0
- package/src/session/core/resolve-tools.ts +241 -0
- package/src/session/core/retry.ts +149 -0
- package/src/session/core/revert.ts +173 -0
- package/src/session/core/run-state.ts +110 -0
- package/src/session/core/schema.ts +35 -0
- package/src/session/core/session-types.ts +160 -0
- package/src/session/core/session.sql.ts +124 -0
- package/src/session/core/session.ts +948 -0
- package/src/session/core/shell-exec.ts +205 -0
- package/src/session/core/status.ts +100 -0
- package/src/session/core/subtask.ts +268 -0
- package/src/session/core/summary.ts +173 -0
- package/src/session/core/system.ts +114 -0
- package/src/session/core/todo.ts +86 -0
- package/src/session/core/user-part.ts +293 -0
- package/src/session/instruction.ts +1 -234
- package/src/session/llm.ts +1 -504
- package/src/session/message/message-errors.ts +83 -0
- package/src/session/message/message-parts.ts +89 -0
- package/src/session/message/message-query.ts +107 -0
- package/src/session/message/message-transform.ts +156 -0
- package/src/session/message/message-types.ts +68 -0
- package/src/session/message/message-v2.ts +73 -0
- package/src/session/message/message.ts +192 -0
- package/src/session/message-errors.ts +1 -83
- package/src/session/message-parts.ts +1 -89
- package/src/session/message-query.ts +1 -107
- package/src/session/message-transform.ts +1 -156
- package/src/session/message-types.ts +1 -68
- package/src/session/message-v2.ts +1 -73
- package/src/session/message.ts +1 -192
- package/src/session/network.ts +1 -392
- package/src/session/overflow.ts +1 -28
- package/src/session/processor.ts +1 -731
- package/src/session/projectors.ts +2 -139
- package/src/session/prompt/prompt-command.ts +93 -0
- package/src/session/prompt/prompt-loop.ts +299 -0
- package/src/session/prompt/prompt-model.ts +44 -0
- package/src/session/prompt/prompt-reminders.ts +120 -0
- package/src/session/prompt/prompt-resolve.ts +42 -0
- package/src/session/prompt/prompt-schemas.ts +128 -0
- package/src/session/prompt/prompt-title.ts +55 -0
- package/src/session/prompt/prompt-types.ts +47 -0
- package/src/session/prompt/prompt-user-msg.ts +80 -0
- package/src/session/prompt/prompt.ts +211 -0
- package/src/session/prompt-command.ts +1 -93
- package/src/session/prompt-loop.ts +1 -299
- package/src/session/prompt-model.ts +1 -44
- package/src/session/prompt-reminders.ts +1 -120
- package/src/session/prompt-resolve.ts +1 -42
- package/src/session/prompt-schemas.ts +1 -128
- package/src/session/prompt-title.ts +1 -55
- package/src/session/prompt-types.ts +1 -47
- package/src/session/prompt-user-msg.ts +1 -80
- package/src/session/prompt.ts +1 -211
- package/src/session/resolve-tools.ts +1 -241
- package/src/session/retry.ts +1 -149
- package/src/session/revert.ts +1 -173
- package/src/session/run-state.ts +1 -110
- package/src/session/schema.ts +1 -35
- package/src/session/session-types.ts +1 -160
- package/src/session/session.sql.ts +1 -124
- package/src/session/session.ts +1 -948
- package/src/session/shell-exec.ts +1 -205
- package/src/session/status.ts +1 -100
- package/src/session/subtask.ts +1 -268
- package/src/session/summary.ts +1 -173
- package/src/session/system.ts +1 -114
- package/src/session/todo.ts +1 -86
- package/src/session/user-part.ts +1 -293
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
import { Slug } from "@saeeol/core/util/slug"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { BusEvent } from "@/bus/bus-event"
|
|
4
|
+
import { Bus } from "@/bus"
|
|
5
|
+
import { Decimal } from "decimal.js"
|
|
6
|
+
import z from "zod"
|
|
7
|
+
import { type ProviderMetadata, type LanguageModelUsage } from "ai"
|
|
8
|
+
import { Flag } from "@saeeol/core/flag/flag"
|
|
9
|
+
import { InstallationVersion } from "@saeeol/core/installation/version"
|
|
10
|
+
|
|
11
|
+
import { Database } from "@/storage/db"
|
|
12
|
+
import { NotFoundError } from "@/storage/storage"
|
|
13
|
+
import { eq, and, gte, isNull, desc, like, or } from "drizzle-orm"
|
|
14
|
+
import { SyncEvent } from "../../sync"
|
|
15
|
+
import { PartTable, SessionTable } from "./session.sql"
|
|
16
|
+
import { Storage } from "@/storage/storage"
|
|
17
|
+
import * as Log from "@saeeol/core/util/log"
|
|
18
|
+
import { MessageV2 } from "../message/message-v2"
|
|
19
|
+
import type { InstanceContext } from "../../project/instance"
|
|
20
|
+
import { InstanceState } from "@/effect/instance-state"
|
|
21
|
+
import { Instance } from "@/project/instance"
|
|
22
|
+
import { Snapshot } from "@/snapshot"
|
|
23
|
+
import { ProjectID } from "../../project/schema"
|
|
24
|
+
import { WorkspaceID } from "../../control-plane/schema"
|
|
25
|
+
import { SessionID, MessageID, PartID } from "./schema"
|
|
26
|
+
|
|
27
|
+
import type { Provider } from "@/provider/provider"
|
|
28
|
+
import { Permission } from "@/permission"
|
|
29
|
+
import { Global } from "@saeeol/core/global"
|
|
30
|
+
import { makeRuntime } from "@/effect/run-service"
|
|
31
|
+
import { SaeeolSession, saeeolSessionFork } from "@/saeeol/session"
|
|
32
|
+
import { fn } from "@/util/fn"
|
|
33
|
+
import { Effect, Layer, Option, Context, Schema, Types } from "effect"
|
|
34
|
+
import { zod } from "@/util/effect-zod"
|
|
35
|
+
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@/util/schema"
|
|
36
|
+
|
|
37
|
+
const log = Log.create({ service: "session" })
|
|
38
|
+
|
|
39
|
+
const parentTitlePrefix = "New session - "
|
|
40
|
+
const childTitlePrefix = "Child session - "
|
|
41
|
+
|
|
42
|
+
function createDefaultTitle(isChild = false) {
|
|
43
|
+
return (isChild ? childTitlePrefix : parentTitlePrefix) + new Date().toISOString()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isDefaultTitle(title: string) {
|
|
47
|
+
return new RegExp(
|
|
48
|
+
`^(${parentTitlePrefix}|${childTitlePrefix})\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$`,
|
|
49
|
+
).test(title)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type SessionRow = typeof SessionTable.$inferSelect
|
|
53
|
+
|
|
54
|
+
export function fromRow(row: SessionRow): Info {
|
|
55
|
+
const summary =
|
|
56
|
+
row.summary_additions !== null || row.summary_deletions !== null || row.summary_files !== null
|
|
57
|
+
? {
|
|
58
|
+
additions: row.summary_additions ?? 0,
|
|
59
|
+
deletions: row.summary_deletions ?? 0,
|
|
60
|
+
files: row.summary_files ?? 0,
|
|
61
|
+
diffs: row.summary_diffs ?? undefined,
|
|
62
|
+
}
|
|
63
|
+
: undefined
|
|
64
|
+
const share = row.share_url ? { url: row.share_url } : undefined
|
|
65
|
+
const revert = row.revert ?? undefined
|
|
66
|
+
return {
|
|
67
|
+
id: row.id,
|
|
68
|
+
slug: row.slug,
|
|
69
|
+
projectID: row.project_id,
|
|
70
|
+
workspaceID: row.workspace_id ?? undefined,
|
|
71
|
+
directory: row.directory,
|
|
72
|
+
path: row.path ?? undefined,
|
|
73
|
+
parentID: row.parent_id ?? undefined,
|
|
74
|
+
title: row.title,
|
|
75
|
+
version: row.version,
|
|
76
|
+
summary,
|
|
77
|
+
share,
|
|
78
|
+
revert,
|
|
79
|
+
permission: row.permission ?? undefined,
|
|
80
|
+
time: {
|
|
81
|
+
created: row.time_created,
|
|
82
|
+
updated: row.time_updated,
|
|
83
|
+
compacting: row.time_compacting ?? undefined,
|
|
84
|
+
archived: row.time_archived ?? undefined,
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function toRow(info: Info) {
|
|
90
|
+
return {
|
|
91
|
+
id: info.id,
|
|
92
|
+
project_id: info.projectID,
|
|
93
|
+
workspace_id: info.workspaceID,
|
|
94
|
+
parent_id: info.parentID,
|
|
95
|
+
slug: info.slug,
|
|
96
|
+
directory: info.directory,
|
|
97
|
+
path: info.path,
|
|
98
|
+
title: info.title,
|
|
99
|
+
version: info.version,
|
|
100
|
+
share_url: info.share?.url,
|
|
101
|
+
summary_additions: info.summary?.additions,
|
|
102
|
+
summary_deletions: info.summary?.deletions,
|
|
103
|
+
summary_files: info.summary?.files,
|
|
104
|
+
summary_diffs: info.summary?.diffs,
|
|
105
|
+
revert: info.revert ?? null,
|
|
106
|
+
permission: info.permission,
|
|
107
|
+
time_created: info.time.created,
|
|
108
|
+
time_updated: info.time.updated,
|
|
109
|
+
time_compacting: info.time.compacting,
|
|
110
|
+
time_archived: info.time.archived,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getForkedTitle(title: string): string {
|
|
115
|
+
const match = title.match(/^(.+) \(fork #(\d+)\)$/)
|
|
116
|
+
if (match) {
|
|
117
|
+
const base = match[1]
|
|
118
|
+
const num = parseInt(match[2], 10)
|
|
119
|
+
return `${base} (fork #${num + 1})`
|
|
120
|
+
}
|
|
121
|
+
return `${title} (fork #1)`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sessionPath(worktree: string, cwd: string) {
|
|
125
|
+
return path.relative(path.resolve(worktree), cwd).replaceAll("\\", "/")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const Summary = Schema.Struct({
|
|
129
|
+
additions: NonNegativeInt,
|
|
130
|
+
deletions: NonNegativeInt,
|
|
131
|
+
files: NonNegativeInt,
|
|
132
|
+
diffs: optionalOmitUndefined(Schema.Array(Snapshot.SummaryFileDiff)),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const Share = Schema.Struct({
|
|
136
|
+
url: Schema.String,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Legacy HTTP accepted negative values here. Keep archive timestamps permissive
|
|
140
|
+
// while excluding non-finite values that cannot round-trip through JSON.
|
|
141
|
+
export const ArchivedTimestamp = Schema.Finite
|
|
142
|
+
|
|
143
|
+
const Time = Schema.Struct({
|
|
144
|
+
created: NonNegativeInt,
|
|
145
|
+
updated: NonNegativeInt,
|
|
146
|
+
compacting: optionalOmitUndefined(NonNegativeInt),
|
|
147
|
+
archived: optionalOmitUndefined(ArchivedTimestamp),
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const Revert = Schema.Struct({
|
|
151
|
+
messageID: MessageID,
|
|
152
|
+
partID: optionalOmitUndefined(PartID),
|
|
153
|
+
snapshot: optionalOmitUndefined(Schema.String),
|
|
154
|
+
diff: optionalOmitUndefined(Schema.String),
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
export const Info = Schema.Struct({
|
|
158
|
+
id: SessionID,
|
|
159
|
+
slug: Schema.String,
|
|
160
|
+
projectID: ProjectID,
|
|
161
|
+
workspaceID: optionalOmitUndefined(WorkspaceID),
|
|
162
|
+
directory: Schema.String,
|
|
163
|
+
path: optionalOmitUndefined(Schema.String),
|
|
164
|
+
parentID: optionalOmitUndefined(SessionID),
|
|
165
|
+
summary: optionalOmitUndefined(Summary),
|
|
166
|
+
share: optionalOmitUndefined(Share),
|
|
167
|
+
title: Schema.String,
|
|
168
|
+
version: Schema.String,
|
|
169
|
+
time: Time,
|
|
170
|
+
permission: optionalOmitUndefined(Permission.Ruleset),
|
|
171
|
+
revert: optionalOmitUndefined(Revert),
|
|
172
|
+
})
|
|
173
|
+
.annotate({ identifier: "Session" })
|
|
174
|
+
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
175
|
+
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
|
176
|
+
|
|
177
|
+
export const ProjectInfo = Schema.Struct({
|
|
178
|
+
id: ProjectID,
|
|
179
|
+
name: optionalOmitUndefined(Schema.String),
|
|
180
|
+
worktree: Schema.String,
|
|
181
|
+
})
|
|
182
|
+
.annotate({ identifier: "ProjectSummary" })
|
|
183
|
+
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
184
|
+
export type ProjectInfo = Types.DeepMutable<Schema.Schema.Type<typeof ProjectInfo>>
|
|
185
|
+
|
|
186
|
+
export const GlobalInfo = Schema.Struct({
|
|
187
|
+
...Info.fields,
|
|
188
|
+
project: Schema.NullOr(ProjectInfo),
|
|
189
|
+
worktreeName: Schema.optional(Schema.String),
|
|
190
|
+
})
|
|
191
|
+
.annotate({ identifier: "GlobalSession" })
|
|
192
|
+
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
193
|
+
export type GlobalInfo = Types.DeepMutable<Schema.Schema.Type<typeof GlobalInfo>>
|
|
194
|
+
|
|
195
|
+
export const CreateInput = Schema.optional(
|
|
196
|
+
Schema.Struct({
|
|
197
|
+
parentID: Schema.optional(SessionID),
|
|
198
|
+
title: Schema.optional(Schema.String),
|
|
199
|
+
permission: Schema.optional(Permission.Ruleset),
|
|
200
|
+
platform: Schema.optional(Schema.String),
|
|
201
|
+
workspaceID: Schema.optional(WorkspaceID),
|
|
202
|
+
}),
|
|
203
|
+
).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
204
|
+
export type CreateInput = Types.DeepMutable<Schema.Schema.Type<typeof CreateInput>>
|
|
205
|
+
|
|
206
|
+
export const ForkInput = Schema.Struct({
|
|
207
|
+
sessionID: SessionID,
|
|
208
|
+
messageID: Schema.optional(MessageID),
|
|
209
|
+
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
210
|
+
export const GetInput = SessionID
|
|
211
|
+
export const ChildrenInput = SessionID
|
|
212
|
+
export const RemoveInput = SessionID
|
|
213
|
+
export const SetTitleInput = Schema.Struct({ sessionID: SessionID, title: Schema.String }).pipe(
|
|
214
|
+
withStatics((s) => ({ zod: zod(s) })),
|
|
215
|
+
)
|
|
216
|
+
export const SetArchivedInput = Schema.Struct({
|
|
217
|
+
sessionID: SessionID,
|
|
218
|
+
time: Schema.optional(ArchivedTimestamp),
|
|
219
|
+
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
220
|
+
export const SetPermissionInput = Schema.Struct({
|
|
221
|
+
sessionID: SessionID,
|
|
222
|
+
permission: Permission.Ruleset,
|
|
223
|
+
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
224
|
+
export const SetRevertInput = Schema.Struct({
|
|
225
|
+
sessionID: SessionID,
|
|
226
|
+
revert: Schema.optional(Revert),
|
|
227
|
+
summary: Schema.optional(Summary),
|
|
228
|
+
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
229
|
+
export const MessagesInput = Schema.Struct({
|
|
230
|
+
sessionID: SessionID,
|
|
231
|
+
limit: Schema.optional(NonNegativeInt),
|
|
232
|
+
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
233
|
+
export type ListInput = {
|
|
234
|
+
directory?: string
|
|
235
|
+
scope?: "project"
|
|
236
|
+
path?: string
|
|
237
|
+
workspaceID?: WorkspaceID
|
|
238
|
+
roots?: boolean
|
|
239
|
+
start?: number
|
|
240
|
+
search?: string
|
|
241
|
+
limit?: number
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const CreatedEventSchema = Schema.Struct({
|
|
245
|
+
sessionID: SessionID,
|
|
246
|
+
info: Info,
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const UpdatedShare = Schema.Struct({
|
|
250
|
+
url: Schema.optional(Schema.NullOr(Schema.String)),
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const UpdatedTime = Schema.Struct({
|
|
254
|
+
created: Schema.optional(Schema.NullOr(NonNegativeInt)),
|
|
255
|
+
updated: Schema.optional(Schema.NullOr(NonNegativeInt)),
|
|
256
|
+
compacting: Schema.optional(Schema.NullOr(NonNegativeInt)),
|
|
257
|
+
archived: Schema.optional(Schema.NullOr(ArchivedTimestamp)),
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const UpdatedInfo = Schema.Struct({
|
|
261
|
+
id: Schema.optional(Schema.NullOr(SessionID)),
|
|
262
|
+
slug: Schema.optional(Schema.NullOr(Schema.String)),
|
|
263
|
+
projectID: Schema.optional(Schema.NullOr(ProjectID)),
|
|
264
|
+
workspaceID: Schema.optional(Schema.NullOr(WorkspaceID)),
|
|
265
|
+
directory: Schema.optional(Schema.NullOr(Schema.String)),
|
|
266
|
+
path: Schema.optional(Schema.NullOr(Schema.String)),
|
|
267
|
+
parentID: Schema.optional(Schema.NullOr(SessionID)),
|
|
268
|
+
summary: Schema.optional(Schema.NullOr(Summary)),
|
|
269
|
+
share: Schema.optional(UpdatedShare),
|
|
270
|
+
title: Schema.optional(Schema.NullOr(Schema.String)),
|
|
271
|
+
version: Schema.optional(Schema.NullOr(Schema.String)),
|
|
272
|
+
time: Schema.optional(UpdatedTime),
|
|
273
|
+
permission: Schema.optional(Schema.NullOr(Permission.Ruleset)),
|
|
274
|
+
revert: Schema.optional(Schema.NullOr(Revert)),
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const UpdatedEventSchema = Schema.Struct({
|
|
278
|
+
sessionID: SessionID,
|
|
279
|
+
info: UpdatedInfo,
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
export const Event = {
|
|
283
|
+
Created: SyncEvent.define({
|
|
284
|
+
type: "session.created",
|
|
285
|
+
version: 1,
|
|
286
|
+
aggregate: "sessionID",
|
|
287
|
+
schema: CreatedEventSchema,
|
|
288
|
+
}),
|
|
289
|
+
Updated: SyncEvent.define({
|
|
290
|
+
type: "session.updated",
|
|
291
|
+
version: 1,
|
|
292
|
+
aggregate: "sessionID",
|
|
293
|
+
schema: UpdatedEventSchema,
|
|
294
|
+
busSchema: CreatedEventSchema,
|
|
295
|
+
}),
|
|
296
|
+
Deleted: SyncEvent.define({
|
|
297
|
+
type: "session.deleted",
|
|
298
|
+
version: 1,
|
|
299
|
+
aggregate: "sessionID",
|
|
300
|
+
schema: CreatedEventSchema,
|
|
301
|
+
}),
|
|
302
|
+
Diff: BusEvent.define(
|
|
303
|
+
"session.diff",
|
|
304
|
+
Schema.Struct({
|
|
305
|
+
sessionID: SessionID,
|
|
306
|
+
diff: Schema.Array(Snapshot.FileDiff),
|
|
307
|
+
}),
|
|
308
|
+
),
|
|
309
|
+
Error: BusEvent.define(
|
|
310
|
+
"session.error",
|
|
311
|
+
Schema.Struct({
|
|
312
|
+
sessionID: Schema.optional(SessionID),
|
|
313
|
+
// Reuses MessageV2.Assistant.fields.error (already Schema.optional) so
|
|
314
|
+
// the derived zod keeps the same discriminated-union shape on the bus.
|
|
315
|
+
error: MessageV2.Assistant.fields.error,
|
|
316
|
+
}),
|
|
317
|
+
),
|
|
318
|
+
TurnOpen: SaeeolSession.Event.TurnOpen,
|
|
319
|
+
TurnClose: SaeeolSession.Event.TurnClose,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function plan(input: { slug: string; time: { created: number } }, instance: InstanceContext) {
|
|
323
|
+
const base = instance.project.vcs
|
|
324
|
+
? path.join(instance.worktree, ".saeeol", "plans")
|
|
325
|
+
: path.join(Global.Path.data, "plans")
|
|
326
|
+
return path.join(base, [input.time.created, input.slug].join("-") + ".md")
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export const getUsage = (input: {
|
|
330
|
+
model: Provider.Model
|
|
331
|
+
usage: LanguageModelUsage
|
|
332
|
+
metadata?: ProviderMetadata
|
|
333
|
+
provider?: Provider.Info
|
|
334
|
+
}) => {
|
|
335
|
+
const safe = (value: number) => {
|
|
336
|
+
if (!Number.isFinite(value)) return 0
|
|
337
|
+
return value
|
|
338
|
+
}
|
|
339
|
+
const inputTokens = safe(input.usage.inputTokens ?? 0)
|
|
340
|
+
const outputTokens = safe(input.usage.outputTokens ?? 0)
|
|
341
|
+
const reasoningTokens = safe(input.usage.outputTokenDetails?.reasoningTokens ?? input.usage.reasoningTokens ?? 0)
|
|
342
|
+
|
|
343
|
+
const cacheReadInputTokens = safe(
|
|
344
|
+
input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0,
|
|
345
|
+
)
|
|
346
|
+
const cacheWriteInputTokens = safe(
|
|
347
|
+
Number(
|
|
348
|
+
input.usage.inputTokenDetails?.cacheWriteTokens ??
|
|
349
|
+
input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
|
350
|
+
// google-vertex-anthropic returns metadata under "vertex" key
|
|
351
|
+
// (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages')
|
|
352
|
+
input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ??
|
|
353
|
+
// @ts-expect-error
|
|
354
|
+
input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
|
|
355
|
+
// @ts-expect-error
|
|
356
|
+
input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ??
|
|
357
|
+
0,
|
|
358
|
+
),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
// AI SDK v6 normalized inputTokens to include cached tokens across all providers
|
|
362
|
+
// (including Anthropic/Bedrock which previously excluded them). Always subtract cache
|
|
363
|
+
// tokens to get the non-cached input count for separate cost calculation.
|
|
364
|
+
const adjustedInputTokens = safe(inputTokens - cacheReadInputTokens - cacheWriteInputTokens)
|
|
365
|
+
|
|
366
|
+
const total = input.usage.totalTokens
|
|
367
|
+
|
|
368
|
+
const tokens = {
|
|
369
|
+
total,
|
|
370
|
+
input: adjustedInputTokens,
|
|
371
|
+
output: safe(outputTokens - reasoningTokens),
|
|
372
|
+
reasoning: reasoningTokens,
|
|
373
|
+
cache: {
|
|
374
|
+
write: cacheWriteInputTokens,
|
|
375
|
+
read: cacheReadInputTokens,
|
|
376
|
+
},
|
|
377
|
+
}
|
|
378
|
+
const reported = SaeeolSession.providerCost({
|
|
379
|
+
metadata: input.metadata,
|
|
380
|
+
usage: input.usage,
|
|
381
|
+
provider: input.provider,
|
|
382
|
+
providerID: input.model.providerID,
|
|
383
|
+
})
|
|
384
|
+
if (reported !== undefined) return { cost: safe(reported), tokens }
|
|
385
|
+
|
|
386
|
+
const costInfo =
|
|
387
|
+
input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000
|
|
388
|
+
? input.model.cost.experimentalOver200K
|
|
389
|
+
: input.model.cost
|
|
390
|
+
return {
|
|
391
|
+
cost: safe(
|
|
392
|
+
new Decimal(0)
|
|
393
|
+
.add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
|
|
394
|
+
.add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
|
|
395
|
+
.add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000))
|
|
396
|
+
.add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000))
|
|
397
|
+
// TODO: update models.dev to have better pricing model, for now:
|
|
398
|
+
// charge reasoning tokens at the same rate as output tokens
|
|
399
|
+
.add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
|
|
400
|
+
.toNumber(),
|
|
401
|
+
),
|
|
402
|
+
tokens,
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export class BusyError extends Error {
|
|
407
|
+
constructor(public readonly sessionID: string) {
|
|
408
|
+
super(`Session ${sessionID} is busy`)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export interface Interface {
|
|
413
|
+
readonly list: (input?: ListInput) => Effect.Effect<Info[]>
|
|
414
|
+
readonly create: (input?: {
|
|
415
|
+
parentID?: SessionID
|
|
416
|
+
title?: string
|
|
417
|
+
permission?: Permission.Ruleset
|
|
418
|
+
platform?: string
|
|
419
|
+
workspaceID?: WorkspaceID
|
|
420
|
+
}) => Effect.Effect<Info>
|
|
421
|
+
readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Info>
|
|
422
|
+
readonly touch: (sessionID: SessionID) => Effect.Effect<void>
|
|
423
|
+
readonly get: (id: SessionID) => Effect.Effect<Info>
|
|
424
|
+
readonly setTitle: (input: { sessionID: SessionID; title: string }) => Effect.Effect<void>
|
|
425
|
+
readonly setArchived: (input: { sessionID: SessionID; time?: number }) => Effect.Effect<void>
|
|
426
|
+
readonly setPermission: (input: { sessionID: SessionID; permission: Permission.Ruleset }) => Effect.Effect<void>
|
|
427
|
+
readonly setRevert: (input: {
|
|
428
|
+
sessionID: SessionID
|
|
429
|
+
revert: Info["revert"]
|
|
430
|
+
summary: Info["summary"]
|
|
431
|
+
}) => Effect.Effect<void>
|
|
432
|
+
readonly clearRevert: (sessionID: SessionID) => Effect.Effect<void>
|
|
433
|
+
readonly setSummary: (input: { sessionID: SessionID; summary: Info["summary"] }) => Effect.Effect<void>
|
|
434
|
+
readonly diff: (sessionID: SessionID) => Effect.Effect<Snapshot.FileDiff[]>
|
|
435
|
+
readonly messages: (input: { sessionID: SessionID; limit?: number }) => Effect.Effect<MessageV2.WithParts[]>
|
|
436
|
+
readonly children: (parentID: SessionID) => Effect.Effect<Info[]>
|
|
437
|
+
readonly remove: (sessionID: SessionID) => Effect.Effect<void>
|
|
438
|
+
readonly updateMessage: <T extends MessageV2.Info>(msg: T) => Effect.Effect<T>
|
|
439
|
+
readonly removeMessage: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect<MessageID>
|
|
440
|
+
readonly removePart: (input: { sessionID: SessionID; messageID: MessageID; partID: PartID }) => Effect.Effect<PartID>
|
|
441
|
+
readonly getPart: (input: {
|
|
442
|
+
sessionID: SessionID
|
|
443
|
+
messageID: MessageID
|
|
444
|
+
partID: PartID
|
|
445
|
+
}) => Effect.Effect<MessageV2.Part | undefined>
|
|
446
|
+
readonly updatePart: <T extends MessageV2.Part>(part: T) => Effect.Effect<T>
|
|
447
|
+
readonly updatePartDelta: (input: {
|
|
448
|
+
sessionID: SessionID
|
|
449
|
+
messageID: MessageID
|
|
450
|
+
partID: PartID
|
|
451
|
+
field: string
|
|
452
|
+
delta: string
|
|
453
|
+
}) => Effect.Effect<void>
|
|
454
|
+
/** Finds the first message matching the predicate, searching newest-first. */
|
|
455
|
+
readonly findMessage: (
|
|
456
|
+
sessionID: SessionID,
|
|
457
|
+
predicate: (msg: MessageV2.WithParts) => boolean,
|
|
458
|
+
) => Effect.Effect<Option.Option<MessageV2.WithParts>>
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export class Service extends Context.Service<Service, Interface>()("@saeeol/Session") {}
|
|
462
|
+
|
|
463
|
+
export type Patch = Types.DeepMutable<SyncEvent.Event<typeof Event.Updated>["data"]["info"]>
|
|
464
|
+
|
|
465
|
+
const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
|
|
466
|
+
Effect.sync(() => Database.use(fn))
|
|
467
|
+
|
|
468
|
+
export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service | SyncEvent.Service> = Layer.effect(
|
|
469
|
+
Service,
|
|
470
|
+
Effect.gen(function* () {
|
|
471
|
+
const bus = yield* Bus.Service
|
|
472
|
+
const storage = yield* Storage.Service
|
|
473
|
+
const sync = yield* SyncEvent.Service
|
|
474
|
+
|
|
475
|
+
const createNext = Effect.fn("Session.createNext")(function* (input: {
|
|
476
|
+
id?: SessionID
|
|
477
|
+
title?: string
|
|
478
|
+
parentID?: SessionID
|
|
479
|
+
workspaceID?: WorkspaceID
|
|
480
|
+
directory: string
|
|
481
|
+
path?: string
|
|
482
|
+
permission?: Permission.Ruleset
|
|
483
|
+
platform?: string
|
|
484
|
+
}) {
|
|
485
|
+
const ctx = yield* InstanceState.context
|
|
486
|
+
const result: Info = {
|
|
487
|
+
id: SessionID.descending(input.id),
|
|
488
|
+
slug: Slug.create(),
|
|
489
|
+
version: InstallationVersion,
|
|
490
|
+
projectID: ctx.project.id,
|
|
491
|
+
directory: input.directory,
|
|
492
|
+
path: input.path,
|
|
493
|
+
workspaceID: input.workspaceID,
|
|
494
|
+
parentID: input.parentID,
|
|
495
|
+
title: input.title ?? createDefaultTitle(!!input.parentID),
|
|
496
|
+
permission: input.permission,
|
|
497
|
+
time: {
|
|
498
|
+
created: Date.now(),
|
|
499
|
+
updated: Date.now(),
|
|
500
|
+
},
|
|
501
|
+
}
|
|
502
|
+
log.info("created", result)
|
|
503
|
+
SaeeolSession.register({ id: result.id, parentID: result.parentID, platform: input.platform })
|
|
504
|
+
|
|
505
|
+
yield* sync.run(Event.Created, { sessionID: result.id, info: result })
|
|
506
|
+
|
|
507
|
+
if (!Flag.SAEEOL_EXPERIMENTAL_WORKSPACES) {
|
|
508
|
+
// This only exist for backwards compatibility. We should not be
|
|
509
|
+
// manually publishing this event; it is a sync event now
|
|
510
|
+
yield* bus.publish(Event.Updated, {
|
|
511
|
+
sessionID: result.id,
|
|
512
|
+
info: result,
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return result
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
const get = Effect.fn("Session.get")(function* (id: SessionID) {
|
|
520
|
+
const row = yield* db((d) => d.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
|
|
521
|
+
if (!row) throw new NotFoundError({ message: `Session not found: ${id}` })
|
|
522
|
+
return fromRow(row)
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
const list = Effect.fn("Session.list")(function* (input?: ListInput) {
|
|
526
|
+
const ctx = yield* InstanceState.context
|
|
527
|
+
return Array.from(listByProject({ projectID: ctx.project.id, ...(input ?? {}) }))
|
|
528
|
+
})
|
|
529
|
+
const children = Effect.fn("Session.children")(function* (parentID: SessionID) {
|
|
530
|
+
const ctx = yield* Effect.try({ try: () => Instance.current, catch: () => undefined }).pipe(Effect.option)
|
|
531
|
+
const conditions = [eq(SessionTable.parent_id, parentID)]
|
|
532
|
+
if (Option.isSome(ctx)) conditions.push(eq(SessionTable.project_id, ctx.value.project.id))
|
|
533
|
+
const rows = yield* db((d) =>
|
|
534
|
+
d
|
|
535
|
+
.select()
|
|
536
|
+
.from(SessionTable)
|
|
537
|
+
.where(and(...conditions))
|
|
538
|
+
.all(),
|
|
539
|
+
)
|
|
540
|
+
return rows.map(fromRow)
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
const remove: Interface["remove"] = Effect.fnUntraced(function* (sessionID: SessionID) {
|
|
544
|
+
try {
|
|
545
|
+
const session = yield* get(sessionID)
|
|
546
|
+
const kids = yield* children(sessionID)
|
|
547
|
+
for (const child of kids) {
|
|
548
|
+
yield* remove(child.id)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// `remove` needs to work in all cases, such as a broken
|
|
552
|
+
// sessions that run cleanup. In certain cases these will
|
|
553
|
+
// run without any instance state, so we need to turn off
|
|
554
|
+
// publishing of events in that case
|
|
555
|
+
const hasInstance = yield* InstanceState.directory.pipe(
|
|
556
|
+
Effect.as(true),
|
|
557
|
+
Effect.catchCause(() => Effect.succeed(false)),
|
|
558
|
+
)
|
|
559
|
+
yield* Effect.promise(() => SaeeolSession.removeSession(sessionID)).pipe(Effect.ignore)
|
|
560
|
+
SaeeolSession.clearPlatformOverride(sessionID)
|
|
561
|
+
if (hasInstance) {
|
|
562
|
+
void Promise.all([import("@/effect/app-runtime"), import("./run-state")]).then(([app, run]) =>
|
|
563
|
+
app.AppRuntime.runPromise(run.SessionRunState.Service.use((svc) => svc.cancel(sessionID))).catch(() => {}),
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
yield* sync.run(Event.Deleted, { sessionID, info: session }, { publish: hasInstance })
|
|
567
|
+
yield* sync.remove(sessionID)
|
|
568
|
+
} catch (e) {
|
|
569
|
+
log.error(e)
|
|
570
|
+
}
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
const updateMessage = <T extends MessageV2.Info>(msg: T): Effect.Effect<T> =>
|
|
574
|
+
Effect.gen(function* () {
|
|
575
|
+
yield* Effect.sync(() =>
|
|
576
|
+
SaeeolSession.runSyncSafe(
|
|
577
|
+
() => SyncEvent.run(MessageV2.Event.Updated, { sessionID: msg.sessionID, info: msg }),
|
|
578
|
+
{ type: "message update", id: msg.id, sessionID: msg.sessionID },
|
|
579
|
+
),
|
|
580
|
+
)
|
|
581
|
+
return msg
|
|
582
|
+
}).pipe(Effect.withSpan("Session.updateMessage"))
|
|
583
|
+
|
|
584
|
+
const updatePart = <T extends MessageV2.Part>(part: T): Effect.Effect<T> =>
|
|
585
|
+
Effect.gen(function* () {
|
|
586
|
+
yield* Effect.sync(() =>
|
|
587
|
+
SaeeolSession.runSyncSafe(
|
|
588
|
+
() =>
|
|
589
|
+
SyncEvent.run(MessageV2.Event.PartUpdated, {
|
|
590
|
+
sessionID: part.sessionID,
|
|
591
|
+
part: structuredClone(part),
|
|
592
|
+
time: Date.now(),
|
|
593
|
+
}),
|
|
594
|
+
{ type: "part update", id: part.id, sessionID: part.sessionID },
|
|
595
|
+
),
|
|
596
|
+
)
|
|
597
|
+
return part
|
|
598
|
+
}).pipe(Effect.withSpan("Session.updatePart"))
|
|
599
|
+
|
|
600
|
+
const getPart: Interface["getPart"] = Effect.fn("Session.getPart")(function* (input) {
|
|
601
|
+
const row = Database.use((db) =>
|
|
602
|
+
db
|
|
603
|
+
.select()
|
|
604
|
+
.from(PartTable)
|
|
605
|
+
.where(
|
|
606
|
+
and(
|
|
607
|
+
eq(PartTable.session_id, input.sessionID),
|
|
608
|
+
eq(PartTable.message_id, input.messageID),
|
|
609
|
+
eq(PartTable.id, input.partID),
|
|
610
|
+
),
|
|
611
|
+
)
|
|
612
|
+
.get(),
|
|
613
|
+
)
|
|
614
|
+
if (!row) return
|
|
615
|
+
return {
|
|
616
|
+
...row.data,
|
|
617
|
+
id: row.id,
|
|
618
|
+
sessionID: row.session_id,
|
|
619
|
+
messageID: row.message_id,
|
|
620
|
+
} as MessageV2.Part
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
const create = Effect.fn("Session.create")(function* (input?: {
|
|
624
|
+
parentID?: SessionID
|
|
625
|
+
title?: string
|
|
626
|
+
permission?: Permission.Ruleset
|
|
627
|
+
platform?: string
|
|
628
|
+
workspaceID?: WorkspaceID
|
|
629
|
+
}) {
|
|
630
|
+
const ctx = yield* InstanceState.context
|
|
631
|
+
const workspace = yield* InstanceState.workspaceID
|
|
632
|
+
const session = yield* createNext({
|
|
633
|
+
parentID: input?.parentID,
|
|
634
|
+
directory: ctx.directory,
|
|
635
|
+
path: sessionPath(ctx.worktree, ctx.directory),
|
|
636
|
+
title: input?.title,
|
|
637
|
+
permission: input?.permission,
|
|
638
|
+
platform: input?.platform,
|
|
639
|
+
workspaceID: input?.workspaceID ?? workspace,
|
|
640
|
+
})
|
|
641
|
+
return session
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
const fork = Effect.fn("Session.fork")(function* (input: { sessionID: SessionID; messageID?: MessageID }) {
|
|
645
|
+
const ctx = yield* InstanceState.context
|
|
646
|
+
const original = yield* get(input.sessionID)
|
|
647
|
+
const title = getForkedTitle(original.title)
|
|
648
|
+
const session = yield* createNext({
|
|
649
|
+
directory: ctx.directory,
|
|
650
|
+
path: sessionPath(ctx.worktree, ctx.directory),
|
|
651
|
+
workspaceID: original.workspaceID,
|
|
652
|
+
title,
|
|
653
|
+
})
|
|
654
|
+
const msgs = yield* messages({ sessionID: input.sessionID })
|
|
655
|
+
const idMap = new Map<string, MessageID>()
|
|
656
|
+
|
|
657
|
+
for (const msg of msgs) {
|
|
658
|
+
if (input.messageID && msg.info.id >= input.messageID) break
|
|
659
|
+
const newID = MessageID.ascending()
|
|
660
|
+
idMap.set(msg.info.id, newID)
|
|
661
|
+
|
|
662
|
+
const parentID = msg.info.role === "assistant" && msg.info.parentID ? idMap.get(msg.info.parentID) : undefined
|
|
663
|
+
const cloned = yield* updateMessage({
|
|
664
|
+
...msg.info,
|
|
665
|
+
sessionID: session.id,
|
|
666
|
+
id: newID,
|
|
667
|
+
...(parentID && { parentID }),
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
for (const part of msg.parts) {
|
|
671
|
+
const p: MessageV2.Part = {
|
|
672
|
+
...part,
|
|
673
|
+
id: PartID.ascending(),
|
|
674
|
+
messageID: cloned.id,
|
|
675
|
+
sessionID: session.id,
|
|
676
|
+
}
|
|
677
|
+
if (p.type === "compaction" && p.tail_start_id) {
|
|
678
|
+
p.tail_start_id = idMap.get(p.tail_start_id)
|
|
679
|
+
}
|
|
680
|
+
yield* updatePart(p)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return session
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
const patch = (sessionID: SessionID, info: Patch) => sync.run(Event.Updated, { sessionID, info })
|
|
687
|
+
|
|
688
|
+
const touch = Effect.fn("Session.touch")(function* (sessionID: SessionID) {
|
|
689
|
+
yield* patch(sessionID, { time: { updated: Date.now() } })
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
const setTitle = Effect.fn("Session.setTitle")(function* (input: { sessionID: SessionID; title: string }) {
|
|
693
|
+
yield* patch(input.sessionID, { title: input.title })
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
const setArchived = Effect.fn("Session.setArchived")(function* (input: { sessionID: SessionID; time?: number }) {
|
|
697
|
+
yield* patch(input.sessionID, { time: { archived: input.time } })
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
const setPermission = Effect.fn("Session.setPermission")(function* (input: {
|
|
701
|
+
sessionID: SessionID
|
|
702
|
+
permission: Permission.Ruleset
|
|
703
|
+
}) {
|
|
704
|
+
yield* patch(input.sessionID, { permission: input.permission, time: { updated: Date.now() } })
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
const setRevert = Effect.fn("Session.setRevert")(function* (input: {
|
|
708
|
+
sessionID: SessionID
|
|
709
|
+
revert: Info["revert"]
|
|
710
|
+
summary: Info["summary"]
|
|
711
|
+
}) {
|
|
712
|
+
yield* patch(input.sessionID, { summary: input.summary, time: { updated: Date.now() }, revert: input.revert })
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
const clearRevert = Effect.fn("Session.clearRevert")(function* (sessionID: SessionID) {
|
|
716
|
+
yield* patch(sessionID, { time: { updated: Date.now() }, revert: null })
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
const setSummary = Effect.fn("Session.setSummary")(function* (input: {
|
|
720
|
+
sessionID: SessionID
|
|
721
|
+
summary: Info["summary"]
|
|
722
|
+
}) {
|
|
723
|
+
yield* patch(input.sessionID, { time: { updated: Date.now() }, summary: input.summary })
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
const diff = Effect.fn("Session.diff")(function* (sessionID: SessionID) {
|
|
727
|
+
return yield* storage
|
|
728
|
+
.read<Snapshot.FileDiff[]>(["session_diff", sessionID])
|
|
729
|
+
.pipe(Effect.orElseSucceed((): Snapshot.FileDiff[] => []))
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
const messages = Effect.fn("Session.messages")(function* (input: { sessionID: SessionID; limit?: number }) {
|
|
733
|
+
if (input.limit) {
|
|
734
|
+
return MessageV2.page({ sessionID: input.sessionID, limit: input.limit }).items
|
|
735
|
+
}
|
|
736
|
+
return Array.from(MessageV2.stream(input.sessionID)).reverse()
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
const removeMessage = Effect.fn("Session.removeMessage")(function* (input: {
|
|
740
|
+
sessionID: SessionID
|
|
741
|
+
messageID: MessageID
|
|
742
|
+
}) {
|
|
743
|
+
yield* sync.run(MessageV2.Event.Removed, {
|
|
744
|
+
sessionID: input.sessionID,
|
|
745
|
+
messageID: input.messageID,
|
|
746
|
+
})
|
|
747
|
+
return input.messageID
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
const removePart = Effect.fn("Session.removePart")(function* (input: {
|
|
751
|
+
sessionID: SessionID
|
|
752
|
+
messageID: MessageID
|
|
753
|
+
partID: PartID
|
|
754
|
+
}) {
|
|
755
|
+
yield* sync.run(MessageV2.Event.PartRemoved, {
|
|
756
|
+
sessionID: input.sessionID,
|
|
757
|
+
messageID: input.messageID,
|
|
758
|
+
partID: input.partID,
|
|
759
|
+
})
|
|
760
|
+
return input.partID
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
const updatePartDelta = Effect.fnUntraced(function* (input: {
|
|
764
|
+
sessionID: SessionID
|
|
765
|
+
messageID: MessageID
|
|
766
|
+
partID: PartID
|
|
767
|
+
field: string
|
|
768
|
+
delta: string
|
|
769
|
+
}) {
|
|
770
|
+
yield* bus.publish(MessageV2.Event.PartDelta, input)
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
/** Finds the first message matching the predicate, searching newest-first. */
|
|
774
|
+
const findMessage = Effect.fn("Session.findMessage")(function* (
|
|
775
|
+
sessionID: SessionID,
|
|
776
|
+
predicate: (msg: MessageV2.WithParts) => boolean,
|
|
777
|
+
) {
|
|
778
|
+
for (const item of MessageV2.stream(sessionID)) {
|
|
779
|
+
if (predicate(item)) return Option.some(item)
|
|
780
|
+
}
|
|
781
|
+
return Option.none<MessageV2.WithParts>()
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
return Service.of({
|
|
785
|
+
list,
|
|
786
|
+
create,
|
|
787
|
+
fork,
|
|
788
|
+
touch,
|
|
789
|
+
get,
|
|
790
|
+
setTitle,
|
|
791
|
+
setArchived,
|
|
792
|
+
setPermission,
|
|
793
|
+
setRevert,
|
|
794
|
+
clearRevert,
|
|
795
|
+
setSummary,
|
|
796
|
+
diff,
|
|
797
|
+
messages,
|
|
798
|
+
children,
|
|
799
|
+
remove,
|
|
800
|
+
updateMessage,
|
|
801
|
+
removeMessage,
|
|
802
|
+
removePart,
|
|
803
|
+
updatePart,
|
|
804
|
+
getPart,
|
|
805
|
+
updatePartDelta,
|
|
806
|
+
findMessage,
|
|
807
|
+
})
|
|
808
|
+
}),
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
export const defaultLayer = layer.pipe(
|
|
812
|
+
Layer.provide(Bus.layer),
|
|
813
|
+
Layer.provide(Storage.defaultLayer),
|
|
814
|
+
Layer.provide(SyncEvent.defaultLayer),
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
function* listByProject(
|
|
818
|
+
input: ListInput & {
|
|
819
|
+
projectID: ProjectID
|
|
820
|
+
},
|
|
821
|
+
) {
|
|
822
|
+
// (see PR #8875). That directory-anchored filter conflicts with upstream's path-prefix filter,
|
|
823
|
+
// so bypass it when input.path is provided and fall back to the plain project_id base.
|
|
824
|
+
const conditions =
|
|
825
|
+
input.path !== undefined
|
|
826
|
+
? [eq(SessionTable.project_id, input.projectID)]
|
|
827
|
+
: SaeeolSession.filters({ projectID: input.projectID, directory: input.directory })
|
|
828
|
+
|
|
829
|
+
if (input.workspaceID) {
|
|
830
|
+
conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
|
|
831
|
+
}
|
|
832
|
+
if (input.path !== undefined) {
|
|
833
|
+
if (input.path) {
|
|
834
|
+
const conds = [eq(SessionTable.path, input.path), like(SessionTable.path, `${input.path}/%`)]
|
|
835
|
+
|
|
836
|
+
conditions.push(
|
|
837
|
+
input.directory
|
|
838
|
+
? or(...conds, and(isNull(SessionTable.path), eq(SessionTable.directory, input.directory))!)!
|
|
839
|
+
: or(...conds)!,
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
} else if (input.scope !== "project" && !Flag.SAEEOL_EXPERIMENTAL_WORKSPACES) {
|
|
843
|
+
// if (input.directory) {
|
|
844
|
+
// conditions.push(eq(SessionTable.directory, input.directory))
|
|
845
|
+
// }
|
|
846
|
+
}
|
|
847
|
+
if (input.roots) {
|
|
848
|
+
conditions.push(isNull(SessionTable.parent_id))
|
|
849
|
+
}
|
|
850
|
+
if (input.start) {
|
|
851
|
+
conditions.push(gte(SessionTable.time_updated, input.start))
|
|
852
|
+
}
|
|
853
|
+
if (input.search) {
|
|
854
|
+
conditions.push(like(SessionTable.title, `%${input.search}%`))
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const limit = input.limit ?? 100
|
|
858
|
+
|
|
859
|
+
const rows = Database.use((db) =>
|
|
860
|
+
db
|
|
861
|
+
.select()
|
|
862
|
+
.from(SessionTable)
|
|
863
|
+
.where(and(...conditions))
|
|
864
|
+
.orderBy(desc(SessionTable.time_updated))
|
|
865
|
+
.limit(limit)
|
|
866
|
+
.all(),
|
|
867
|
+
)
|
|
868
|
+
for (const row of rows) {
|
|
869
|
+
yield fromRow(row)
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
export function* listGlobal(input?: {
|
|
873
|
+
projectID?: string
|
|
874
|
+
directory?: string
|
|
875
|
+
directories?: string[]
|
|
876
|
+
roots?: boolean
|
|
877
|
+
start?: number
|
|
878
|
+
cursor?: number
|
|
879
|
+
search?: string
|
|
880
|
+
limit?: number
|
|
881
|
+
archived?: boolean
|
|
882
|
+
}) {
|
|
883
|
+
yield* SaeeolSession.listGlobal<GlobalInfo>({ ...input, fromRow })
|
|
884
|
+
}
|
|
885
|
+
const { runPromise } = makeRuntime(Service, defaultLayer)
|
|
886
|
+
|
|
887
|
+
const decodeCreate = Schema.decodeUnknownSync(CreateInput)
|
|
888
|
+
const decodeGet = Schema.decodeUnknownSync(GetInput)
|
|
889
|
+
const decodeSetTitle = Schema.decodeUnknownSync(SetTitleInput)
|
|
890
|
+
const decodeSetArchived = Schema.decodeUnknownSync(SetArchivedInput)
|
|
891
|
+
const decodeSetPermission = Schema.decodeUnknownSync(SetPermissionInput)
|
|
892
|
+
const decodeSetRevert = Schema.decodeUnknownSync(SetRevertInput)
|
|
893
|
+
const decodeMessages = Schema.decodeUnknownSync(MessagesInput)
|
|
894
|
+
const decodeChildren = Schema.decodeUnknownSync(ChildrenInput)
|
|
895
|
+
const decodeRemove = Schema.decodeUnknownSync(RemoveInput)
|
|
896
|
+
|
|
897
|
+
export const create = (input?: CreateInput) => runPromise((svc) => svc.create(decodeCreate(input) as CreateInput))
|
|
898
|
+
export const fork = saeeolSessionFork
|
|
899
|
+
export const get = (id: SessionID) => runPromise((svc) => svc.get(decodeGet(id)))
|
|
900
|
+
export const setTitle = (input: { sessionID: SessionID; title: string }) =>
|
|
901
|
+
runPromise((svc) => svc.setTitle(decodeSetTitle(input)))
|
|
902
|
+
export const setArchived = (input: { sessionID: SessionID; time?: number }) =>
|
|
903
|
+
runPromise((svc) => svc.setArchived(decodeSetArchived(input)))
|
|
904
|
+
export const setPermission = (input: { sessionID: SessionID; permission: Permission.Ruleset }) =>
|
|
905
|
+
runPromise((svc) =>
|
|
906
|
+
svc.setPermission(decodeSetPermission(input) as { sessionID: SessionID; permission: Permission.Ruleset }),
|
|
907
|
+
)
|
|
908
|
+
export const setRevert = (input: { sessionID: SessionID; revert?: Info["revert"]; summary?: Info["summary"] }) => {
|
|
909
|
+
const parsed = decodeSetRevert(input) as { sessionID: SessionID; revert?: Info["revert"]; summary?: Info["summary"] }
|
|
910
|
+
return runPromise((svc) =>
|
|
911
|
+
svc.setRevert({ sessionID: parsed.sessionID, revert: parsed.revert, summary: parsed.summary }),
|
|
912
|
+
)
|
|
913
|
+
}
|
|
914
|
+
export const messages = (input: { sessionID: SessionID; limit?: number }) =>
|
|
915
|
+
runPromise((svc) => svc.messages(decodeMessages(input)))
|
|
916
|
+
export const children = (id: SessionID) => runPromise((svc) => svc.children(decodeChildren(id)))
|
|
917
|
+
export const remove = (id: SessionID) => runPromise((svc) => svc.remove(decodeRemove(id)))
|
|
918
|
+
export async function updateMessage<T extends MessageV2.Info>(msg: T): Promise<T> {
|
|
919
|
+
MessageV2.Info.zod.parse(msg)
|
|
920
|
+
return runPromise((svc) => svc.updateMessage(msg))
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
export const removeMessage = fn(z.object({ sessionID: SessionID.zod, messageID: MessageID.zod }), (input) =>
|
|
924
|
+
runPromise((svc) => svc.removeMessage(input)),
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
export const removePart = fn(
|
|
928
|
+
z.object({ sessionID: SessionID.zod, messageID: MessageID.zod, partID: PartID.zod }),
|
|
929
|
+
(input) => runPromise((svc) => svc.removePart(input)),
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
export async function updatePart<T extends MessageV2.Part>(part: T): Promise<T> {
|
|
933
|
+
MessageV2.Part.zod.parse(part)
|
|
934
|
+
return runPromise((svc) => svc.updatePart(part))
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
export const updatePartDelta = fn(
|
|
938
|
+
z.object({
|
|
939
|
+
sessionID: SessionID.zod,
|
|
940
|
+
messageID: MessageID.zod,
|
|
941
|
+
partID: PartID.zod,
|
|
942
|
+
field: z.string(),
|
|
943
|
+
delta: z.string(),
|
|
944
|
+
}),
|
|
945
|
+
(input) => runPromise((svc) => svc.updatePartDelta(input)),
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
export * as Session from "./session"
|