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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +183 -0
- package/README.zh.md +183 -0
- package/THIRD_PARTY_NOTICES.md +199 -0
- package/dist/_headers +3 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/index-C-1AiF3k.js +112 -0
- package/dist/assets/index-CSGf6Qzd.css +1 -0
- package/dist/assets/langs/c-BIGW1oBm.js +2 -0
- package/dist/assets/langs/cpp-DIPi6g--.js +2 -0
- package/dist/assets/langs/csharp-DSvCPggb.js +2 -0
- package/dist/assets/langs/css-CLj8gQPS.js +2 -0
- package/dist/assets/langs/go-C27-OAKa.js +2 -0
- package/dist/assets/langs/html-CfGypltT.js +2 -0
- package/dist/assets/langs/ini-BEwlwnbL.js +2 -0
- package/dist/assets/langs/java-CylS5w8V.js +2 -0
- package/dist/assets/langs/kotlin-BdnUsdx6.js +2 -0
- package/dist/assets/langs/less-B1dDrJ26.js +2 -0
- package/dist/assets/langs/lua-BaeVxFsk.js +2 -0
- package/dist/assets/langs/markdown-Cvjx9yec.js +2 -0
- package/dist/assets/langs/mdx-Cmh6b_Ma.js +2 -0
- package/dist/assets/langs/php-D-uCvoST.js +2 -0
- package/dist/assets/langs/python-B6aJPvgy.js +2 -0
- package/dist/assets/langs/ruby-DEItuUFs.js +2 -0
- package/dist/assets/langs/rust-B1yitclQ.js +2 -0
- package/dist/assets/langs/scss-D5BDwBP9.js +2 -0
- package/dist/assets/langs/sql-CRqJ_cUM.js +2 -0
- package/dist/assets/langs/swift-D82vCrfD.js +2 -0
- package/dist/assets/langs/toml-vGWfd6FD.js +2 -0
- package/dist/assets/langs/xml-sdJ4AIDG.js +2 -0
- package/dist/assets/langs/yaml-Buea-lGh.js +2 -0
- package/dist/assets/vendor-Cjbwl5VI.js +411 -0
- package/dist/assets/vendor-CjyC-hUb.css +1 -0
- package/dist/favicon.svg +8 -0
- package/dist/index.html +35 -0
- package/dist/manifest.webmanifest +16 -0
- package/dist/plugins/@deepseek-ai/dsh-api-gateway/client.js +420 -0
- package/dist/plugins/@deepseek-ai/dsh-api-remotes/client.js +5937 -0
- package/dist/plugins/@deepseek-ai/dsh-client-connection/client.js +10207 -0
- package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +1231 -0
- package/dist/plugins/@deepseek-ai/dsh-client-modules/client.js +259 -0
- package/dist/plugins/@deepseek-ai/dsh-client-runtime/client.js +10539 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +1725 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +1116 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +9832 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +373 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-edge/client.js +549 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +859 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +280 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +456 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +804 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +461 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings/client.js +254 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +609 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +292 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +327 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +710 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +1312 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +1630 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +7370 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +700 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +474 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +2426 -0
- package/dist/plugins/@deepseek-ai/dsh-typert-registry/client.js +1372 -0
- package/package.json +97 -0
- package/scripts/assemble-web.mjs +230 -0
- package/scripts/bundle-size.d.mts +2 -0
- package/scripts/bundle-size.mjs +26 -0
- package/scripts/bundle.mjs +53 -0
- package/scripts/cli.d.mts +39 -0
- package/scripts/cli.mjs +496 -0
- package/scripts/install.d.mts +149 -0
- package/scripts/install.mjs +989 -0
- package/scripts/legal-files.mjs +24 -0
- package/scripts/verify-packed.mjs +32 -0
- package/scripts/wrangler-config.d.mts +18 -0
- package/scripts/wrangler-config.mjs +84 -0
- package/src/agent.ts +119 -0
- package/src/auth.ts +352 -0
- package/src/deepseek.ts +231 -0
- package/src/deployment.ts +147 -0
- package/src/direct-shell-core-empty.ts +10 -0
- package/src/direct-shell-protocol.ts +5 -0
- package/src/direct-shell.ts +341 -0
- package/src/do-session-persistence.ts +1064 -0
- package/src/edge-api.ts +796 -0
- package/src/edge-credentials.ts +56 -0
- package/src/edge-remotes.ts +41 -0
- package/src/edge-workspace-store.ts +339 -0
- package/src/http.ts +212 -0
- package/src/index.ts +196 -0
- package/src/instance.ts +1007 -0
- package/src/isolated-direct-shell-unavailable.ts +11 -0
- package/src/protocol.ts +39 -0
- package/src/release.ts +9 -0
- package/src/session-store.ts +1110 -0
- package/src/sse.ts +86 -0
- package/src/web-search.ts +38 -0
- package/src/workspace.ts +270 -0
- 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
|