dsh-edge 0.1.0

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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +183 -0
  4. package/README.zh.md +183 -0
  5. package/THIRD_PARTY_NOTICES.md +199 -0
  6. package/dist/_headers +3 -0
  7. package/dist/assets/fonts/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  8. package/dist/assets/fonts/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  9. package/dist/assets/fonts/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  10. package/dist/assets/fonts/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  11. package/dist/assets/fonts/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  12. package/dist/assets/fonts/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  13. package/dist/assets/fonts/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  14. package/dist/assets/fonts/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  15. package/dist/assets/fonts/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  16. package/dist/assets/fonts/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  17. package/dist/assets/fonts/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  18. package/dist/assets/fonts/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  19. package/dist/assets/fonts/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  20. package/dist/assets/fonts/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  21. package/dist/assets/fonts/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  22. package/dist/assets/fonts/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  23. package/dist/assets/fonts/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  24. package/dist/assets/fonts/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  25. package/dist/assets/fonts/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  26. package/dist/assets/fonts/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  27. package/dist/assets/fonts/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  28. package/dist/assets/fonts/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  29. package/dist/assets/fonts/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  30. package/dist/assets/fonts/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  31. package/dist/assets/fonts/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  32. package/dist/assets/fonts/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  33. package/dist/assets/fonts/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  34. package/dist/assets/fonts/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  35. package/dist/assets/fonts/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  36. package/dist/assets/fonts/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  37. package/dist/assets/fonts/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  38. package/dist/assets/fonts/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  39. package/dist/assets/fonts/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  40. package/dist/assets/fonts/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  41. package/dist/assets/fonts/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  42. package/dist/assets/fonts/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  43. package/dist/assets/fonts/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  44. package/dist/assets/fonts/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  45. package/dist/assets/fonts/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  46. package/dist/assets/fonts/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  47. package/dist/assets/fonts/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  48. package/dist/assets/fonts/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  49. package/dist/assets/fonts/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  50. package/dist/assets/fonts/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  51. package/dist/assets/fonts/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  52. package/dist/assets/fonts/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  53. package/dist/assets/fonts/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  54. package/dist/assets/fonts/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  55. package/dist/assets/fonts/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  56. package/dist/assets/fonts/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  57. package/dist/assets/fonts/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  58. package/dist/assets/fonts/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  59. package/dist/assets/fonts/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  60. package/dist/assets/fonts/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  61. package/dist/assets/fonts/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  62. package/dist/assets/fonts/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  63. package/dist/assets/fonts/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  64. package/dist/assets/fonts/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  65. package/dist/assets/fonts/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  66. package/dist/assets/index-C-1AiF3k.js +112 -0
  67. package/dist/assets/index-CSGf6Qzd.css +1 -0
  68. package/dist/assets/langs/c-BIGW1oBm.js +2 -0
  69. package/dist/assets/langs/cpp-DIPi6g--.js +2 -0
  70. package/dist/assets/langs/csharp-DSvCPggb.js +2 -0
  71. package/dist/assets/langs/css-CLj8gQPS.js +2 -0
  72. package/dist/assets/langs/go-C27-OAKa.js +2 -0
  73. package/dist/assets/langs/html-CfGypltT.js +2 -0
  74. package/dist/assets/langs/ini-BEwlwnbL.js +2 -0
  75. package/dist/assets/langs/java-CylS5w8V.js +2 -0
  76. package/dist/assets/langs/kotlin-BdnUsdx6.js +2 -0
  77. package/dist/assets/langs/less-B1dDrJ26.js +2 -0
  78. package/dist/assets/langs/lua-BaeVxFsk.js +2 -0
  79. package/dist/assets/langs/markdown-Cvjx9yec.js +2 -0
  80. package/dist/assets/langs/mdx-Cmh6b_Ma.js +2 -0
  81. package/dist/assets/langs/php-D-uCvoST.js +2 -0
  82. package/dist/assets/langs/python-B6aJPvgy.js +2 -0
  83. package/dist/assets/langs/ruby-DEItuUFs.js +2 -0
  84. package/dist/assets/langs/rust-B1yitclQ.js +2 -0
  85. package/dist/assets/langs/scss-D5BDwBP9.js +2 -0
  86. package/dist/assets/langs/sql-CRqJ_cUM.js +2 -0
  87. package/dist/assets/langs/swift-D82vCrfD.js +2 -0
  88. package/dist/assets/langs/toml-vGWfd6FD.js +2 -0
  89. package/dist/assets/langs/xml-sdJ4AIDG.js +2 -0
  90. package/dist/assets/langs/yaml-Buea-lGh.js +2 -0
  91. package/dist/assets/vendor-Cjbwl5VI.js +411 -0
  92. package/dist/assets/vendor-CjyC-hUb.css +1 -0
  93. package/dist/favicon.svg +8 -0
  94. package/dist/index.html +35 -0
  95. package/dist/manifest.webmanifest +16 -0
  96. package/dist/plugins/@deepseek-ai/dsh-api-gateway/client.js +420 -0
  97. package/dist/plugins/@deepseek-ai/dsh-api-remotes/client.js +5937 -0
  98. package/dist/plugins/@deepseek-ai/dsh-client-connection/client.js +10207 -0
  99. package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +1231 -0
  100. package/dist/plugins/@deepseek-ai/dsh-client-modules/client.js +259 -0
  101. package/dist/plugins/@deepseek-ai/dsh-client-runtime/client.js +10539 -0
  102. package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +1725 -0
  103. package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +1116 -0
  104. package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +9832 -0
  105. package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +373 -0
  106. package/dist/plugins/@deepseek-ai/dsh-client-ui-edge/client.js +549 -0
  107. package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +859 -0
  108. package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +280 -0
  109. package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +456 -0
  110. package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +804 -0
  111. package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +461 -0
  112. package/dist/plugins/@deepseek-ai/dsh-client-ui-settings/client.js +254 -0
  113. package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +609 -0
  114. package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +292 -0
  115. package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +327 -0
  116. package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +710 -0
  117. package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +1312 -0
  118. package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +1630 -0
  119. package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +7370 -0
  120. package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +700 -0
  121. package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +474 -0
  122. package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +2426 -0
  123. package/dist/plugins/@deepseek-ai/dsh-typert-registry/client.js +1372 -0
  124. package/package.json +97 -0
  125. package/scripts/assemble-web.mjs +230 -0
  126. package/scripts/bundle-size.d.mts +2 -0
  127. package/scripts/bundle-size.mjs +26 -0
  128. package/scripts/bundle.mjs +53 -0
  129. package/scripts/cli.d.mts +39 -0
  130. package/scripts/cli.mjs +496 -0
  131. package/scripts/install.d.mts +149 -0
  132. package/scripts/install.mjs +989 -0
  133. package/scripts/legal-files.mjs +24 -0
  134. package/scripts/verify-packed.mjs +32 -0
  135. package/scripts/wrangler-config.d.mts +18 -0
  136. package/scripts/wrangler-config.mjs +84 -0
  137. package/src/agent.ts +119 -0
  138. package/src/auth.ts +352 -0
  139. package/src/deepseek.ts +231 -0
  140. package/src/deployment.ts +147 -0
  141. package/src/direct-shell-core-empty.ts +10 -0
  142. package/src/direct-shell-protocol.ts +5 -0
  143. package/src/direct-shell.ts +341 -0
  144. package/src/do-session-persistence.ts +1064 -0
  145. package/src/edge-api.ts +796 -0
  146. package/src/edge-credentials.ts +56 -0
  147. package/src/edge-remotes.ts +41 -0
  148. package/src/edge-workspace-store.ts +339 -0
  149. package/src/http.ts +212 -0
  150. package/src/index.ts +196 -0
  151. package/src/instance.ts +1007 -0
  152. package/src/isolated-direct-shell-unavailable.ts +11 -0
  153. package/src/protocol.ts +39 -0
  154. package/src/release.ts +9 -0
  155. package/src/session-store.ts +1110 -0
  156. package/src/sse.ts +86 -0
  157. package/src/web-search.ts +38 -0
  158. package/src/workspace.ts +270 -0
  159. package/wrangler.jsonc +56 -0
