saeeol 1.2.0 → 1.2.2

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.
Files changed (193) hide show
  1. package/package.json +14 -14
  2. package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
  3. package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
  4. package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
  5. package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
  6. package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
  7. package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
  8. package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
  9. package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
  10. package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
  11. package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
  12. package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
  13. package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
  14. package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
  15. package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
  16. package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
  17. package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
  18. package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
  19. package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
  20. package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
  21. package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
  22. package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
  23. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
  24. package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
  25. package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
  26. package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
  27. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
  28. package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
  29. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
  30. package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
  31. package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
  32. package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
  33. package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
  34. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
  35. package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
  36. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
  37. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
  38. package/src/session/compaction-helpers.ts +1 -169
  39. package/src/session/compaction.ts +1 -712
  40. package/src/session/core/compaction/compaction-helpers.ts +169 -0
  41. package/src/session/core/compaction/compaction.ts +712 -0
  42. package/src/session/core/compaction/overflow.ts +28 -0
  43. package/src/session/core/instruction.ts +234 -0
  44. package/src/session/core/llm.ts +504 -0
  45. package/src/session/core/network.ts +392 -0
  46. package/src/session/core/processor.ts +731 -0
  47. package/src/session/core/projectors.ts +139 -0
  48. package/src/session/core/resolve-tools.ts +241 -0
  49. package/src/session/core/retry.ts +149 -0
  50. package/src/session/core/revert.ts +173 -0
  51. package/src/session/core/run-state.ts +110 -0
  52. package/src/session/core/schema.ts +35 -0
  53. package/src/session/core/session-types.ts +160 -0
  54. package/src/session/core/session.sql.ts +124 -0
  55. package/src/session/core/session.ts +948 -0
  56. package/src/session/core/shell-exec.ts +205 -0
  57. package/src/session/core/status.ts +100 -0
  58. package/src/session/core/subtask.ts +268 -0
  59. package/src/session/core/summary.ts +173 -0
  60. package/src/session/core/system.ts +114 -0
  61. package/src/session/core/todo.ts +86 -0
  62. package/src/session/core/user-part.ts +293 -0
  63. package/src/session/instruction.ts +1 -234
  64. package/src/session/llm.ts +1 -504
  65. package/src/session/message/message-errors.ts +83 -0
  66. package/src/session/message/message-parts.ts +89 -0
  67. package/src/session/message/message-query.ts +107 -0
  68. package/src/session/message/message-transform.ts +156 -0
  69. package/src/session/message/message-types.ts +68 -0
  70. package/src/session/message/message-v2.ts +73 -0
  71. package/src/session/message/message.ts +192 -0
  72. package/src/session/message-errors.ts +1 -83
  73. package/src/session/message-parts.ts +1 -89
  74. package/src/session/message-query.ts +1 -107
  75. package/src/session/message-transform.ts +1 -156
  76. package/src/session/message-types.ts +1 -68
  77. package/src/session/message-v2.ts +1 -73
  78. package/src/session/message.ts +1 -192
  79. package/src/session/network.ts +1 -392
  80. package/src/session/overflow.ts +1 -28
  81. package/src/session/processor.ts +1 -731
  82. package/src/session/projectors.ts +2 -139
  83. package/src/session/prompt/prompt-command.ts +93 -0
  84. package/src/session/prompt/prompt-loop.ts +299 -0
  85. package/src/session/prompt/prompt-model.ts +44 -0
  86. package/src/session/prompt/prompt-reminders.ts +120 -0
  87. package/src/session/prompt/prompt-resolve.ts +42 -0
  88. package/src/session/prompt/prompt-schemas.ts +128 -0
  89. package/src/session/prompt/prompt-title.ts +55 -0
  90. package/src/session/prompt/prompt-types.ts +47 -0
  91. package/src/session/prompt/prompt-user-msg.ts +80 -0
  92. package/src/session/prompt/prompt.ts +211 -0
  93. package/src/session/prompt-command.ts +1 -93
  94. package/src/session/prompt-loop.ts +1 -299
  95. package/src/session/prompt-model.ts +1 -44
  96. package/src/session/prompt-reminders.ts +1 -120
  97. package/src/session/prompt-resolve.ts +1 -42
  98. package/src/session/prompt-schemas.ts +1 -128
  99. package/src/session/prompt-title.ts +1 -55
  100. package/src/session/prompt-types.ts +1 -47
  101. package/src/session/prompt-user-msg.ts +1 -80
  102. package/src/session/prompt.ts +1 -211
  103. package/src/session/resolve-tools.ts +1 -241
  104. package/src/session/retry.ts +1 -149
  105. package/src/session/revert.ts +1 -173
  106. package/src/session/run-state.ts +1 -110
  107. package/src/session/schema.ts +1 -35
  108. package/src/session/session-types.ts +1 -160
  109. package/src/session/session.sql.ts +1 -124
  110. package/src/session/session.ts +1 -948
  111. package/src/session/shell-exec.ts +1 -205
  112. package/src/session/status.ts +1 -100
  113. package/src/session/subtask.ts +1 -268
  114. package/src/session/summary.ts +1 -173
  115. package/src/session/system.ts +1 -114
  116. package/src/session/todo.ts +1 -86
  117. package/src/session/user-part.ts +1 -293
  118. package/src/tool/apply_patch.ts +1 -334
  119. package/src/tool/bash.ts +1 -656
  120. package/src/tool/core/external-directory.ts +55 -0
  121. package/src/tool/core/invalid.ts +21 -0
  122. package/src/tool/core/recall.ts +164 -0
  123. package/src/tool/core/recall.txt +12 -0
  124. package/src/tool/core/schema.ts +16 -0
  125. package/src/tool/core/tool.ts +162 -0
  126. package/src/tool/core/truncate.ts +160 -0
  127. package/src/tool/core/truncation-dir.ts +4 -0
  128. package/src/tool/diagnostics.ts +1 -20
  129. package/src/tool/edit-replacers.ts +1 -288
  130. package/src/tool/edit-utils.ts +1 -86
  131. package/src/tool/edit.ts +1 -262
  132. package/src/tool/external-directory.ts +1 -55
  133. package/src/tool/file/apply_patch.ts +334 -0
  134. package/src/tool/file/apply_patch.txt +33 -0
  135. package/src/tool/file/bash.ts +656 -0
  136. package/src/tool/file/bash.txt +119 -0
  137. package/src/tool/file/edit-replacers.ts +288 -0
  138. package/src/tool/file/edit-utils.ts +86 -0
  139. package/src/tool/file/edit.ts +262 -0
  140. package/src/tool/file/edit.txt +10 -0
  141. package/src/tool/file/read.ts +389 -0
  142. package/src/tool/file/read.txt +14 -0
  143. package/src/tool/file/write.ts +114 -0
  144. package/src/tool/file/write.txt +8 -0
  145. package/src/tool/glob.ts +1 -115
  146. package/src/tool/grep.ts +1 -151
  147. package/src/tool/integration/diagnostics.ts +20 -0
  148. package/src/tool/integration/lsp.ts +113 -0
  149. package/src/tool/integration/lsp.txt +24 -0
  150. package/src/tool/integration/mcp-exa.ts +73 -0
  151. package/src/tool/integration/package.ts +168 -0
  152. package/src/tool/integration/registry.ts +375 -0
  153. package/src/tool/invalid.ts +1 -21
  154. package/src/tool/lsp.ts +1 -113
  155. package/src/tool/mcp-exa.ts +1 -73
  156. package/src/tool/package.ts +1 -168
  157. package/src/tool/plan.ts +1 -30
  158. package/src/tool/question.ts +1 -52
  159. package/src/tool/read.ts +1 -389
  160. package/src/tool/recall.ts +1 -164
  161. package/src/tool/registry.ts +1 -375
  162. package/src/tool/schema.ts +1 -16
  163. package/src/tool/search/glob.ts +115 -0
  164. package/src/tool/search/glob.txt +6 -0
  165. package/src/tool/search/grep.ts +151 -0
  166. package/src/tool/search/grep.txt +8 -0
  167. package/src/tool/search/warpgrep.ts +107 -0
  168. package/src/tool/search/warpgrep.txt +10 -0
  169. package/src/tool/search/webfetch.ts +202 -0
  170. package/src/tool/search/webfetch.txt +13 -0
  171. package/src/tool/search/websearch.ts +71 -0
  172. package/src/tool/search/websearch.txt +14 -0
  173. package/src/tool/skill.ts +1 -91
  174. package/src/tool/task.ts +1 -197
  175. package/src/tool/todo.ts +1 -62
  176. package/src/tool/tool.ts +1 -162
  177. package/src/tool/truncate.ts +1 -160
  178. package/src/tool/truncation-dir.ts +1 -4
  179. package/src/tool/warpgrep.ts +1 -107
  180. package/src/tool/webfetch.ts +1 -202
  181. package/src/tool/websearch.ts +1 -71
  182. package/src/tool/workflow/plan-enter.txt +14 -0
  183. package/src/tool/workflow/plan-exit.txt +13 -0
  184. package/src/tool/workflow/plan.ts +30 -0
  185. package/src/tool/workflow/question.ts +52 -0
  186. package/src/tool/workflow/question.txt +11 -0
  187. package/src/tool/workflow/skill.ts +91 -0
  188. package/src/tool/workflow/skill.txt +5 -0
  189. package/src/tool/workflow/task.ts +197 -0
  190. package/src/tool/workflow/task.txt +57 -0
  191. package/src/tool/workflow/todo.ts +62 -0
  192. package/src/tool/workflow/todowrite.txt +167 -0
  193. package/src/tool/write.ts +1 -114
@@ -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"