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.
Files changed (81) hide show
  1. package/package.json +14 -14
  2. package/src/session/compaction-helpers.ts +1 -169
  3. package/src/session/compaction.ts +1 -712
  4. package/src/session/core/compaction/compaction-helpers.ts +169 -0
  5. package/src/session/core/compaction/compaction.ts +712 -0
  6. package/src/session/core/compaction/overflow.ts +28 -0
  7. package/src/session/core/instruction.ts +234 -0
  8. package/src/session/core/llm.ts +504 -0
  9. package/src/session/core/network.ts +392 -0
  10. package/src/session/core/processor.ts +731 -0
  11. package/src/session/core/projectors.ts +139 -0
  12. package/src/session/core/resolve-tools.ts +241 -0
  13. package/src/session/core/retry.ts +149 -0
  14. package/src/session/core/revert.ts +173 -0
  15. package/src/session/core/run-state.ts +110 -0
  16. package/src/session/core/schema.ts +35 -0
  17. package/src/session/core/session-types.ts +160 -0
  18. package/src/session/core/session.sql.ts +124 -0
  19. package/src/session/core/session.ts +948 -0
  20. package/src/session/core/shell-exec.ts +205 -0
  21. package/src/session/core/status.ts +100 -0
  22. package/src/session/core/subtask.ts +268 -0
  23. package/src/session/core/summary.ts +173 -0
  24. package/src/session/core/system.ts +114 -0
  25. package/src/session/core/todo.ts +86 -0
  26. package/src/session/core/user-part.ts +293 -0
  27. package/src/session/instruction.ts +1 -234
  28. package/src/session/llm.ts +1 -504
  29. package/src/session/message/message-errors.ts +83 -0
  30. package/src/session/message/message-parts.ts +89 -0
  31. package/src/session/message/message-query.ts +107 -0
  32. package/src/session/message/message-transform.ts +156 -0
  33. package/src/session/message/message-types.ts +68 -0
  34. package/src/session/message/message-v2.ts +73 -0
  35. package/src/session/message/message.ts +192 -0
  36. package/src/session/message-errors.ts +1 -83
  37. package/src/session/message-parts.ts +1 -89
  38. package/src/session/message-query.ts +1 -107
  39. package/src/session/message-transform.ts +1 -156
  40. package/src/session/message-types.ts +1 -68
  41. package/src/session/message-v2.ts +1 -73
  42. package/src/session/message.ts +1 -192
  43. package/src/session/network.ts +1 -392
  44. package/src/session/overflow.ts +1 -28
  45. package/src/session/processor.ts +1 -731
  46. package/src/session/projectors.ts +2 -139
  47. package/src/session/prompt/prompt-command.ts +93 -0
  48. package/src/session/prompt/prompt-loop.ts +299 -0
  49. package/src/session/prompt/prompt-model.ts +44 -0
  50. package/src/session/prompt/prompt-reminders.ts +120 -0
  51. package/src/session/prompt/prompt-resolve.ts +42 -0
  52. package/src/session/prompt/prompt-schemas.ts +128 -0
  53. package/src/session/prompt/prompt-title.ts +55 -0
  54. package/src/session/prompt/prompt-types.ts +47 -0
  55. package/src/session/prompt/prompt-user-msg.ts +80 -0
  56. package/src/session/prompt/prompt.ts +211 -0
  57. package/src/session/prompt-command.ts +1 -93
  58. package/src/session/prompt-loop.ts +1 -299
  59. package/src/session/prompt-model.ts +1 -44
  60. package/src/session/prompt-reminders.ts +1 -120
  61. package/src/session/prompt-resolve.ts +1 -42
  62. package/src/session/prompt-schemas.ts +1 -128
  63. package/src/session/prompt-title.ts +1 -55
  64. package/src/session/prompt-types.ts +1 -47
  65. package/src/session/prompt-user-msg.ts +1 -80
  66. package/src/session/prompt.ts +1 -211
  67. package/src/session/resolve-tools.ts +1 -241
  68. package/src/session/retry.ts +1 -149
  69. package/src/session/revert.ts +1 -173
  70. package/src/session/run-state.ts +1 -110
  71. package/src/session/schema.ts +1 -35
  72. package/src/session/session-types.ts +1 -160
  73. package/src/session/session.sql.ts +1 -124
  74. package/src/session/session.ts +1 -948
  75. package/src/session/shell-exec.ts +1 -205
  76. package/src/session/status.ts +1 -100
  77. package/src/session/subtask.ts +1 -268
  78. package/src/session/summary.ts +1 -173
  79. package/src/session/system.ts +1 -114
  80. package/src/session/todo.ts +1 -86
  81. package/src/session/user-part.ts +1 -293