@@ -0,0 +1,1064 @@
1
+ /** Upstream SessionPersistence implemented over Cloudflare Durable Object SQL. */
2
+
3
+ import { Context } from '@deepseek-ai/cordis'
4
+ import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
5
+ import type {
6
+ Session,
7
+ SessionEvent,
8
+ SessionId,
9
+ SessionHeader,
10
+ SessionPreparation,
11
+ SurfaceOp,
12
+ } from '@deepseek-ai/dsh-session'
13
+ import {
14
+ DEFAULT_PREPARED_SESSION_CACHE_SIZE,
15
+ DEFAULT_WRITE_BATCH_MAX_DELAY_MS,
16
+ PersistenceCoordinator,
17
+ SessionFormatUnsupportedError,
18
+ SessionPersistence,
19
+ SessionPersistenceRevision,
20
+ sessionFormatVersionRefusal,
21
+ type PersistenceBackend,
22
+ type SessionInspection,
23
+ type SessionLocation,
24
+ type SessionPersistenceRevision as PersistenceRevision,
25
+ type SessionPersistenceSnapshot,
26
+ type StoredPrefix,
27
+ type StoredSuffix,
28
+ } from '@deepseek-ai/dsh-session-persistence'
29
+ import {
30
+ SessionTitleProviderId,
31
+ type SessionTitleEventData,
32
+ type SessionTitleSource,
33
+ } from '@deepseek-ai/dsh-session-title'
34
+
35
+ /** Versions the DO table layout; SessionHeader.version owns the event format. */
36
+ const DO_SESSION_SCHEMA_VERSION = 2
37
+
38
+ interface DurableObjectSessionPersistenceConfig {
39
+ storage: DurableObjectStorage
40
+ preparedSessionCacheSize?: number
41
+ writeBatchMaxDelayMs?: number
42
+ }
43
+
44
+ interface StateRow extends Record<string, SqlStorageValue> {
45
+ schema_version: number
46
+ store_id: string
47
+ }
48
+
49
+ interface HeaderRow extends Record<string, SqlStorageValue> {
50
+ id: string
51
+ version: number
52
+ created_at: number
53
+ cwd: string | null
54
+ parent_session: string | null
55
+ seed_length: number | null
56
+ origin: string | null
57
+ delegation_depth: number | null
58
+ agent_preset: string | null
59
+ incarnation: string
60
+ revision: number
61
+ }
62
+
63
+ interface BlankSessionRow extends Record<string, SqlStorageValue> {
64
+ id: string
65
+ version: number
66
+ created_at: number
67
+ cwd: string | null
68
+ parent_session: string | null
69
+ seed_length: number | null
70
+ origin: string | null
71
+ delegation_depth: number | null
72
+ agent_preset: string | null
73
+ }
74
+
75
+ interface EventRow extends Record<string, SqlStorageValue> {
76
+ seq: number
77
+ type: string
78
+ time: number
79
+ data: string
80
+ source_event_seqs: string | null
81
+ surface_op: string | null
82
+ ignorable: number | null
83
+ }
84
+
85
+ interface EventSizeRow extends Record<string, SqlStorageValue> {
86
+ seq: number
87
+ stored_bytes: number
88
+ parseable: number
89
+ }
90
+
91
+ interface LastSequenceRow extends Record<string, SqlStorageValue> {
92
+ seq: number | null
93
+ }
94
+
95
+ interface HistoryBoundaryRow extends Record<string, SqlStorageValue> {
96
+ seq: number
97
+ source_event_seqs: string | null
98
+ }
99
+
100
+ /** Shared response budget applied before live/cold history paths diverge. */
101
+ export const EDGE_HISTORY_PAGE_LIMITS = {
102
+ maxEvents: 8_192,
103
+ maxStoredBytes: 8 * 1_024 * 1_024,
104
+ maxMessages: 50,
105
+ } as const
106
+
107
+ interface SessionSummaryRow extends HeaderRow {
108
+ updated_at: number | null
109
+ last_prompt_at: number | null
110
+ last_seq: number | null
111
+ blank: number
112
+ title_seq: number | null
113
+ title_time: number | null
114
+ title_data: string | null
115
+ }
116
+
117
+ const SESSION_SUMMARY_SELECT = `SELECT s.id, s.version, s.created_at, s.cwd, s.parent_session,
118
+ s.seed_length, s.origin, s.delegation_depth, s.agent_preset, s.incarnation, s.revision,
119
+ (SELECT e.time FROM dsh_session_events e
120
+ WHERE e.session_id = s.id ORDER BY e.seq DESC LIMIT 1) AS updated_at,
121
+ (SELECT e.time FROM dsh_session_events e
122
+ WHERE e.session_id = s.id AND e.type = 'user/message'
123
+ AND json_extract(e.data, '$.source.kind') = 'user'
124
+ ORDER BY e.seq DESC LIMIT 1) AS last_prompt_at,
125
+ (SELECT e.seq FROM dsh_session_events e
126
+ WHERE e.session_id = s.id ORDER BY e.seq DESC LIMIT 1) AS last_seq,
127
+ NOT EXISTS (
128
+ SELECT 1 FROM dsh_session_events e
129
+ WHERE e.session_id = s.id AND e.type = 'turn/start'
130
+ ) AS blank,
131
+ (SELECT e.seq FROM dsh_session_events e
132
+ WHERE e.session_id = s.id AND e.type = 'session/title'
133
+ ORDER BY e.seq DESC LIMIT 1) AS title_seq,
134
+ (SELECT e.time FROM dsh_session_events e
135
+ WHERE e.session_id = s.id AND e.type = 'session/title'
136
+ ORDER BY e.seq DESC LIMIT 1) AS title_time,
137
+ (SELECT e.data FROM dsh_session_events e
138
+ WHERE e.session_id = s.id AND e.type = 'session/title'
139
+ ORDER BY e.seq DESC LIMIT 1) AS title_data
140
+ FROM dsh_sessions s`
141
+
142
+ /** One cheap session-list item derived from canonical header/event rows. */
143
+ export interface EdgeStoredSessionSummary {
144
+ meta: SessionHeader
145
+ titleEvent?: SessionEvent<'session/title'>
146
+ updatedAt: number
147
+ lastPromptAt: number | null
148
+ lastSeq: number
149
+ blank: boolean
150
+ }
151
+
152
+ /** One bounded session-list page. */
153
+ export interface EdgeStoredSessionSummaryPage {
154
+ sessions: EdgeStoredSessionSummary[]
155
+ hasMore: boolean
156
+ }
157
+
158
+ /** One validated page retained within the Edge replay byte budget. */
159
+ export interface EdgeEventPage {
160
+ meta: SessionHeader
161
+ events: SessionEvent[]
162
+ hasMore: boolean
163
+ }
164
+
165
+ /** One upstream-compatible history window selected before event payloads are decoded. */
166
+ export interface EdgeStoredHistoryPage {
167
+ summary: EdgeStoredSessionSummary
168
+ events: SessionEvent[]
169
+ hasMore: boolean
170
+ }
171
+
172
+ /** Cordis service + backend primitives; coordinator owns all session orchestration. */
173
+ export class DurableObjectSessionPersistence
174
+ extends SessionPersistence
175
+ implements PersistenceBackend<number> {
176
+ static inject = ['sessions']
177
+
178
+ override readonly name = 'session-persistence-durable-object'
179
+ override readonly supportsRawArtifacts = false
180
+
181
+ private readonly storage: DurableObjectStorage
182
+ private readonly storeIdentity: string
183
+ private readonly coordinator: PersistenceCoordinator<number>
184
+
185
+ constructor(ctx: Context, config: DurableObjectSessionPersistenceConfig) {
186
+ super(ctx)
187
+ this.storage = config.storage
188
+ this.storeIdentity = this.initialize()
189
+ this.coordinator = new PersistenceCoordinator(ctx, this, {
190
+ preparedSessionCacheSize: config.preparedSessionCacheSize
191
+ ?? DEFAULT_PREPARED_SESSION_CACHE_SIZE,
192
+ writeBatchMaxDelayMs: config.writeBatchMaxDelayMs
193
+ ?? DEFAULT_WRITE_BATCH_MAX_DELAY_MS,
194
+ })
195
+ }
196
+
197
+ locate(_meta: SessionHeader): SessionLocation | undefined {
198
+ return undefined
199
+ }
200
+
201
+ create(meta: SessionHeader): Promise<void> {
202
+ return this.coordinator.create(meta)
203
+ }
204
+
205
+ append(id: SessionId, events: readonly SessionEvent[]): Promise<void> {
206
+ return this.coordinator.append(id, events)
207
+ }
208
+
209
+ /** Prevent disposal from retrying a failed first materialization. */
210
+ abandonUnmaterializedSession(session: Session): Promise<void> {
211
+ return this.coordinator.abandonUnmaterialized(session)
212
+ }
213
+
214
+ override prepare(id: SessionId, signal?: AbortSignal): Promise<SessionPreparation> {
215
+ return this.coordinator.prepare(id, signal)
216
+ }
217
+
218
+ load(id: SessionId): Promise<SessionInspection> {
219
+ return this.coordinator.load(id)
220
+ }
221
+
222
+ inspect(id: SessionId, signal?: AbortSignal): Promise<SessionInspection> {
223
+ return this.coordinator.inspect(id, signal)
224
+ }
225
+
226
+ readFrom(
227
+ id: SessionId,
228
+ fromSeq: number,
229
+ signal?: AbortSignal,
230
+ ): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
231
+ return this.coordinator.readFrom(id, fromSeq, signal)
232
+ }
233
+
234
+ /** Read and canonically validate one Edge-only replay page. */
235
+ readEventPage(
236
+ id: SessionId,
237
+ fromSeq: number,
238
+ limit: number,
239
+ maxStoredBytes: number,
240
+ signal?: AbortSignal,
241
+ ): Promise<EdgeEventPage> {
242
+ if (!Number.isSafeInteger(maxStoredBytes) || maxStoredBytes <= 0) {
243
+ return Promise.reject(new TypeError(
244
+ `readEventPage maxStoredBytes must be a positive safe integer, got ${String(maxStoredBytes)}`,
245
+ ))
246
+ }
247
+ return this.coordinator.readValidatedPage(
248
+ id,
249
+ fromSeq,
250
+ limit,
251
+ (pageId, pageFromSeq, pageLimit, pageSignal) => this.loadStoredPage(
252
+ pageId,
253
+ pageFromSeq,
254
+ pageLimit,
255
+ maxStoredBytes,
256
+ pageSignal,
257
+ ),
258
+ signal,
259
+ )
260
+ }
261
+
262
+ /** Select a cold history boundary in SQL, then decode only that raw event window. */
263
+ async readHistoryPage(
264
+ id: SessionId,
265
+ beforeSeq: number | undefined,
266
+ maxMessages: number,
267
+ signal?: AbortSignal,
268
+ ): Promise<EdgeStoredHistoryPage | undefined> {
269
+ const summary = this.readSessionSummary(id)
270
+ if (summary === undefined) return undefined
271
+ const boundaryLimit = Math.min(maxMessages, EDGE_HISTORY_PAGE_LIMITS.maxMessages)
272
+ const upperExclusive = Math.min(beforeSeq ?? summary.lastSeq + 1, summary.lastSeq + 1)
273
+ if (upperExclusive <= 0) return { summary, events: [], hasMore: false }
274
+ const boundaryRows = this.storage.sql.exec<HistoryBoundaryRow>(
275
+ `SELECT seq, source_event_seqs FROM dsh_session_events
276
+ WHERE session_id = ? AND seq < ?
277
+ AND type IN ('user/message', 'assistant/message')
278
+ AND surface_op = '"append"'
279
+ ORDER BY seq DESC LIMIT ?`,
280
+ id,
281
+ upperExclusive,
282
+ boundaryLimit,
283
+ ).toArray()
284
+ const oldestBoundary = boundaryRows.at(-1)
285
+ const boundary = boundaryRows.length < boundaryLimit || oldestBoundary === undefined
286
+ ? 0
287
+ : historyGroupStart(oldestBoundary)
288
+ if (boundary >= upperExclusive) return { summary, events: [], hasMore: boundary > 0 }
289
+ const page = await this.coordinator.readValidatedPage(
290
+ id,
291
+ boundary,
292
+ EDGE_HISTORY_PAGE_LIMITS.maxEvents,
293
+ (pageId, pageFromSeq, pageLimit, pageSignal) => {
294
+ if (pageFromSeq !== boundary) {
295
+ return Promise.reject(new Error(
296
+ `stored session ${id} history contains a legacy event that requires an unbounded prefix`,
297
+ ))
298
+ }
299
+ return this.loadStoredPage(
300
+ pageId,
301
+ pageFromSeq,
302
+ pageLimit,
303
+ EDGE_HISTORY_PAGE_LIMITS.maxStoredBytes,
304
+ pageSignal,
305
+ upperExclusive,
306
+ )
307
+ },
308
+ signal,
309
+ )
310
+ if (page.hasMore) {
311
+ throw new Error(
312
+ `stored session ${id} history page exceeds the Edge limit of ${EDGE_HISTORY_PAGE_LIMITS.maxEvents} events or ${EDGE_HISTORY_PAGE_LIMITS.maxStoredBytes} stored bytes`,
313
+ )
314
+ }
315
+ return { summary, events: page.events, hasMore: boundary > 0 }
316
+ }
317
+
318
+ loadStored(
319
+ id: SessionId,
320
+ signal?: AbortSignal,
321
+ ): Promise<StoredPrefix<number> | undefined> {
322
+ return promiseFromSync(() => {
323
+ signal?.throwIfAborted()
324
+ const snapshot = this.storage.transactionSync(() => {
325
+ const row = this.rowFor(id)
326
+ if (row === undefined) return undefined
327
+ return { row, eventRows: this.eventRows(id, 0) }
328
+ })
329
+ signal?.throwIfAborted()
330
+ if (snapshot === undefined) return undefined
331
+ const { preserved, tornFrom } = scanRows(snapshot.eventRows)
332
+ return {
333
+ meta: rowToHeader(snapshot.row),
334
+ events: preserved,
335
+ revision: revisionOf(this.storeIdentity, snapshot.row),
336
+ ...tornFrom === undefined ? {} : { tornMarker: tornFrom },
337
+ }
338
+ })
339
+ }
340
+
341
+ readStoredRevision(
342
+ id: SessionId,
343
+ signal?: AbortSignal,
344
+ ): Promise<PersistenceRevision | undefined> {
345
+ return promiseFromSync(() => {
346
+ signal?.throwIfAborted()
347
+ const row = this.rowFor(id)
348
+ signal?.throwIfAborted()
349
+ return row === undefined ? undefined : revisionOf(this.storeIdentity, row)
350
+ })
351
+ }
352
+
353
+ loadStoredFrom(
354
+ id: SessionId,
355
+ fromSeq: number,
356
+ signal?: AbortSignal,
357
+ ): Promise<StoredSuffix | undefined> {
358
+ return promiseFromSync(() => {
359
+ signal?.throwIfAborted()
360
+ const snapshot = this.storage.transactionSync(() => {
361
+ const row = this.rowFor(id)
362
+ if (row === undefined) return undefined
363
+ return { row, eventRows: this.eventRows(id, fromSeq) }
364
+ })
365
+ signal?.throwIfAborted()
366
+ if (snapshot === undefined) return undefined
367
+ return {
368
+ meta: rowToHeader(snapshot.row),
369
+ events: scanRows(snapshot.eventRows, fromSeq).preserved,
370
+ }
371
+ })
372
+ }
373
+
374
+ private loadStoredPage(
375
+ id: SessionId,
376
+ fromSeq: number,
377
+ limit: number,
378
+ maxStoredBytes: number,
379
+ signal?: AbortSignal,
380
+ toSeqExclusive?: number,
381
+ ): Promise<EdgeEventPage | undefined> {
382
+ return promiseFromSync(() => {
383
+ signal?.throwIfAborted()
384
+ const page = this.storage.transactionSync(() => {
385
+ const row = this.rowFor(id)
386
+ if (row === undefined) return undefined
387
+ const sizes = this.eventPageSizes(id, fromSeq, limit + 1, toSeqExclusive)
388
+ const lastTurnEnd = this.lastCommittedSequence(id)
389
+ const rows: EventRow[] = []
390
+ let storedBytes = 0
391
+ let hasMore = false
392
+ for (const [index, size] of sizes.entries()) {
393
+ const expectedSeq = fromSeq + index
394
+ if (size.seq !== expectedSeq) {
395
+ if (size.seq <= lastTurnEnd) {
396
+ throw new Error(
397
+ `corrupt session log: seq gap in committed region (expected ${expectedSeq}, got ${size.seq})`,
398
+ )
399
+ }
400
+ break
401
+ }
402
+ if (size.parseable !== 1) {
403
+ if (size.seq <= lastTurnEnd) {
404
+ throw new Error(`corrupt session log: unparsable committed event at seq ${size.seq}`)
405
+ }
406
+ break
407
+ }
408
+ if (index >= limit || size.stored_bytes > maxStoredBytes - storedBytes) {
409
+ hasMore = true
410
+ break
411
+ }
412
+ const eventRow = this.eventRow(id, size.seq)
413
+ if (eventRow === undefined) {
414
+ throw new Error(`stored session ${id} event ${size.seq} disappeared during replay`)
415
+ }
416
+ rows.push(eventRow)
417
+ storedBytes += size.stored_bytes
418
+ }
419
+ const events = scanRows(rows, fromSeq, lastTurnEnd).preserved
420
+ return {
421
+ meta: rowToHeader(row),
422
+ events,
423
+ hasMore,
424
+ }
425
+ })
426
+ signal?.throwIfAborted()
427
+ return page
428
+ })
429
+ }
430
+
431
+ appendBatch(
432
+ meta: SessionHeader,
433
+ events: readonly SessionEvent[],
434
+ isMaterialized: boolean,
435
+ ): Promise<void> {
436
+ return promiseFromSync(() => {
437
+ this.storage.transactionSync(() => {
438
+ if (!isMaterialized) this.writeRow(meta)
439
+ for (const event of events) this.insertEvent(meta.id, event)
440
+ const updated = this.storage.sql.exec(
441
+ 'UPDATE dsh_sessions SET revision = revision + 1 WHERE id = ?',
442
+ meta.id,
443
+ )
444
+ if (updated.rowsWritten !== 1) throw new Error(`session ${meta.id} is not materialized`)
445
+ this.storage.sql.exec('DELETE FROM dsh_edge_blank_sessions WHERE id = ?', meta.id)
446
+ })
447
+ })
448
+ }
449
+
450
+ commitRepair(
451
+ meta: SessionHeader,
452
+ tornMarker: number | undefined,
453
+ closers: readonly SessionEvent[],
454
+ ): Promise<void> {
455
+ return promiseFromSync(() => {
456
+ this.storage.transactionSync(() => {
457
+ if (tornMarker !== undefined) {
458
+ this.storage.sql.exec(
459
+ 'DELETE FROM dsh_session_events WHERE session_id = ? AND seq >= ?',
460
+ meta.id,
461
+ tornMarker,
462
+ )
463
+ }
464
+ for (const event of closers) this.insertEvent(meta.id, event)
465
+ if (tornMarker !== undefined || closers.length > 0) {
466
+ this.storage.sql.exec(
467
+ 'UPDATE dsh_sessions SET revision = revision + 1 WHERE id = ?',
468
+ meta.id,
469
+ )
470
+ }
471
+ })
472
+ })
473
+ }
474
+
475
+ list(signal?: AbortSignal): Promise<SessionHeader[]> {
476
+ return promiseFromSync(() => {
477
+ signal?.throwIfAborted()
478
+ const rows = this.storage.sql.exec<HeaderRow>(
479
+ `SELECT id, version, created_at, cwd, parent_session, seed_length, origin,
480
+ delegation_depth, agent_preset, incarnation, revision
481
+ FROM dsh_sessions`,
482
+ ).toArray()
483
+ signal?.throwIfAborted()
484
+ return rows.map(rowToHeader)
485
+ })
486
+ }
487
+
488
+ listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]> {
489
+ return promiseFromSync(() => {
490
+ signal?.throwIfAborted()
491
+ const rows = this.storage.sql.exec<HeaderRow>(
492
+ `SELECT id, version, created_at, cwd, parent_session, seed_length, origin,
493
+ delegation_depth, agent_preset, incarnation, revision
494
+ FROM dsh_sessions`,
495
+ ).toArray()
496
+ signal?.throwIfAborted()
497
+ return rows.map(row => ({
498
+ header: rowToHeader(row),
499
+ revision: revisionOf(this.storeIdentity, row),
500
+ }))
501
+ })
502
+ }
503
+
504
+ /** Test one materialized session identity without listing stored headers. */
505
+ hasSession(id: SessionId): boolean {
506
+ return this.rowFor(id) !== undefined
507
+ }
508
+
509
+ /** Read and format-check one canonical header without touching event rows. */
510
+ readSessionHeader(id: SessionId): SessionHeader | undefined {
511
+ const row = this.rowFor(id)
512
+ if (row === undefined) return undefined
513
+ const header = rowToHeader(row)
514
+ if (header.version !== SESSION_FORMAT_VERSION) {
515
+ throw new SessionFormatUnsupportedError(
516
+ sessionFormatVersionRefusal(header.id, header.version),
517
+ )
518
+ }
519
+ return header
520
+ }
521
+
522
+ /** Retain the process-local blank identity across Durable Object hibernation. */
523
+ retainBlankSession(meta: SessionHeader): Promise<void> {
524
+ return promiseFromSync(() => {
525
+ this.storage.sql.exec(
526
+ `INSERT INTO dsh_edge_blank_sessions
527
+ (id, version, created_at, cwd, parent_session, seed_length, origin,
528
+ delegation_depth, agent_preset)
529
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
530
+ meta.id,
531
+ meta.version,
532
+ meta.createdAt,
533
+ meta.cwd ?? null,
534
+ meta.parentSession ?? null,
535
+ meta.seedLength ?? null,
536
+ meta.origin ?? null,
537
+ meta.delegationDepth ?? null,
538
+ meta.agentPreset ?? null,
539
+ )
540
+ })
541
+ }
542
+
543
+ /** Read one retained blank header without treating it as a canonical log. */
544
+ readBlankSession(id: SessionId): SessionHeader | undefined {
545
+ const row = this.storage.sql.exec<BlankSessionRow>(
546
+ `SELECT id, version, created_at, cwd, parent_session, seed_length, origin,
547
+ delegation_depth, agent_preset
548
+ FROM dsh_edge_blank_sessions WHERE id = ?`,
549
+ id,
550
+ ).toArray()[0]
551
+ return row === undefined ? undefined : blankRowToHeader(row)
552
+ }
553
+
554
+ /** Read every retained blank header for the upstream session-list baseline. */
555
+ readAllBlankSessions(): SessionHeader[] {
556
+ return this.storage.sql.exec<BlankSessionRow>(
557
+ `SELECT id, version, created_at, cwd, parent_session, seed_length, origin,
558
+ delegation_depth, agent_preset
559
+ FROM dsh_edge_blank_sessions ORDER BY created_at DESC, id ASC`,
560
+ ).toArray().map(blankRowToHeader)
561
+ }
562
+
563
+ /** Read only the newest retained blank headers needed by bounded consumers. */
564
+ readRecentBlankSessions(limit: number): SessionHeader[] {
565
+ return this.storage.sql.exec<BlankSessionRow>(
566
+ `SELECT id, version, created_at, cwd, parent_session, seed_length, origin,
567
+ delegation_depth, agent_preset
568
+ FROM dsh_edge_blank_sessions ORDER BY created_at DESC, id ASC LIMIT ?`,
569
+ limit,
570
+ ).toArray().map(blankRowToHeader)
571
+ }
572
+
573
+ /** Promote a retained blank into the canonical empty-log schema before cold resume. */
574
+ materializeBlankSession(id: SessionId): Promise<boolean> {
575
+ return promiseFromSync(() => this.storage.transactionSync(() => {
576
+ const blank = this.readBlankSession(id)
577
+ if (blank === undefined) return false
578
+ if (this.rowFor(id) === undefined) this.writeRow(blank)
579
+ this.storage.sql.exec('DELETE FROM dsh_edge_blank_sessions WHERE id = ?', id)
580
+ return true
581
+ }))
582
+ }
583
+
584
+ /** Read one canonical header/title summary without materializing its event log. */
585
+ readSessionSummary(id: SessionId): EdgeStoredSessionSummary | undefined {
586
+ const row = this.storage.sql.exec<SessionSummaryRow>(
587
+ `${SESSION_SUMMARY_SELECT} WHERE s.id = ?`,
588
+ id,
589
+ ).toArray()[0]
590
+ return row === undefined ? undefined : rowToStoredSessionSummary(row)
591
+ }
592
+
593
+ /** Read a bounded metadata/title page without materializing session logs. */
594
+ readSessionSummaryPage(
595
+ after: SessionId | undefined,
596
+ limit: number,
597
+ ): EdgeStoredSessionSummaryPage | undefined {
598
+ return this.storage.transactionSync(() => {
599
+ const cursor = after === undefined ? undefined : this.rowFor(after)
600
+ if (after !== undefined && cursor === undefined) return undefined
601
+ const bindings: SqlStorageValue[] = []
602
+ const where = cursor === undefined
603
+ ? ''
604
+ : 'WHERE (s.created_at < ? OR (s.created_at = ? AND s.id > ?))'
605
+ if (cursor !== undefined) bindings.push(cursor.created_at, cursor.created_at, cursor.id)
606
+ bindings.push(limit + 1)
607
+ const rows = this.storage.sql.exec<SessionSummaryRow>(
608
+ `${SESSION_SUMMARY_SELECT} ${where}
609
+ ORDER BY s.created_at DESC, s.id ASC LIMIT ?`,
610
+ ...bindings,
611
+ ).toArray()
612
+ return {
613
+ sessions: rows.slice(0, limit).map(rowToStoredSessionSummary),
614
+ hasMore: rows.length > limit,
615
+ }
616
+ })
617
+ }
618
+
619
+ /** Read the upstream session-list baseline ordered by human activity. */
620
+ readAllSessionSummaries(): EdgeStoredSessionSummary[] {
621
+ return this.storage.sql.exec<SessionSummaryRow>(
622
+ `${SESSION_SUMMARY_SELECT}
623
+ ORDER BY coalesce(last_prompt_at, s.created_at) DESC, s.id ASC`,
624
+ ).toArray().map(rowToStoredSessionSummary)
625
+ }
626
+
627
+ /** Read only the newest canonical summaries needed by bounded consumers. */
628
+ readRecentSessionSummaries(limit: number): EdgeStoredSessionSummary[] {
629
+ return this.storage.sql.exec<SessionSummaryRow>(
630
+ `${SESSION_SUMMARY_SELECT}
631
+ ORDER BY coalesce(last_prompt_at, s.created_at) DESC, s.id ASC LIMIT ?`,
632
+ limit,
633
+ ).toArray().map(rowToStoredSessionSummary)
634
+ }
635
+
636
+ private initialize(): string {
637
+ this.storage.sql.exec('PRAGMA foreign_keys = ON')
638
+ return this.storage.transactionSync(() => {
639
+ this.storage.sql.exec(`CREATE TABLE IF NOT EXISTS dsh_session_persistence_state (
640
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
641
+ schema_version INTEGER NOT NULL,
642
+ store_id TEXT NOT NULL
643
+ ) STRICT`)
644
+ const state = this.storage.sql.exec<StateRow>(
645
+ 'SELECT schema_version, store_id FROM dsh_session_persistence_state WHERE singleton = 1',
646
+ ).toArray()[0]
647
+ if (state !== undefined && state.schema_version !== DO_SESSION_SCHEMA_VERSION) {
648
+ throw new Error(
649
+ `dsh-edge session schema ${state.schema_version} is incompatible with ${DO_SESSION_SCHEMA_VERSION}`,
650
+ )
651
+ }
652
+ if (state !== undefined && state.store_id.length === 0) {
653
+ throw new Error('dsh-edge session persistence has no valid store identity')
654
+ }
655
+ if (state === undefined) this.removePreCanonicalPrototype()
656
+ const storeId = state?.store_id ?? crypto.randomUUID()
657
+ this.storage.sql.exec(
658
+ `INSERT OR IGNORE INTO dsh_session_persistence_state
659
+ (singleton, schema_version, store_id) VALUES (1, ?, ?)`,
660
+ DO_SESSION_SCHEMA_VERSION,
661
+ storeId,
662
+ )
663
+ this.storage.sql.exec(`CREATE TABLE IF NOT EXISTS dsh_sessions (
664
+ id TEXT PRIMARY KEY,
665
+ version INTEGER NOT NULL,
666
+ created_at INTEGER NOT NULL,
667
+ cwd TEXT,
668
+ parent_session TEXT,
669
+ seed_length INTEGER,
670
+ origin TEXT,
671
+ delegation_depth INTEGER,
672
+ agent_preset TEXT,
673
+ incarnation TEXT NOT NULL,
674
+ revision INTEGER NOT NULL
675
+ ) STRICT`)
676
+ this.storage.sql.exec(`CREATE TABLE IF NOT EXISTS dsh_session_events (
677
+ session_id TEXT NOT NULL REFERENCES dsh_sessions(id) ON DELETE CASCADE,
678
+ seq INTEGER NOT NULL,
679
+ type TEXT NOT NULL,
680
+ time INTEGER NOT NULL,
681
+ data TEXT NOT NULL,
682
+ source_event_seqs TEXT,
683
+ surface_op TEXT,
684
+ ignorable INTEGER,
685
+ PRIMARY KEY (session_id, seq)
686
+ ) STRICT`)
687
+ this.storage.sql.exec(`CREATE TABLE IF NOT EXISTS dsh_edge_blank_sessions (
688
+ id TEXT PRIMARY KEY,
689
+ version INTEGER NOT NULL,
690
+ created_at INTEGER NOT NULL,
691
+ cwd TEXT,
692
+ parent_session TEXT,
693
+ seed_length INTEGER,
694
+ origin TEXT,
695
+ delegation_depth INTEGER,
696
+ agent_preset TEXT
697
+ ) STRICT`)
698
+ return `durable-object:store:${storeId}`
699
+ })
700
+ }
701
+
702
+ private removePreCanonicalPrototype(): void {
703
+ this.storage.sql.exec('DROP TABLE IF EXISTS dsh_session_events')
704
+ this.storage.sql.exec('DROP TABLE IF EXISTS dsh_sessions')
705
+ this.storage.sql.exec('DROP TABLE IF EXISTS dsh_edge_events')
706
+ this.storage.sql.exec('DROP TABLE IF EXISTS dsh_edge_messages')
707
+ this.storage.sql.exec('DROP TABLE IF EXISTS dsh_edge_turns')
708
+ this.storage.sql.exec('DROP TABLE IF EXISTS dsh_edge_session_metadata')
709
+ this.storage.sql.exec('DROP TABLE IF EXISTS dsh_edge_sessions')
710
+ }
711
+
712
+ private rowFor(id: SessionId): HeaderRow | undefined {
713
+ return this.storage.sql.exec<HeaderRow>(
714
+ `SELECT id, version, created_at, cwd, parent_session, seed_length, origin,
715
+ delegation_depth, agent_preset, incarnation, revision
716
+ FROM dsh_sessions WHERE id = ?`,
717
+ id,
718
+ ).toArray()[0]
719
+ }
720
+
721
+ private eventRows(id: SessionId, fromSeq: number): EventRow[] {
722
+ return this.storage.sql.exec<EventRow>(
723
+ `SELECT seq, type, time, data, source_event_seqs, surface_op, ignorable
724
+ FROM dsh_session_events
725
+ WHERE session_id = ? AND seq >= ? ORDER BY seq`,
726
+ id,
727
+ fromSeq,
728
+ ).toArray()
729
+ }
730
+
731
+ private eventPageSizes(
732
+ id: SessionId,
733
+ fromSeq: number,
734
+ limit: number,
735
+ toSeqExclusive?: number,
736
+ ): EventSizeRow[] {
737
+ const upperClause = toSeqExclusive === undefined ? '' : ' AND seq < ?'
738
+ const bindings: SqlStorageValue[] = [id, fromSeq]
739
+ if (toSeqExclusive !== undefined) bindings.push(toSeqExclusive)
740
+ bindings.push(limit)
741
+ return this.storage.sql.exec<EventSizeRow>(
742
+ `SELECT seq,
743
+ length(CAST(type AS BLOB)) + length(CAST(data AS BLOB))
744
+ + coalesce(length(CAST(source_event_seqs AS BLOB)), 0)
745
+ + coalesce(length(CAST(surface_op AS BLOB)), 0) + 32 AS stored_bytes,
746
+ CASE WHEN json_valid(data)
747
+ AND (source_event_seqs IS NULL OR json_valid(source_event_seqs))
748
+ AND (surface_op IS NULL OR json_valid(surface_op))
749
+ THEN 1 ELSE 0 END AS parseable
750
+ FROM dsh_session_events
751
+ WHERE session_id = ? AND seq >= ?${upperClause} ORDER BY seq LIMIT ?`,
752
+ ...bindings,
753
+ ).toArray()
754
+ }
755
+
756
+ private eventRow(id: SessionId, seq: number): EventRow | undefined {
757
+ return this.storage.sql.exec<EventRow>(
758
+ `SELECT seq, type, time, data, source_event_seqs, surface_op, ignorable
759
+ FROM dsh_session_events WHERE session_id = ? AND seq = ?`,
760
+ id,
761
+ seq,
762
+ ).toArray()[0]
763
+ }
764
+
765
+ /** Find the newest parseable commit marker without materializing marker rows. */
766
+ private lastCommittedSequence(id: SessionId): number {
767
+ return this.storage.sql.exec<LastSequenceRow>(
768
+ `SELECT MAX(seq) AS seq FROM dsh_session_events
769
+ WHERE session_id = ? AND type = 'turn/end'
770
+ AND json_valid(data)
771
+ AND (source_event_seqs IS NULL OR json_valid(source_event_seqs))
772
+ AND (surface_op IS NULL OR json_valid(surface_op))`,
773
+ id,
774
+ ).toArray()[0]?.seq ?? -1
775
+ }
776
+
777
+ private writeRow(meta: SessionHeader): void {
778
+ this.storage.sql.exec(
779
+ `INSERT INTO dsh_sessions
780
+ (id, version, created_at, cwd, parent_session, seed_length, origin,
781
+ delegation_depth, agent_preset, incarnation, revision)
782
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
783
+ ON CONFLICT(id) DO UPDATE SET
784
+ version = excluded.version,
785
+ created_at = excluded.created_at,
786
+ cwd = excluded.cwd,
787
+ parent_session = excluded.parent_session,
788
+ seed_length = excluded.seed_length,
789
+ origin = excluded.origin,
790
+ delegation_depth = excluded.delegation_depth,
791
+ agent_preset = excluded.agent_preset`,
792
+ meta.id,
793
+ meta.version,
794
+ meta.createdAt,
795
+ meta.cwd ?? null,
796
+ meta.parentSession ?? null,
797
+ meta.seedLength ?? null,
798
+ meta.origin ?? null,
799
+ meta.delegationDepth ?? null,
800
+ meta.agentPreset ?? null,
801
+ crypto.randomUUID(),
802
+ )
803
+ }
804
+
805
+ private insertEvent(id: SessionId, event: SessionEvent): void {
806
+ const envelope = event as SessionEvent & {
807
+ sourceEventSeqs?: number[]
808
+ surfaceOp?: SurfaceOp
809
+ }
810
+ this.storage.sql.exec(
811
+ `INSERT INTO dsh_session_events
812
+ (session_id, seq, type, time, data, source_event_seqs, surface_op, ignorable)
813
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
814
+ id,
815
+ event.seq,
816
+ event.type,
817
+ event.time,
818
+ JSON.stringify(event.data),
819
+ envelope.sourceEventSeqs === undefined ? null : JSON.stringify(envelope.sourceEventSeqs),
820
+ envelope.surfaceOp === undefined ? null : JSON.stringify(envelope.surfaceOp),
821
+ event.ignorable === true ? 1 : null,
822
+ )
823
+ }
824
+ }
825
+
826
+ function revisionOf(storeIdentity: string, row: HeaderRow): PersistenceRevision {
827
+ return SessionPersistenceRevision(
828
+ `${storeIdentity}:incarnation:${row.incarnation}:revision:${row.revision}`,
829
+ )
830
+ }
831
+
832
+ /** Convert the DO's synchronous SQL surface into the backend's Promise contract. */
833
+ function promiseFromSync<T>(operation: () => T): Promise<T> {
834
+ try {
835
+ return Promise.resolve(operation())
836
+ } catch (error) {
837
+ // Preserve the exact synchronous rejection value, including AbortSignal.reason.
838
+ const deferred = Promise.withResolvers<T>()
839
+ deferred.reject(error)
840
+ return deferred.promise
841
+ }
842
+ }
843
+
844
+ function rowToHeader(row: HeaderRow): SessionHeader {
845
+ if (!Number.isSafeInteger(row.created_at) || row.created_at < 0) {
846
+ throw new Error('stored session createdAt must be a non-negative safe integer')
847
+ }
848
+ return {
849
+ id: row.id as SessionId,
850
+ version: row.version,
851
+ createdAt: row.created_at,
852
+ ...row.cwd === null ? {} : { cwd: row.cwd },
853
+ ...row.parent_session === null ? {} : { parentSession: row.parent_session as SessionId },
854
+ ...row.seed_length === null ? {} : { seedLength: row.seed_length },
855
+ ...row.origin === null ? {} : { origin: row.origin as 'subagent' },
856
+ ...row.delegation_depth === null ? {} : { delegationDepth: row.delegation_depth },
857
+ ...row.agent_preset === null ? {} : { agentPreset: row.agent_preset },
858
+ }
859
+ }
860
+
861
+ function blankRowToHeader(row: BlankSessionRow): SessionHeader {
862
+ const header = rowToHeader({ ...row, incarnation: '', revision: 0 })
863
+ if (header.version !== SESSION_FORMAT_VERSION) {
864
+ throw new SessionFormatUnsupportedError(
865
+ sessionFormatVersionRefusal(header.id, header.version),
866
+ )
867
+ }
868
+ return header
869
+ }
870
+
871
+ function rowToEvent(row: EventRow): SessionEvent {
872
+ return {
873
+ type: row.type as SessionEvent['type'],
874
+ seq: row.seq,
875
+ time: row.time,
876
+ data: JSON.parse(row.data) as SessionEvent['data'],
877
+ ...row.source_event_seqs === null
878
+ ? {}
879
+ : { sourceEventSeqs: JSON.parse(row.source_event_seqs) as number[] },
880
+ ...row.surface_op === null
881
+ ? {}
882
+ : { surfaceOp: JSON.parse(row.surface_op) as SurfaceOp },
883
+ ...row.ignorable === 1 ? { ignorable: true as const } : {},
884
+ } as SessionEvent
885
+ }
886
+
887
+ function historyGroupStart(row: HistoryBoundaryRow): number {
888
+ if (row.source_event_seqs === null) return row.seq
889
+ let sources: unknown
890
+ try {
891
+ sources = JSON.parse(row.source_event_seqs)
892
+ } catch {
893
+ return row.seq
894
+ }
895
+ if (!Array.isArray(sources) || !sources.every(value => Number.isSafeInteger(value))) {
896
+ return row.seq
897
+ }
898
+ return (sources as number[]).reduce((minimum, value) => Math.min(minimum, value), row.seq)
899
+ }
900
+
901
+ function rowToStoredSessionSummary(row: SessionSummaryRow): EdgeStoredSessionSummary {
902
+ const meta = rowToHeader(row)
903
+ if (meta.version !== SESSION_FORMAT_VERSION) {
904
+ throw new SessionFormatUnsupportedError(
905
+ sessionFormatVersionRefusal(meta.id, meta.version),
906
+ )
907
+ }
908
+ const updatedAt = row.updated_at ?? meta.createdAt
909
+ if (!Number.isSafeInteger(updatedAt)) {
910
+ throw new Error(`stored session ${meta.id} has an invalid latest event time`)
911
+ }
912
+ if (row.last_prompt_at !== null && !Number.isSafeInteger(row.last_prompt_at)) {
913
+ throw new Error(`stored session ${meta.id} has an invalid latest prompt time`)
914
+ }
915
+ const lastSeq = row.last_seq ?? -1
916
+ if (!Number.isSafeInteger(lastSeq) || lastSeq < -1) {
917
+ throw new Error(`stored session ${meta.id} has an invalid latest event sequence`)
918
+ }
919
+ if (row.blank !== 0 && row.blank !== 1) {
920
+ throw new Error(`stored session ${meta.id} has an invalid blank-session marker`)
921
+ }
922
+ const base = {
923
+ meta,
924
+ updatedAt,
925
+ lastPromptAt: row.last_prompt_at,
926
+ lastSeq,
927
+ blank: row.blank === 1,
928
+ }
929
+ const titleFields = [row.title_seq, row.title_time, row.title_data]
930
+ if (titleFields.every(value => value === null)) return base
931
+ if (row.title_seq === null || row.title_time === null || row.title_data === null) {
932
+ throw new Error(`stored session ${meta.id} has an incomplete title event`)
933
+ }
934
+ return {
935
+ ...base,
936
+ titleEvent: storedTitleEvent(meta.id, row.title_seq, row.title_time, row.title_data),
937
+ }
938
+ }
939
+
940
+ /** Validate the upstream session-title shape at the abbreviated durable read boundary. */
941
+ function storedTitleEvent(
942
+ id: SessionId,
943
+ seq: number,
944
+ time: number,
945
+ rawData: string,
946
+ ): SessionEvent<'session/title'> {
947
+ if (!Number.isSafeInteger(seq) || seq < 0 || !Number.isSafeInteger(time)) {
948
+ throw new Error(`stored session ${id} has an invalid session/title envelope`)
949
+ }
950
+ let data: unknown
951
+ try {
952
+ data = JSON.parse(rawData)
953
+ } catch {
954
+ throw new Error(`stored session ${id} has unparsable session/title data`)
955
+ }
956
+ const validated = sessionTitleEventData(data)
957
+ if (validated === undefined) {
958
+ throw new Error(`stored session ${id} has invalid session/title data`)
959
+ }
960
+ return { type: 'session/title', seq, time, data: validated }
961
+ }
962
+
963
+ function sessionTitleEventData(value: unknown): SessionTitleEventData | undefined {
964
+ if (!hasOnlyKeys(value, ['title', 'messageSeqs', 'source'])) return undefined
965
+ if (typeof value.title !== 'string' || value.title.length === 0) return undefined
966
+ if (!isSessionTitleMessageSeqs(value.messageSeqs)) return undefined
967
+ const source = sessionTitleSource(value.source)
968
+ if (source === undefined) return undefined
969
+ return { title: value.title, messageSeqs: [...value.messageSeqs], source }
970
+ }
971
+
972
+ function sessionTitleSource(value: unknown): SessionTitleSource | undefined {
973
+ if (!hasOnlyKeys(value, ['kind'], ['provider', 'model'])) return undefined
974
+ switch (value.kind) {
975
+ case 'fallback':
976
+ case 'user':
977
+ return Object.keys(value).length === 1 ? { kind: value.kind } : undefined
978
+ case 'provider': {
979
+ if (typeof value.provider !== 'string' || value.provider.length === 0) return undefined
980
+ if (value.model === undefined) {
981
+ return { kind: 'provider', provider: SessionTitleProviderId(value.provider) }
982
+ }
983
+ if (!hasOnlyKeys(value.model, ['provider', 'model'])
984
+ || typeof value.model.provider !== 'string'
985
+ || value.model.provider.length === 0
986
+ || typeof value.model.model !== 'string'
987
+ || value.model.model.length === 0) return undefined
988
+ return {
989
+ kind: 'provider',
990
+ provider: SessionTitleProviderId(value.provider),
991
+ model: { provider: value.model.provider, model: value.model.model },
992
+ }
993
+ }
994
+ default:
995
+ return undefined
996
+ }
997
+ }
998
+
999
+ function isSessionTitleMessageSeqs(value: unknown): value is number[] {
1000
+ return Array.isArray(value)
1001
+ && value.every((seq: unknown) => typeof seq === 'number'
1002
+ && Number.isSafeInteger(seq)
1003
+ && seq >= 0)
1004
+ }
1005
+
1006
+ function hasOnlyKeys(
1007
+ value: unknown,
1008
+ required: readonly string[],
1009
+ optional: readonly string[] = [],
1010
+ ): value is Record<string, unknown> {
1011
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false
1012
+ const keys = Object.keys(value)
1013
+ return required.every(key => Object.hasOwn(value, key))
1014
+ && keys.every(key => required.includes(key) || optional.includes(key))
1015
+ }
1016
+
1017
+ /** Preserve the valid contiguous prefix and identify a never-committed torn tail. */
1018
+ function scanRows(
1019
+ rows: readonly EventRow[],
1020
+ base = 0,
1021
+ committedThrough?: number,
1022
+ ): { preserved: SessionEvent[]; tornFrom?: number } {
1023
+ const parsed = rows.map((row) => {
1024
+ try {
1025
+ return { ok: true as const, event: rowToEvent(row) }
1026
+ } catch {
1027
+ return { ok: false as const }
1028
+ }
1029
+ })
1030
+ let lastTurnEnd = committedThrough
1031
+ if (lastTurnEnd === undefined) {
1032
+ lastTurnEnd = -1
1033
+ for (let index = parsed.length - 1; index >= 0; index -= 1) {
1034
+ if (parsed[index]?.ok === true && rows[index]?.type === 'turn/end') {
1035
+ lastTurnEnd = rows[index]?.seq ?? -1
1036
+ break
1037
+ }
1038
+ }
1039
+ }
1040
+ const preserved: SessionEvent[] = []
1041
+ for (let index = 0; index < rows.length; index += 1) {
1042
+ const candidate = parsed[index]
1043
+ if (candidate?.ok !== true) {
1044
+ if ((rows[index]?.seq ?? base + index) <= lastTurnEnd) {
1045
+ throw new Error(`corrupt session log: unparsable committed event at seq ${rows[index]?.seq}`)
1046
+ }
1047
+ break
1048
+ }
1049
+ if (candidate.event.seq !== base + index) {
1050
+ if (candidate.event.seq <= lastTurnEnd) {
1051
+ throw new Error(
1052
+ `corrupt session log: seq gap in committed region (expected ${base + index}, got ${candidate.event.seq})`,
1053
+ )
1054
+ }
1055
+ break
1056
+ }
1057
+ preserved.push(candidate.event)
1058
+ }
1059
+ return preserved.length < rows.length
1060
+ ? { preserved, tornFrom: base + preserved.length }
1061
+ : { preserved }
1062
+ }
1063
+
1064
+ export default DurableObjectSessionPersistence