@@ -0,0 +1,173 @@
1
+ import { Effect, Layer, Context, Schema } from "effect"
2
+ import { Bus } from "../../bus"
3
+ import { Snapshot } from "../../snapshot"
4
+ import { Storage } from "@/storage/storage"
5
+ import { SyncEvent } from "../../sync"
6
+ import * as Log from "@saeeol/core/util/log"
7
+ import { zod } from "@/util/effect-zod"
8
+ import { withStatics } from "@/util/schema"
9
+ import * as Session from "./session"
10
+ import { MessageV2 } from "../message/message-v2"
11
+ import { SessionID, MessageID, PartID } from "./schema"
12
+ import { SessionRunState } from "./run-state"
13
+ import { SessionSummary } from "./summary"
14
+
15
+ const log = Log.create({ service: "session.revert" })
16
+
17
+ export const RevertInput = Schema.Struct({
18
+ sessionID: SessionID,
19
+ messageID: MessageID,
20
+ partID: Schema.optional(PartID),
21
+ }).pipe(withStatics((s) => ({ zod: zod(s) })))
22
+ export type RevertInput = Schema.Schema.Type<typeof RevertInput>
23
+
24
+ export interface Interface {
25
+ readonly revert: (input: RevertInput) => Effect.Effect<Session.Info>
26
+ readonly unrevert: (input: { sessionID: SessionID }) => Effect.Effect<Session.Info>
27
+ readonly cleanup: (session: Session.Info) => Effect.Effect<void>
28
+ }
29
+
30
+ export class Service extends Context.Service<Service, Interface>()("@saeeol/SessionRevert") {}
31
+
32
+ export const layer = Layer.effect(
33
+ Service,
34
+ Effect.gen(function* () {
35
+ const sessions = yield* Session.Service
36
+ const snap = yield* Snapshot.Service
37
+ const storage = yield* Storage.Service
38
+ const bus = yield* Bus.Service
39
+ const summary = yield* SessionSummary.Service
40
+ const state = yield* SessionRunState.Service
41
+ const sync = yield* SyncEvent.Service
42
+
43
+ const revert = Effect.fn("SessionRevert.revert")(function* (input: RevertInput) {
44
+ yield* state.assertNotBusy(input.sessionID)
45
+ const all = yield* sessions.messages({ sessionID: input.sessionID })
46
+ let lastUser: MessageV2.User | undefined
47
+ const session = yield* sessions.get(input.sessionID)
48
+
49
+ let rev: Session.Info["revert"]
50
+ const patches: Snapshot.Patch[] = []
51
+ for (const msg of all) {
52
+ if (msg.info.role === "user") lastUser = msg.info
53
+ const remaining = []
54
+ for (const part of msg.parts) {
55
+ if (rev) {
56
+ if (part.type === "patch") patches.push(part)
57
+ continue
58
+ }
59
+
60
+ if (!rev) {
61
+ if ((msg.info.id === input.messageID && !input.partID) || part.id === input.partID) {
62
+ const partID = remaining.some((item) => ["text", "tool"].includes(item.type)) ? input.partID : undefined
63
+ rev = {
64
+ messageID: !partID && lastUser ? lastUser.id : msg.info.id,
65
+ partID,
66
+ }
67
+ }
68
+ remaining.push(part)
69
+ }
70
+ }
71
+ }
72
+
73
+ if (!rev) return session
74
+
75
+ rev.snapshot = session.revert?.snapshot ?? (yield* snap.track())
76
+ if (session.revert?.snapshot) yield* snap.restore(session.revert.snapshot)
77
+ // reflects changes being undone (files on disk still have AI modifications)
78
+ const range = all.filter((msg) => msg.info.id >= rev!.messageID)
79
+ const diffs = yield* summary.computeDiff({ messages: range })
80
+
81
+ yield* snap.revert(patches)
82
+ if (rev.snapshot) rev.diff = yield* snap.diff(rev.snapshot as string)
83
+ yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore)
84
+ yield* bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs })
85
+ const summaryDiffs: Snapshot.SummaryFileDiff[] = diffs.map((d) => ({
86
+ file: d.file,
87
+ additions: d.additions,
88
+ deletions: d.deletions,
89
+ status: d.status,
90
+ }))
91
+ yield* sessions.setRevert({
92
+ sessionID: input.sessionID,
93
+ revert: rev,
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
+ diffs: summaryDiffs,
99
+ },
100
+ })
101
+ return yield* sessions.get(input.sessionID)
102
+ })
103
+
104
+ const unrevert = Effect.fn("SessionRevert.unrevert")(function* (input: { sessionID: SessionID }) {
105
+ log.info("unreverting", input)
106
+ yield* state.assertNotBusy(input.sessionID)
107
+ const session = yield* sessions.get(input.sessionID)
108
+ if (!session.revert) return session
109
+ if (session.revert.snapshot) yield* snap.restore(session.revert!.snapshot!)
110
+ yield* sessions.clearRevert(input.sessionID)
111
+ return yield* sessions.get(input.sessionID)
112
+ })
113
+
114
+ const cleanup = Effect.fn("SessionRevert.cleanup")(function* (session: Session.Info) {
115
+ if (!session.revert) return
116
+ const sessionID = session.id
117
+ const msgs = yield* sessions.messages({ sessionID })
118
+ const messageID = session.revert.messageID
119
+ const remove = [] as MessageV2.WithParts[]
120
+ let target: MessageV2.WithParts | undefined
121
+ for (const msg of msgs) {
122
+ if (msg.info.id < messageID) continue
123
+ if (msg.info.id > messageID) {
124
+ remove.push(msg)
125
+ continue
126
+ }
127
+ if (session.revert.partID) {
128
+ target = msg
129
+ continue
130
+ }
131
+ remove.push(msg)
132
+ }
133
+ for (const msg of remove) {
134
+ yield* sync.run(MessageV2.Event.Removed, {
135
+ sessionID,
136
+ messageID: msg.info.id,
137
+ })
138
+ }
139
+ if (session.revert.partID && target) {
140
+ const partID = session.revert.partID
141
+ const idx = target.parts.findIndex((part) => part.id === partID)
142
+ if (idx >= 0) {
143
+ const removeParts = target.parts.slice(idx)
144
+ target.parts = target.parts.slice(0, idx)
145
+ for (const part of removeParts) {
146
+ yield* sync.run(MessageV2.Event.PartRemoved, {
147
+ sessionID,
148
+ messageID: target.info.id,
149
+ partID: part.id,
150
+ })
151
+ }
152
+ }
153
+ }
154
+ yield* sessions.clearRevert(sessionID)
155
+ })
156
+
157
+ return Service.of({ revert, unrevert, cleanup })
158
+ }),
159
+ )
160
+
161
+ export const defaultLayer = Layer.suspend(() =>
162
+ layer.pipe(
163
+ Layer.provide(SessionRunState.defaultLayer),
164
+ Layer.provide(Session.defaultLayer),
165
+ Layer.provide(Snapshot.defaultLayer),
166
+ Layer.provide(Storage.defaultLayer),
167
+ Layer.provide(Bus.layer),
168
+ Layer.provide(SessionSummary.defaultLayer),
169
+ Layer.provide(SyncEvent.defaultLayer),
170
+ ),
171
+ )
172
+
173
+ export * as SessionRevert from "./revert"
@@ -0,0 +1,110 @@
1
+ import { InstanceState } from "@/effect/instance-state"
2
+ import { Runner } from "@/effect/runner"
3
+ import { Effect, Latch, Layer, Scope, Context } from "effect"
4
+ import * as Session from "./session"
5
+ import { MessageV2 } from "../message/message-v2"
6
+ import { SessionID } from "./schema"
7
+ import { SessionStatus } from "./status"
8
+
9
+ export interface Interface {
10
+ readonly assertNotBusy: (sessionID: SessionID) => Effect.Effect<void>
11
+ readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
12
+ readonly ensureRunning: (
13
+ sessionID: SessionID,
14
+ onInterrupt: Effect.Effect<MessageV2.WithParts>,
15
+ work: Effect.Effect<MessageV2.WithParts>,
16
+ ) => Effect.Effect<MessageV2.WithParts>
17
+ readonly startShell: (
18
+ sessionID: SessionID,
19
+ onInterrupt: Effect.Effect<MessageV2.WithParts>,
20
+ work: Effect.Effect<MessageV2.WithParts>,
21
+ ready?: Latch.Latch,
22
+ ) => Effect.Effect<MessageV2.WithParts>
23
+ }
24
+
25
+ export class Service extends Context.Service<Service, Interface>()("@saeeol/SessionRunState") {}
26
+
27
+ export const layer = Layer.effect(
28
+ Service,
29
+ Effect.gen(function* () {
30
+ const status = yield* SessionStatus.Service
31
+
32
+ const state = yield* InstanceState.make(
33
+ Effect.fn("SessionRunState.state")(function* () {
34
+ const scope = yield* Scope.Scope
35
+ const runners = new Map<SessionID, Runner.Runner<MessageV2.WithParts>>()
36
+ yield* Effect.addFinalizer(
37
+ Effect.fnUntraced(function* () {
38
+ yield* Effect.forEach(runners.values(), (runner) => runner.cancel, {
39
+ concurrency: "unbounded",
40
+ discard: true,
41
+ })
42
+ runners.clear()
43
+ }),
44
+ )
45
+ return { runners, scope }
46
+ }),
47
+ )
48
+
49
+ const runner = Effect.fn("SessionRunState.runner")(function* (
50
+ sessionID: SessionID,
51
+ onInterrupt: Effect.Effect<MessageV2.WithParts>,
52
+ ) {
53
+ const data = yield* InstanceState.get(state)
54
+ const existing = data.runners.get(sessionID)
55
+ if (existing) return existing
56
+ const next = Runner.make<MessageV2.WithParts>(data.scope, {
57
+ onIdle: Effect.gen(function* () {
58
+ data.runners.delete(sessionID)
59
+ yield* status.set(sessionID, { type: "idle" })
60
+ }),
61
+ onBusy: status.set(sessionID, { type: "busy" }),
62
+ onInterrupt,
63
+ busy: () => {
64
+ throw new Session.BusyError(sessionID)
65
+ },
66
+ })
67
+ data.runners.set(sessionID, next)
68
+ return next
69
+ })
70
+
71
+ const assertNotBusy = Effect.fn("SessionRunState.assertNotBusy")(function* (sessionID: SessionID) {
72
+ const data = yield* InstanceState.get(state)
73
+ const existing = data.runners.get(sessionID)
74
+ if (existing?.busy) throw new Session.BusyError(sessionID)
75
+ })
76
+
77
+ const cancel = Effect.fn("SessionRunState.cancel")(function* (sessionID: SessionID) {
78
+ const data = yield* InstanceState.get(state)
79
+ const existing = data.runners.get(sessionID)
80
+ if (!existing || !existing.busy) {
81
+ yield* status.set(sessionID, { type: "idle" })
82
+ return
83
+ }
84
+ yield* existing.cancel
85
+ })
86
+
87
+ const ensureRunning = Effect.fn("SessionRunState.ensureRunning")(function* (
88
+ sessionID: SessionID,
89
+ onInterrupt: Effect.Effect<MessageV2.WithParts>,
90
+ work: Effect.Effect<MessageV2.WithParts>,
91
+ ) {
92
+ return yield* (yield* runner(sessionID, onInterrupt)).ensureRunning(work)
93
+ })
94
+
95
+ const startShell = Effect.fn("SessionRunState.startShell")(function* (
96
+ sessionID: SessionID,
97
+ onInterrupt: Effect.Effect<MessageV2.WithParts>,
98
+ work: Effect.Effect<MessageV2.WithParts>,
99
+ ready?: Latch.Latch,
100
+ ) {
101
+ return yield* (yield* runner(sessionID, onInterrupt)).startShell(work, ready)
102
+ })
103
+
104
+ return Service.of({ assertNotBusy, cancel, ensureRunning, startShell })
105
+ }),
106
+ )
107
+
108
+ export const defaultLayer = layer.pipe(Layer.provide(SessionStatus.defaultLayer))
109
+
110
+ export * as SessionRunState from "./run-state"
@@ -0,0 +1,35 @@
1
+ import { Schema } from "effect"
2
+
3
+ import { Identifier } from "@/id/id"
4
+ import { zod, ZodOverride } from "@/util/effect-zod"
5
+ import { withStatics } from "@/util/schema"
6
+
7
+ export const SessionID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("session") }).pipe(
8
+ Schema.brand("SessionID"),
9
+ withStatics((s) => ({
10
+ descending: (id?: string) => s.make(Identifier.descending("session", id)),
11
+ zod: zod(s),
12
+ })),
13
+ )
14
+
15
+ export type SessionID = Schema.Schema.Type<typeof SessionID>
16
+
17
+ export const MessageID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("message") }).pipe(
18
+ Schema.brand("MessageID"),
19
+ withStatics((s) => ({
20
+ ascending: (id?: string) => s.make(Identifier.ascending("message", id)),
21
+ zod: zod(s),
22
+ })),
23
+ )
24
+
25
+ export type MessageID = Schema.Schema.Type<typeof MessageID>
26
+
27
+ export const PartID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("part") }).pipe(
28
+ Schema.brand("PartID"),
29
+ withStatics((s) => ({
30
+ ascending: (id?: string) => s.make(Identifier.ascending("part", id)),
31
+ zod: zod(s),
32
+ })),
33
+ )
34
+
35
+ export type PartID = Schema.Schema.Type<typeof PartID>
@@ -0,0 +1,160 @@
1
+ import path from "path"
2
+ import { Decimal } from "decimal.js"
3
+ import z from "zod"
4
+ import { type ProviderMetadata, type LanguageModelUsage } from "ai"
5
+ import { InstallationVersion } from "@saeeol/core/installation/version"
6
+ import { BusEvent } from "@/bus/bus-event"
7
+ import { SyncEvent } from "../../sync"
8
+ import { PartTable, SessionTable } from "./session.sql"
9
+ import * as Log from "@saeeol/core/util/log"
10
+ import { MessageV2 } from "../message/message-v2"
11
+ import { Snapshot } from "@/snapshot"
12
+ import { ProjectID } from "../../project/schema"
13
+ import { WorkspaceID } from "../../control-plane/schema"
14
+ import { SessionID, MessageID, PartID } from "./schema"
15
+ import type { Provider } from "@/provider/provider"
16
+ import { Permission } from "@/permission"
17
+ import { Global } from "@saeeol/core/global"
18
+ import { SaeeolSession } from "@/saeeol/session"
19
+ import { Effect, Schema, Types } from "effect"
20
+ import { zod } from "@/util/effect-zod"
21
+ import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@/util/schema"
22
+
23
+ const log = Log.create({ service: "session" })
24
+
25
+ const parentTitlePrefix = "New session - "
26
+ const childTitlePrefix = "Child session - "
27
+
28
+ export function createDefaultTitle(isChild = false) {
29
+ return (isChild ? childTitlePrefix : parentTitlePrefix) + new Date().toISOString()
30
+ }
31
+
32
+ export function isDefaultTitle(title: string) {
33
+ return new RegExp(`^(${parentTitlePrefix}|${childTitlePrefix})\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$`).test(title)
34
+ }
35
+
36
+ type SessionRow = typeof SessionTable.$inferSelect
37
+
38
+ export function fromRow(row: SessionRow): Info {
39
+ const summary = row.summary_additions !== null || row.summary_deletions !== null || row.summary_files !== null
40
+ ? { additions: row.summary_additions ?? 0, deletions: row.summary_deletions ?? 0, files: row.summary_files ?? 0, diffs: row.summary_diffs ?? undefined }
41
+ : undefined
42
+ const share = row.share_url ? { url: row.share_url } : undefined
43
+ return {
44
+ id: row.id, slug: row.slug, projectID: row.project_id, workspaceID: row.workspace_id ?? undefined,
45
+ directory: row.directory, path: row.path ?? undefined, parentID: row.parent_id ?? undefined,
46
+ title: row.title, version: row.version, summary, share, revert: row.revert ?? undefined,
47
+ permission: row.permission ?? undefined,
48
+ time: { created: row.time_created, updated: row.time_updated, compacting: row.time_compacting ?? undefined, archived: row.time_archived ?? undefined },
49
+ }
50
+ }
51
+
52
+ export function toRow(info: Info) {
53
+ return {
54
+ id: info.id, project_id: info.projectID, workspace_id: info.workspaceID, parent_id: info.parentID,
55
+ slug: info.slug, directory: info.directory, path: info.path, title: info.title, version: info.version,
56
+ share_url: info.share?.url, summary_additions: info.summary?.additions, summary_deletions: info.summary?.deletions,
57
+ summary_files: info.summary?.files, summary_diffs: info.summary?.diffs, revert: info.revert ?? null,
58
+ permission: info.permission, time_created: info.time.created, time_updated: info.time.updated,
59
+ time_compacting: info.time.compacting, time_archived: info.time.archived,
60
+ }
61
+ }
62
+
63
+ export function getForkedTitle(title: string): string {
64
+ const match = title.match(/^(.+) \(fork #(\d+)\)$/)
65
+ if (match) return `${match[1]} (fork #${parseInt(match[2], 10) + 1})`
66
+ return `${title} (fork #1)`
67
+ }
68
+
69
+ export function sessionPath(worktree: string, cwd: string) {
70
+ return path.relative(path.resolve(worktree), cwd).replaceAll("\\", "/")
71
+ }
72
+
73
+ const Summary = Schema.Struct({ additions: NonNegativeInt, deletions: NonNegativeInt, files: NonNegativeInt, diffs: optionalOmitUndefined(Schema.Array(Snapshot.SummaryFileDiff)) })
74
+ const Share = Schema.Struct({ url: Schema.String })
75
+ export const ArchivedTimestamp = Schema.Finite
76
+ const Time = Schema.Struct({ created: NonNegativeInt, updated: NonNegativeInt, compacting: optionalOmitUndefined(NonNegativeInt), archived: optionalOmitUndefined(ArchivedTimestamp) })
77
+ const Revert = Schema.Struct({ messageID: MessageID, partID: optionalOmitUndefined(PartID), snapshot: optionalOmitUndefined(Schema.String), diff: optionalOmitUndefined(Schema.String) })
78
+
79
+ export const Info = Schema.Struct({
80
+ id: SessionID, slug: Schema.String, projectID: ProjectID, workspaceID: optionalOmitUndefined(WorkspaceID),
81
+ directory: Schema.String, path: optionalOmitUndefined(Schema.String), parentID: optionalOmitUndefined(SessionID),
82
+ summary: optionalOmitUndefined(Summary), share: optionalOmitUndefined(Share), title: Schema.String,
83
+ version: Schema.String, time: Time, permission: optionalOmitUndefined(Permission.Ruleset), revert: optionalOmitUndefined(Revert),
84
+ }).annotate({ identifier: "Session" }).pipe(withStatics((s) => ({ zod: zod(s) })))
85
+ export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
86
+
87
+ export const ProjectInfo = Schema.Struct({ id: ProjectID, name: optionalOmitUndefined(Schema.String), worktree: Schema.String })
88
+ .annotate({ identifier: "ProjectSummary" }).pipe(withStatics((s) => ({ zod: zod(s) })))
89
+ export type ProjectInfo = Types.DeepMutable<Schema.Schema.Type<typeof ProjectInfo>>
90
+
91
+ export const GlobalInfo = Schema.Struct({ ...Info.fields, project: Schema.NullOr(ProjectInfo), worktreeName: Schema.optional(Schema.String) })
92
+ .annotate({ identifier: "GlobalSession" }).pipe(withStatics((s) => ({ zod: zod(s) })))
93
+ export type GlobalInfo = Types.DeepMutable<Schema.Schema.Type<typeof GlobalInfo>>
94
+
95
+ export const CreateInput = Schema.optional(Schema.Struct({
96
+ parentID: Schema.optional(SessionID), title: Schema.optional(Schema.String), permission: Schema.optional(Permission.Ruleset),
97
+ platform: Schema.optional(Schema.String), workspaceID: Schema.optional(WorkspaceID),
98
+ })).pipe(withStatics((s) => ({ zod: zod(s) })))
99
+ export type CreateInput = Types.DeepMutable<Schema.Schema.Type<typeof CreateInput>>
100
+
101
+ export const ForkInput = Schema.Struct({ sessionID: SessionID, messageID: Schema.optional(MessageID) }).pipe(withStatics((s) => ({ zod: zod(s) })))
102
+ export const GetInput = SessionID
103
+ export const ChildrenInput = SessionID
104
+ export const RemoveInput = SessionID
105
+ export const SetTitleInput = Schema.Struct({ sessionID: SessionID, title: Schema.String }).pipe(withStatics((s) => ({ zod: zod(s) })))
106
+ export const SetArchivedInput = Schema.Struct({ sessionID: SessionID, time: Schema.optional(ArchivedTimestamp) }).pipe(withStatics((s) => ({ zod: zod(s) })))
107
+ export const SetPermissionInput = Schema.Struct({ sessionID: SessionID, permission: Permission.Ruleset }).pipe(withStatics((s) => ({ zod: zod(s) })))
108
+ export const SetRevertInput = Schema.Struct({ sessionID: SessionID, revert: Schema.optional(Revert), summary: Schema.optional(Summary) }).pipe(withStatics((s) => ({ zod: zod(s) })))
109
+ export const MessagesInput = Schema.Struct({ sessionID: SessionID, limit: Schema.optional(NonNegativeInt) }).pipe(withStatics((s) => ({ zod: zod(s) })))
110
+
111
+ export type ListInput = { directory?: string; scope?: "project"; path?: string; workspaceID?: WorkspaceID; roots?: boolean; start?: number; search?: string; limit?: number }
112
+
113
+ const CreatedEventSchema = Schema.Struct({ sessionID: SessionID, info: Info })
114
+ const UpdatedShare = Schema.Struct({ url: Schema.optional(Schema.NullOr(Schema.String)) })
115
+ const UpdatedTime = Schema.Struct({ created: Schema.optional(Schema.NullOr(NonNegativeInt)), updated: Schema.optional(Schema.NullOr(NonNegativeInt)), compacting: Schema.optional(Schema.NullOr(NonNegativeInt)), archived: Schema.optional(Schema.NullOr(ArchivedTimestamp)) })
116
+ const UpdatedInfo = Schema.Struct({
117
+ id: Schema.optional(Schema.NullOr(SessionID)), slug: Schema.optional(Schema.NullOr(Schema.String)),
118
+ projectID: Schema.optional(Schema.NullOr(ProjectID)), workspaceID: Schema.optional(Schema.NullOr(WorkspaceID)),
119
+ directory: Schema.optional(Schema.NullOr(Schema.String)), path: Schema.optional(Schema.NullOr(Schema.String)),
120
+ parentID: Schema.optional(Schema.NullOr(SessionID)), summary: Schema.optional(Schema.NullOr(Summary)),
121
+ share: Schema.optional(UpdatedShare), title: Schema.optional(Schema.NullOr(Schema.String)),
122
+ version: Schema.optional(Schema.NullOr(Schema.String)), time: Schema.optional(UpdatedTime),
123
+ permission: Schema.optional(Schema.NullOr(Permission.Ruleset)), revert: Schema.optional(Schema.NullOr(Revert)),
124
+ })
125
+ const UpdatedEventSchema = Schema.Struct({ sessionID: SessionID, info: UpdatedInfo })
126
+
127
+ export const Event = {
128
+ Created: SyncEvent.define({ type: "session.created", version: 1, aggregate: "sessionID", schema: CreatedEventSchema }),
129
+ Updated: SyncEvent.define({ type: "session.updated", version: 1, aggregate: "sessionID", schema: UpdatedEventSchema, busSchema: CreatedEventSchema }),
130
+ Deleted: SyncEvent.define({ type: "session.deleted", version: 1, aggregate: "sessionID", schema: CreatedEventSchema }),
131
+ Diff: BusEvent.define("session.diff", Schema.Struct({ sessionID: SessionID, diff: Schema.Array(Snapshot.FileDiff) })),
132
+ Error: BusEvent.define("session.error", Schema.Struct({ sessionID: Schema.optional(SessionID), error: MessageV2.Assistant.fields.error })),
133
+ TurnOpen: SaeeolSession.Event.TurnOpen,
134
+ TurnClose: SaeeolSession.Event.TurnClose,
135
+ }
136
+
137
+ export function plan(input: { slug: string; time: { created: number } }, instance: any) {
138
+ const base = instance.project.vcs ? path.join(instance.worktree, ".saeeol", "plans") : path.join(Global.Path.data, "plans")
139
+ return path.join(base, [input.time.created, input.slug].join("-") + ".md")
140
+ }
141
+
142
+ export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsage; metadata?: ProviderMetadata; provider?: Provider.Info }) => {
143
+ const safe = (v: number) => (Number.isFinite(v) ? v : 0)
144
+ const inputTokens = safe(input.usage.inputTokens ?? 0)
145
+ const outputTokens = safe(input.usage.outputTokens ?? 0)
146
+ const reasoningTokens = safe(input.usage.outputTokenDetails?.reasoningTokens ?? input.usage.reasoningTokens ?? 0)
147
+ const cacheRead = safe(input.usage.inputTokenDetails?.cacheReadTokens ?? input.usage.cachedInputTokens ?? 0)
148
+ const cacheWrite = safe(Number(input.usage.inputTokenDetails?.cacheWriteTokens ?? input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ?? (input.metadata as any)?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? (input.metadata as any)?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ?? 0))
149
+ const adjustedInput = safe(inputTokens - cacheRead - cacheWrite)
150
+ const tokens = { total: input.usage.totalTokens, input: adjustedInput, output: safe(outputTokens - reasoningTokens), reasoning: reasoningTokens, cache: { write: cacheWrite, read: cacheRead } }
151
+ const reported = SaeeolSession.providerCost({ metadata: input.metadata, usage: input.usage, provider: input.provider, providerID: input.model.providerID })
152
+ if (reported !== undefined) return { cost: safe(reported), tokens }
153
+ const costInfo = input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000 ? input.model.cost.experimentalOver200K : input.model.cost
154
+ return {
155
+ cost: safe(new Decimal(0).add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000)).add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000)).add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000)).add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000)).add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000)).toNumber()),
156
+ tokens,
157
+ }
158
+ }
159
+
160
+ export class BusyError extends Error { constructor(public readonly sessionID: string) { super(`Session ${sessionID} is busy`) } }
@@ -0,0 +1,124 @@
1
+ import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core"
2
+ import { ProjectTable } from "../../project/project.sql"
3
+ import type { MessageV2 } from "../message/message-v2"
4
+ import type { SessionEntry } from "../../v2/session-entry"
5
+ import type { Snapshot } from "../../snapshot"
6
+ import type { Permission } from "../../permission"
7
+ import type { ProjectID } from "../../project/schema"
8
+ import type { SessionID, MessageID, PartID } from "./schema"
9
+ import type { WorkspaceID } from "../../control-plane/schema"
10
+ import { Timestamps } from "../../storage/schema.sql"
11
+
12
+ type PartData = Omit<MessageV2.Part, "id" | "sessionID" | "messageID">
13
+ type InfoData = Omit<MessageV2.Info, "id" | "sessionID">
14
+
15
+ export const SessionTable = sqliteTable(
16
+ "session",
17
+ {
18
+ id: text().$type<SessionID>().primaryKey(),
19
+ project_id: text()
20
+ .$type<ProjectID>()
21
+ .notNull()
22
+ .references(() => ProjectTable.id, { onDelete: "cascade" }),
23
+ workspace_id: text().$type<WorkspaceID>(),
24
+ parent_id: text().$type<SessionID>(),
25
+ slug: text().notNull(),
26
+ directory: text().notNull(),
27
+ path: text(),
28
+ title: text().notNull(),
29
+ version: text().notNull(),
30
+ share_url: text(),
31
+ summary_additions: integer(),
32
+ summary_deletions: integer(),
33
+ summary_files: integer(),
34
+ summary_diffs: text({ mode: "json" }).$type<Snapshot.SummaryFileDiff[]>(),
35
+ revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: PartID; snapshot?: string; diff?: string }>(),
36
+ permission: text({ mode: "json" }).$type<Permission.Ruleset>(),
37
+ ...Timestamps,
38
+ time_compacting: integer(),
39
+ time_archived: integer(),
40
+ },
41
+ (table) => [
42
+ index("session_project_idx").on(table.project_id),
43
+ index("session_workspace_idx").on(table.workspace_id),
44
+ index("session_parent_idx").on(table.parent_id),
45
+ ],
46
+ )
47
+
48
+ export const MessageTable = sqliteTable(
49
+ "message",
50
+ {
51
+ id: text().$type<MessageID>().primaryKey(),
52
+ session_id: text()
53
+ .$type<SessionID>()
54
+ .notNull()
55
+ .references(() => SessionTable.id, { onDelete: "cascade" }),
56
+ ...Timestamps,
57
+ data: text({ mode: "json" }).notNull().$type<InfoData>(),
58
+ },
59
+ (table) => [index("message_session_time_created_id_idx").on(table.session_id, table.time_created, table.id)],
60
+ )
61
+
62
+ export const PartTable = sqliteTable(
63
+ "part",
64
+ {
65
+ id: text().$type<PartID>().primaryKey(),
66
+ message_id: text()
67
+ .$type<MessageID>()
68
+ .notNull()
69
+ .references(() => MessageTable.id, { onDelete: "cascade" }),
70
+ session_id: text().$type<SessionID>().notNull(),
71
+ ...Timestamps,
72
+ data: text({ mode: "json" }).notNull().$type<PartData>(),
73
+ },
74
+ (table) => [
75
+ index("part_message_id_id_idx").on(table.message_id, table.id),
76
+ index("part_session_idx").on(table.session_id),
77
+ ],
78
+ )
79
+
80
+ export const TodoTable = sqliteTable(
81
+ "todo",
82
+ {
83
+ session_id: text()
84
+ .$type<SessionID>()
85
+ .notNull()
86
+ .references(() => SessionTable.id, { onDelete: "cascade" }),
87
+ content: text().notNull(),
88
+ status: text().notNull(),
89
+ priority: text().notNull(),
90
+ position: integer().notNull(),
91
+ ...Timestamps,
92
+ },
93
+ (table) => [
94
+ primaryKey({ columns: [table.session_id, table.position] }),
95
+ index("todo_session_idx").on(table.session_id),
96
+ ],
97
+ )
98
+
99
+ export const SessionEntryTable = sqliteTable(
100
+ "session_entry",
101
+ {
102
+ id: text().$type<SessionEntry.ID>().primaryKey(),
103
+ session_id: text()
104
+ .$type<SessionID>()
105
+ .notNull()
106
+ .references(() => SessionTable.id, { onDelete: "cascade" }),
107
+ type: text().$type<SessionEntry.Type>().notNull(),
108
+ ...Timestamps,
109
+ data: text({ mode: "json" }).notNull().$type<Omit<SessionEntry.Entry, "type" | "id">>(),
110
+ },
111
+ (table) => [
112
+ index("session_entry_session_idx").on(table.session_id),
113
+ index("session_entry_session_type_idx").on(table.session_id, table.type),
114
+ index("session_entry_time_created_idx").on(table.time_created),
115
+ ],
116
+ )
117
+
118
+ export const PermissionTable = sqliteTable("permission", {
119
+ project_id: text()
120
+ .primaryKey()
121
+ .references(() => ProjectTable.id, { onDelete: "cascade" }),
122
+ ...Timestamps,
123
+ data: text({ mode: "json" }).notNull().$type<Permission.Ruleset>(),
124
+ })