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,1110 @@
1
+ /** Canonical DSH sessions backed by the upstream persistence service. */
2
+
3
+ import { Context } from '@deepseek-ai/cordis'
4
+ import AgentRegistry, { type Agent, type AgentHandle } from '@deepseek-ai/dsh-agent'
5
+ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
6
+ import { credentialRef, type CredentialInfo } from '@deepseek-ai/dsh-credentials'
7
+ import LlmRuntime, { createUserMessage } from '@deepseek-ai/dsh-llm'
8
+ import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
9
+ import {
10
+ SESSION_SEARCH_RESULT_LIMIT,
11
+ SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS,
12
+ type QueuedInboxItem,
13
+ type RpcId,
14
+ type SessionSearchItem,
15
+ } from '@deepseek-ai/dsh-host-apiproxy/api'
16
+ import SessionStore, {
17
+ SessionId,
18
+ isAppendSurfaceEvent,
19
+ type SessionEvent,
20
+ type SessionEventMap,
21
+ type SessionHeader,
22
+ type UserMessage,
23
+ } from '@deepseek-ai/dsh-session'
24
+ import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
25
+ import { buildSessionEventSearchDocuments } from '@deepseek-ai/dsh-session-query'
26
+ import {
27
+ foldSessionTitle,
28
+ normalizeSessionTitle,
29
+ } from '@deepseek-ai/dsh-session-title'
30
+ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
31
+ import ToolRuntime from '@deepseek-ai/dsh-tools'
32
+ import {
33
+ EDGE_SYSTEM_PROMPT,
34
+ EdgeShellBindings,
35
+ createEdgeBashTool,
36
+ type EdgeShell,
37
+ } from './agent.ts'
38
+ import { EdgeDeepSeekAdapter, type EdgeReasoningEffort } from './deepseek.ts'
39
+ import EdgeCredentialProvider, { EDGE_DEEPSEEK_API_KEY_REF } from './edge-credentials.ts'
40
+ import DurableObjectSessionPersistence, {
41
+ EDGE_HISTORY_PAGE_LIMITS,
42
+ type EdgeEventPage,
43
+ } from './do-session-persistence.ts'
44
+ import type { CreateEdgeSessionInput, EdgeSession } from './protocol.ts'
45
+ import { installEdgeWebSearch } from './web-search.ts'
46
+
47
+ const MAX_TITLE_BYTES = 640
48
+ const MAX_FORK_EVENTS = 8_192
49
+
50
+ interface EdgeSessionStoreConfig {
51
+ readDeepSeekApiKey(): string | undefined
52
+ searchBaseURL?: string
53
+ }
54
+ const MAX_FORK_STORED_BYTES = 8 * 1_024 * 1_024
55
+ const MAX_SEARCH_SESSIONS = 32
56
+ const MAX_SEARCH_EVENTS_PER_SESSION = 512
57
+ const MAX_SEARCH_STORED_BYTES_PER_SESSION = 256 * 1_024
58
+ const EDGE_PROVIDER = 'deepseek-official'
59
+ const DEFAULT_EDGE_MODEL = 'deepseek-v4-flash'
60
+ const MESSAGE_TYPES = new Set<SessionEvent['type']>(['user/message', 'assistant/message'])
61
+
62
+ export interface EdgeSessionListPage {
63
+ sessions: EdgeSession[]
64
+ hasMore: boolean
65
+ nextAfter?: SessionId
66
+ }
67
+
68
+ /** Upstream session-list and subscription metadata derived from live or stored sessions. */
69
+ export interface EdgeApiSessionSummary {
70
+ id: SessionId
71
+ title: string | null
72
+ createdAt: number
73
+ lastPromptAt: number | null
74
+ updatedAt: number
75
+ lastSeq: number
76
+ blank: boolean
77
+ parentSessionId?: SessionId
78
+ origin?: 'subagent'
79
+ cwd?: string
80
+ agentPreset?: string
81
+ }
82
+
83
+ export interface EdgeSessionHistoryPage {
84
+ summary: EdgeApiSessionSummary
85
+ events: SessionEvent[]
86
+ hasMore: boolean
87
+ }
88
+
89
+ export interface EdgeSessionSearchPage {
90
+ items: SessionSearchItem[]
91
+ hasMore: boolean
92
+ }
93
+
94
+ export interface EdgeMuxBaseline {
95
+ sessions: EdgeApiSessionSummary[]
96
+ queues: { sessionId: SessionId; items: QueuedInboxItem[] }[]
97
+ }
98
+
99
+ export interface EdgeAgentPromptAdmission {
100
+ mode: 'queue' | 'steer'
101
+ content: ContentBlock[]
102
+ rpcId?: RpcId
103
+ clientTimeZone?: string
104
+ }
105
+
106
+ export type EdgeAgentPromptAdmitter = (input: EdgeAgentPromptAdmission) => Promise<void>
107
+
108
+ export class EdgeSessionStoreError extends Error {
109
+ constructor(
110
+ readonly code: 'NOT_FOUND' | 'BUSY' | 'INVALID_DATA' | 'TITLE_INVALID' | 'FORK_UNAVAILABLE',
111
+ message: string,
112
+ ) {
113
+ super(message)
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Edge-facing facade over the same SessionStore + SessionPersistence services
119
+ * used by upstream. Durable Object SQL is visible only to the backend plugin.
120
+ */
121
+ export class EdgeSessionStore {
122
+ private readonly context = new Context()
123
+ private readonly model = new EdgeDeepSeekAdapter(async () => (
124
+ await this.context.credentials.resolve(EDGE_DEEPSEEK_API_KEY_REF)
125
+ )?.value)
126
+ private readonly shells = new EdgeShellBindings()
127
+ private readonly blankHandles = new Map<SessionId, AgentHandle>()
128
+ private readonly turnPublishedAgents = new WeakSet<Agent>()
129
+ private readonly ready: Promise<void>
130
+
131
+ constructor(
132
+ storage: DurableObjectStorage,
133
+ config: EdgeSessionStoreConfig,
134
+ ) {
135
+ this.ready = this.initialize(storage, config)
136
+ }
137
+
138
+ private async initialize(
139
+ storage: DurableObjectStorage,
140
+ config: EdgeSessionStoreConfig,
141
+ ): Promise<void> {
142
+ await this.context.plugin(EdgeCredentialProvider, {
143
+ readDeepSeekApiKey: () => config.readDeepSeekApiKey(),
144
+ })
145
+ await this.context.plugin(LlmRuntime)
146
+ await this.context.plugin(SessionStore)
147
+ await this.context.plugin(SystemPrompt, { persona: EDGE_SYSTEM_PROMPT })
148
+ await this.context.plugin(ToolRuntime)
149
+ await this.context.plugin(AgentRegistry)
150
+ await installEdgeWebSearch(this.context, config.searchBaseURL)
151
+ await this.context.plugin(DurableObjectSessionPersistence, { storage })
152
+ await this.context.plugin(AgentLoop, { agents: [] })
153
+ this.context.effect(
154
+ () => this.context.llm.registerAdapter([EDGE_PROVIDER], this.model),
155
+ 'dsh-edge: model adapter',
156
+ )
157
+ this.context.effect(
158
+ () => this.context.tools.register(createEdgeBashTool(this.shells)),
159
+ 'dsh-edge: bash tool',
160
+ )
161
+ }
162
+
163
+ /** Describe one credential through the mounted upstream provider without exposing its value. */
164
+ async describeCredential(ref: string): Promise<CredentialInfo> {
165
+ await this.ready
166
+ return await this.context.credentials.describe(credentialRef(ref))
167
+ }
168
+
169
+ async createSession(input: CreateEdgeSessionInput): Promise<EdgeSession> {
170
+ const { agents, sessions, persistence } = await this.services()
171
+ const title = normalizeSessionTitle(input.title, MAX_TITLE_BYTES)
172
+ if (title.length === 0) {
173
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Session title must contain visible text.')
174
+ }
175
+ const id = SessionId(crypto.randomUUID())
176
+ const handle = await agents.create({
177
+ sessionId: id,
178
+ meta: {
179
+ cwd: '/workspace',
180
+ agentPreset: 'dsh-edge',
181
+ },
182
+ agentOptions: { provider: EDGE_PROVIDER, model: DEFAULT_EDGE_MODEL },
183
+ })
184
+ const { agent } = handle
185
+ const { session } = agent
186
+ try {
187
+ session.append('session/title', {
188
+ title,
189
+ messageSeqs: [],
190
+ source: { kind: 'user' },
191
+ })
192
+ // Upstream session creation is intentionally lazy. The required title
193
+ // supplies the first canonical event before this HTTP API returns 201.
194
+ await sessions.flush(session)
195
+ return summarize(session.header, session.events)
196
+ } catch (error) {
197
+ if (!(persistence instanceof DurableObjectSessionPersistence)) {
198
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Edge persistence backend is unavailable.')
199
+ }
200
+ await persistence.abandonUnmaterializedSession(session)
201
+ throw error
202
+ } finally {
203
+ await handle.dispose().catch((disposeError: unknown) => {
204
+ console.error('dsh-edge failed to release the created session.', disposeError)
205
+ })
206
+ }
207
+ }
208
+
209
+ /** Create the lazy blank session expected by the upstream Web client. */
210
+ async createBlankSession(input: {
211
+ sessionId?: SessionId
212
+ model: string
213
+ }): Promise<{ sessionId: SessionId; agentPreset: string; created: boolean }> {
214
+ const { agents, sessions, persistence } = await this.services()
215
+ const id = input.sessionId ?? SessionId(`session-${crypto.randomUUID()}`)
216
+ const attached = sessions.get(id)
217
+ if (attached !== undefined) {
218
+ if (attached.header.cwd !== '/workspace') {
219
+ throw new EdgeSessionStoreError(
220
+ 'INVALID_DATA',
221
+ `Session ${id} already belongs to ${attached.header.cwd ?? 'an unknown workspace'}.`,
222
+ )
223
+ }
224
+ return {
225
+ sessionId: id,
226
+ agentPreset: attached.header.agentPreset ?? 'dsh-edge',
227
+ created: false,
228
+ }
229
+ }
230
+ if (!(persistence instanceof DurableObjectSessionPersistence)) {
231
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Edge persistence backend is unavailable.')
232
+ }
233
+ const stored = persistence.readSessionSummary(id)
234
+ if (stored !== undefined) {
235
+ if (stored.meta.cwd !== '/workspace') {
236
+ throw new EdgeSessionStoreError(
237
+ 'INVALID_DATA',
238
+ `Session ${id} already belongs to ${stored.meta.cwd ?? 'an unknown workspace'}.`,
239
+ )
240
+ }
241
+ return {
242
+ sessionId: id,
243
+ agentPreset: stored.meta.agentPreset ?? 'dsh-edge',
244
+ created: false,
245
+ }
246
+ }
247
+ const retainedBlank = persistence.readBlankSession(id)
248
+ if (retainedBlank !== undefined) {
249
+ if (retainedBlank.cwd !== '/workspace') {
250
+ throw new EdgeSessionStoreError(
251
+ 'INVALID_DATA',
252
+ `Session ${id} already belongs to ${retainedBlank.cwd ?? 'an unknown workspace'}.`,
253
+ )
254
+ }
255
+ return {
256
+ sessionId: id,
257
+ agentPreset: retainedBlank.agentPreset ?? 'dsh-edge',
258
+ created: false,
259
+ }
260
+ }
261
+ const handle = await agents.create({
262
+ sessionId: id,
263
+ meta: { cwd: '/workspace', agentPreset: 'dsh-edge' },
264
+ agentOptions: { provider: EDGE_PROVIDER, model: input.model },
265
+ })
266
+ try {
267
+ await persistence.retainBlankSession(handle.agent.session.header)
268
+ this.blankHandles.set(id, handle)
269
+ return { sessionId: id, agentPreset: 'dsh-edge', created: true }
270
+ } catch (error) {
271
+ await persistence.abandonUnmaterializedSession(handle.agent.session)
272
+ await handle.dispose().catch((disposeError: unknown) => {
273
+ console.error('dsh-edge failed to roll back blank session creation.', disposeError)
274
+ })
275
+ throw error
276
+ }
277
+ }
278
+
279
+ async listSessions(
280
+ after: SessionId | undefined,
281
+ limit: number,
282
+ ): Promise<EdgeSessionListPage | undefined> {
283
+ const { persistence } = await this.services()
284
+ if (!(persistence instanceof DurableObjectSessionPersistence)) {
285
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Edge persistence backend is unavailable.')
286
+ }
287
+ const page = persistence.readSessionSummaryPage(after, limit)
288
+ if (page === undefined) return undefined
289
+ const sessions = page.sessions.map(summarizeStored)
290
+ const last = sessions.at(-1)
291
+ return {
292
+ sessions,
293
+ hasMore: page.hasMore,
294
+ ...last === undefined ? {} : { nextAfter: last.id },
295
+ }
296
+ }
297
+
298
+ async getSession(id: SessionId): Promise<EdgeSession | undefined> {
299
+ const { persistence } = await this.services()
300
+ if (!(persistence instanceof DurableObjectSessionPersistence)) {
301
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Edge persistence backend is unavailable.')
302
+ }
303
+ const stored = persistence.readSessionSummary(id)
304
+ if (stored !== undefined) return summarizeStored(stored)
305
+ const blank = persistence.readBlankSession(id)
306
+ return blank === undefined ? undefined : summarize(blank, [])
307
+ }
308
+
309
+ /** Read every session summary using the upstream list semantics. */
310
+ async listApiSessions(): Promise<EdgeApiSessionSummary[]> {
311
+ const { sessions, persistence } = await this.services()
312
+ if (!(persistence instanceof DurableObjectSessionPersistence)) {
313
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Edge persistence backend is unavailable.')
314
+ }
315
+ return collectApiSessions(sessions, persistence)
316
+ }
317
+
318
+ /** Read one upstream API summary without scanning the workspace registry. */
319
+ async getApiSessionSummary(id: SessionId): Promise<EdgeApiSessionSummary> {
320
+ const { sessions, persistence } = await this.services()
321
+ const live = sessions.get(id)
322
+ if (live !== undefined) return summarizeApiLive(live.header, live.events)
323
+ if (!(persistence instanceof DurableObjectSessionPersistence)) {
324
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Edge persistence backend is unavailable.')
325
+ }
326
+ const stored = persistence.readSessionSummary(id)
327
+ if (stored !== undefined) return summarizeApiStored(stored)
328
+ const blank = persistence.readBlankSession(id)
329
+ if (blank !== undefined) return summarizeApiLive(blank, [])
330
+ throw new EdgeSessionStoreError('NOT_FOUND', 'Session not found.')
331
+ }
332
+
333
+ /**
334
+ * Search a fixed work budget of canonical current-message surfaces for the sidebar.
335
+ * @param query - Non-empty query already validated by the upstream carrier.
336
+ * @param signal - Request cancellation, checked between session reads.
337
+ * @returns Up to the upstream result limit; `hasMore` also reports skipped over-budget logs.
338
+ */
339
+ async searchApiSessions(query: string, signal?: AbortSignal): Promise<EdgeSessionSearchPage> {
340
+ signal?.throwIfAborted()
341
+ const { sessions, persistence } = await this.services()
342
+ if (!(persistence instanceof DurableObjectSessionPersistence)) {
343
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Edge persistence backend is unavailable.')
344
+ }
345
+ const summaries = collectRecentApiSessions(persistence, MAX_SEARCH_SESSIONS + 1)
346
+ const candidates = summaries.slice(0, MAX_SEARCH_SESSIONS)
347
+ let hasMore = summaries.length > candidates.length
348
+ const items: SessionSearchItem[] = []
349
+ const normalizedQuery = normalizeSearchText(query)
350
+
351
+ for (const summary of candidates) {
352
+ signal?.throwIfAborted()
353
+ const live = sessions.get(summary.id)
354
+ let events: readonly SessionEvent[]
355
+ if (live !== undefined) {
356
+ await sessions.flush(live)
357
+ if (live.events.length > MAX_SEARCH_EVENTS_PER_SESSION) {
358
+ hasMore = true
359
+ continue
360
+ }
361
+ events = live.events
362
+ } else if (persistence.readBlankSession(summary.id) !== undefined) {
363
+ continue
364
+ } else {
365
+ const page = await persistence.readEventPage(
366
+ summary.id,
367
+ 0,
368
+ MAX_SEARCH_EVENTS_PER_SESSION,
369
+ MAX_SEARCH_STORED_BYTES_PER_SESSION,
370
+ signal,
371
+ )
372
+ if (page.hasMore) {
373
+ hasMore = true
374
+ continue
375
+ }
376
+ events = page.events
377
+ }
378
+ const match = buildSessionEventSearchDocuments(summary.id, events)
379
+ .findLast(document => document.surface === 'current'
380
+ && MESSAGE_TYPES.has(document.type)
381
+ && normalizeSearchText(document.text).includes(normalizedQuery))
382
+ if (match === undefined) continue
383
+ items.push({
384
+ sessionId: summary.id,
385
+ snippet: searchSnippet(match.text, query, SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS),
386
+ })
387
+ if (items.length > SESSION_SEARCH_RESULT_LIMIT) {
388
+ return { items: items.slice(0, SESSION_SEARCH_RESULT_LIMIT), hasMore: true }
389
+ }
390
+ }
391
+ return { items, hasMore }
392
+ }
393
+
394
+ /** Consume mux baselines synchronously with socket registration after readiness. */
395
+ async withMuxBaseline<T>(consume: (baseline: EdgeMuxBaseline) => T): Promise<T> {
396
+ const { agents, sessions, persistence } = await this.services()
397
+ if (!(persistence instanceof DurableObjectSessionPersistence)) {
398
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Edge persistence backend is unavailable.')
399
+ }
400
+ const queues: EdgeMuxBaseline['queues'] = []
401
+ for (const session of sessions.list()) {
402
+ const agent = agents.get(session.id)
403
+ if (agent?.session !== session || !agent.inbox.hasPending) continue
404
+ queues.push({ sessionId: session.id, items: queueItems(agent) })
405
+ }
406
+ return consume({ sessions: collectApiSessions(sessions, persistence), queues })
407
+ }
408
+
409
+ /** Require one live, canonical, or retained-blank session using only point reads. */
410
+ async requireSession(id: SessionId): Promise<void> {
411
+ const { sessions, persistence } = await this.services()
412
+ if (sessions.get(id) !== undefined) return
413
+ if (!(persistence instanceof DurableObjectSessionPersistence)) {
414
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Edge persistence backend is unavailable.')
415
+ }
416
+ if (persistence.readSessionHeader(id) !== undefined
417
+ || persistence.readBlankSession(id) !== undefined) return
418
+ throw new EdgeSessionStoreError('NOT_FOUND', 'Session not found.')
419
+ }
420
+
421
+ /** Page live history in memory or cold history at the Durable Object SQL boundary. */
422
+ async readHistoryPage(
423
+ id: SessionId,
424
+ beforeSeq: number | undefined,
425
+ maxMessages: number,
426
+ ): Promise<EdgeSessionHistoryPage> {
427
+ const { sessions, persistence } = await this.services()
428
+ const boundedMaxMessages = Math.min(maxMessages, EDGE_HISTORY_PAGE_LIMITS.maxMessages)
429
+ const live = sessions.get(id)
430
+ if (live !== undefined) {
431
+ const page = paginateHistory(live.events, beforeSeq, boundedMaxMessages)
432
+ return {
433
+ summary: summarizeApiLive(live.header, live.events),
434
+ events: page.events,
435
+ hasMore: page.hasMore,
436
+ }
437
+ }
438
+ if (!(persistence instanceof DurableObjectSessionPersistence)) {
439
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Edge persistence backend is unavailable.')
440
+ }
441
+ const blank = persistence.readBlankSession(id)
442
+ if (blank !== undefined) {
443
+ return { summary: summarizeApiLive(blank, []), events: [], hasMore: false }
444
+ }
445
+ const page = await persistence.readHistoryPage(id, beforeSeq, boundedMaxMessages)
446
+ if (page === undefined) throw new EdgeSessionStoreError('NOT_FOUND', 'Session not found.')
447
+ return {
448
+ summary: summarizeApiStored(page.summary),
449
+ events: page.events,
450
+ hasMore: page.hasMore,
451
+ }
452
+ }
453
+
454
+ /** Fork one completed-turn prefix through the upstream Session seed format. */
455
+ async forkSession(
456
+ id: SessionId,
457
+ atSeq: number | undefined,
458
+ model: string,
459
+ ): Promise<EdgeApiSessionSummary> {
460
+ const { agents, sessions, persistence } = await this.services()
461
+ if (!(persistence instanceof DurableObjectSessionPersistence)) {
462
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Edge persistence backend is unavailable.')
463
+ }
464
+ const live = sessions.get(id)
465
+ let header: SessionHeader
466
+ let events: readonly SessionEvent[]
467
+ if (live !== undefined) {
468
+ header = live.header
469
+ events = live.events
470
+ } else {
471
+ const blank = persistence.readBlankSession(id)
472
+ if (blank !== undefined) {
473
+ header = blank
474
+ events = []
475
+ return await this.createForkedSession(agents, sessions, persistence, id, header, events, atSeq, model)
476
+ }
477
+ if (persistence.readSessionHeader(id) === undefined) {
478
+ throw new EdgeSessionStoreError('NOT_FOUND', 'Session not found.')
479
+ }
480
+ const page = await persistence.readEventPage(
481
+ id,
482
+ 0,
483
+ MAX_FORK_EVENTS,
484
+ MAX_FORK_STORED_BYTES,
485
+ )
486
+ if (page.hasMore) {
487
+ throw new EdgeSessionStoreError(
488
+ 'FORK_UNAVAILABLE',
489
+ `Session ${id} exceeds the Edge fork history limit.`,
490
+ )
491
+ }
492
+ header = page.meta
493
+ events = page.events
494
+ }
495
+ return await this.createForkedSession(agents, sessions, persistence, id, header, events, atSeq, model)
496
+ }
497
+
498
+ private async createForkedSession(
499
+ agents: AgentRegistry,
500
+ sessions: SessionStore,
501
+ persistence: DurableObjectSessionPersistence,
502
+ id: SessionId,
503
+ header: SessionHeader,
504
+ events: readonly SessionEvent[],
505
+ atSeq: number | undefined,
506
+ model: string,
507
+ ): Promise<EdgeApiSessionSummary> {
508
+ const seed = completedForkSeed(id, events, atSeq)
509
+ assertForkSeedWithinLimits(id, seed)
510
+ const childId = SessionId(`session-${crypto.randomUUID()}`)
511
+ const handle = await agents.create({
512
+ sessionId: childId,
513
+ seed,
514
+ meta: {
515
+ ...header.cwd === undefined ? {} : { cwd: header.cwd },
516
+ parentSession: id,
517
+ seedLength: seed.length,
518
+ agentPreset: header.agentPreset ?? 'dsh-edge',
519
+ },
520
+ agentOptions: { provider: EDGE_PROVIDER, model },
521
+ })
522
+ try {
523
+ await sessions.flush(handle.agent.session)
524
+ return summarizeApiLive(handle.agent.session.header, handle.agent.session.events)
525
+ } catch (error) {
526
+ await persistence.abandonUnmaterializedSession(handle.agent.session)
527
+ throw error
528
+ } finally {
529
+ await handle.dispose().catch((disposeError: unknown) => {
530
+ console.error('dsh-edge failed to release the forked session.', disposeError)
531
+ })
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Append the canonical user-owned title event to a live or cold session.
537
+ *
538
+ * Upstream defines the synchronous append as the rename commit point. Its
539
+ * persistence coordinator owns write-behind and retirement retries, so this
540
+ * RPC must not report rejection after the accepted event can still commit.
541
+ * The result also transfers delivery to the caller only when no turn observer
542
+ * owned the event at that same synchronous append point.
543
+ */
544
+ async renameSession(
545
+ id: SessionId,
546
+ title: string,
547
+ model: string,
548
+ ): Promise<{
549
+ title: string
550
+ event: SessionEvent<'session/title'>
551
+ publishRequired: boolean
552
+ }> {
553
+ const normalized = normalizeSessionTitle(title, MAX_TITLE_BYTES)
554
+ if (normalized.length === 0) {
555
+ throw new EdgeSessionStoreError('TITLE_INVALID', 'Session title must contain visible text.')
556
+ }
557
+ const { agents } = await this.services()
558
+ const live = agents.get(id)
559
+ // Retained blanks still belong to this store: claim and retire their
560
+ // handle below. Every other registered agent may be running; metadata
561
+ // appends are valid while its turn owns the process-local handle.
562
+ if (live !== undefined && !this.blankHandles.has(id)) {
563
+ const publishRequired = !this.turnPublishedAgents.has(live)
564
+ return { title: normalized, event: appendUserTitle(live, normalized), publishRequired }
565
+ }
566
+
567
+ const handle = await this.openAgentForTurn(id, model)
568
+ try {
569
+ return {
570
+ title: normalized,
571
+ event: appendUserTitle(handle.agent, normalized),
572
+ publishRequired: true,
573
+ }
574
+ } finally {
575
+ await handle.dispose().catch((disposeError: unknown) => {
576
+ console.error('dsh-edge failed to release the renamed session.', disposeError)
577
+ })
578
+ }
579
+ }
580
+
581
+ /** Count live Agent owners for host.describe. */
582
+ async attachedSessionCount(): Promise<number> {
583
+ const { agents } = await this.services()
584
+ return agents.list().length
585
+ }
586
+
587
+ /** Resolve a live native agent, or cold-resume it through upstream persistence. */
588
+ async openAgentForTurn(id: SessionId, model: string): Promise<AgentHandle> {
589
+ const { agents, persistence } = await this.services()
590
+ const blank = this.blankHandles.get(id)
591
+ if (blank !== undefined) {
592
+ this.blankHandles.delete(id)
593
+ return blank
594
+ }
595
+ if (agents.get(id) !== undefined) {
596
+ throw new EdgeSessionStoreError('BUSY', 'Session already has a live agent owner.')
597
+ }
598
+ if (!(persistence instanceof DurableObjectSessionPersistence)) {
599
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Edge persistence backend is unavailable.')
600
+ }
601
+ if (!persistence.hasSession(id)) {
602
+ const retainedBlank = persistence.readBlankSession(id)
603
+ if (retainedBlank === undefined) {
604
+ throw new EdgeSessionStoreError('NOT_FOUND', 'Session not found.')
605
+ }
606
+ await persistence.materializeBlankSession(id)
607
+ }
608
+ const handle = await agents.resume({
609
+ resumeSessionId: id,
610
+ agentOptions: { provider: EDGE_PROVIDER, model },
611
+ })
612
+ try {
613
+ await this.context.sessions.flush(handle.agent.session)
614
+ return handle
615
+ } catch (error) {
616
+ await handle.dispose().catch((disposeError: unknown) => {
617
+ console.error('dsh-edge failed to roll back agent resume.', disposeError)
618
+ })
619
+ throw error
620
+ }
621
+ }
622
+
623
+ /** Drive one turn through ReactLoopAgent and publish only durable events. */
624
+ async runAgentTurn(input: {
625
+ agent: Agent
626
+ baseURL?: string
627
+ maxTokens?: number
628
+ reasoningEffort?: EdgeReasoningEffort
629
+ streamIdleTimeoutMs?: number
630
+ mode: 'queue' | 'steer'
631
+ content: ContentBlock[]
632
+ rpcId?: RpcId
633
+ clientTimeZone?: string
634
+ shell: EdgeShell
635
+ publish: (event: SessionEvent) => void | Promise<void>
636
+ publishQueue?: (items: QueuedInboxItem[]) => void | Promise<void>
637
+ afterFollowup?: () => void
638
+ onAdmitted?: (admit: EdgeAgentPromptAdmitter) => void
639
+ onClosing?: () => void
640
+ }): Promise<void> {
641
+ const { sessions } = await this.services()
642
+ const { agent } = input
643
+ if (this.context.agents.get(agent.id) !== agent || sessions.get(agent.id) !== agent.session) {
644
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Agent is not the live persistence owner.')
645
+ }
646
+
647
+ const releaseModel = this.model.bind(agent.id, {
648
+ ...input.baseURL === undefined ? {} : { baseURL: input.baseURL },
649
+ ...input.maxTokens === undefined ? {} : { maxTokens: input.maxTokens },
650
+ ...input.reasoningEffort === undefined
651
+ ? {}
652
+ : { reasoningEffort: input.reasoningEffort },
653
+ ...input.streamIdleTimeoutMs === undefined
654
+ ? {}
655
+ : { streamIdleTimeoutMs: input.streamIdleTimeoutMs },
656
+ })
657
+ let releaseShell: () => void
658
+ try {
659
+ releaseShell = this.shells.bind(agent.id, input.shell)
660
+ } catch (error) {
661
+ releaseModel()
662
+ throw error
663
+ }
664
+ let delivery = Promise.resolve()
665
+ let deliveryError: unknown
666
+ const stopObserving = this.context.on('session/event', (subject, event) => {
667
+ if (subject !== agent.session) return
668
+ const queue = event.type === 'agent/inbox/spliced'
669
+ ? queueItems(agent, event.data)
670
+ : undefined
671
+ delivery = delivery.then(async () => {
672
+ await sessions.flush(agent.session)
673
+ if (deliveryError !== undefined) return
674
+ try {
675
+ await input.publish(event)
676
+ if (queue !== undefined) await input.publishQueue?.(queue)
677
+ } catch (error) {
678
+ deliveryError = error
679
+ }
680
+ })
681
+ })
682
+ this.turnPublishedAgents.add(agent)
683
+ const admission = createDurablePromptAdmitter(
684
+ this.context,
685
+ agent,
686
+ () => sessions.flush(agent.session),
687
+ )
688
+
689
+ try {
690
+ const admitted = admission.admit({
691
+ mode: input.mode,
692
+ content: input.content,
693
+ ...input.rpcId === undefined ? {} : { rpcId: input.rpcId },
694
+ ...input.clientTimeZone === undefined
695
+ ? {}
696
+ : { clientTimeZone: input.clientTimeZone },
697
+ })
698
+ input.afterFollowup?.()
699
+ await admitted
700
+ input.onAdmitted?.(admission.admit)
701
+ while (true) {
702
+ await agent.whenIdle()
703
+ if (agent.status !== 'idle') continue
704
+ input.onClosing?.()
705
+ break
706
+ }
707
+ await delivery
708
+ await sessions.flush(agent.session)
709
+ } finally {
710
+ await delivery.catch(() => {})
711
+ await agent.whenIdle().catch(() => {})
712
+ admission.dispose()
713
+ stopObserving()
714
+ this.turnPublishedAgents.delete(agent)
715
+ releaseShell()
716
+ releaseModel()
717
+ }
718
+ }
719
+
720
+ async readEventPage(
721
+ id: SessionId,
722
+ fromSeq: number,
723
+ limit: number,
724
+ maxStoredBytes: number,
725
+ ): Promise<EdgeEventPage> {
726
+ const { persistence } = await this.services()
727
+ if (!(persistence instanceof DurableObjectSessionPersistence)) {
728
+ throw new EdgeSessionStoreError('INVALID_DATA', 'Edge persistence backend is unavailable.')
729
+ }
730
+ try {
731
+ return await persistence.readEventPage(id, fromSeq, limit, maxStoredBytes)
732
+ } catch (error) {
733
+ if (error instanceof Error && error.message === `session "${id}" not found`) {
734
+ throw new EdgeSessionStoreError('NOT_FOUND', 'Session not found.')
735
+ }
736
+ throw error
737
+ }
738
+ }
739
+
740
+ private async services(): Promise<{
741
+ agents: AgentRegistry
742
+ sessions: SessionStore
743
+ persistence: SessionPersistence
744
+ }> {
745
+ await this.ready
746
+ return {
747
+ agents: this.context.agents,
748
+ sessions: this.context.sessions,
749
+ persistence: this.context.sessionPersistence,
750
+ }
751
+ }
752
+ }
753
+
754
+ function appendUserTitle(agent: Agent, title: string): SessionEvent<'session/title'> {
755
+ return agent.session.append('session/title', {
756
+ title,
757
+ messageSeqs: [],
758
+ source: { kind: 'user' },
759
+ })
760
+ }
761
+
762
+ function collapseSearchWhitespace(value: string): string {
763
+ return value.replace(/\s+/gu, ' ').trim()
764
+ }
765
+
766
+ function normalizeSearchText(value: string): string {
767
+ return collapseSearchWhitespace(value).toLowerCase()
768
+ }
769
+
770
+ function searchMatchStart(
771
+ characters: readonly string[],
772
+ query: string,
773
+ ): number {
774
+ const sourceCodePointByCodeUnit: number[] = []
775
+ for (const [sourceIndex, character] of characters.entries()) {
776
+ const folded = character.toLowerCase()
777
+ for (let offset = 0; offset < folded.length; offset += 1) {
778
+ sourceCodePointByCodeUnit.push(sourceIndex)
779
+ }
780
+ }
781
+ // Preserve whole-string Unicode lowercasing semantics (for example, final
782
+ // sigma) while the per-code-point folded lengths provide the source map.
783
+ const normalized = characters.join('').toLowerCase()
784
+ if (sourceCodePointByCodeUnit.length !== normalized.length) {
785
+ throw new TypeError('Search normalization produced an unmappable source offset.')
786
+ }
787
+ const matchIndex = normalized.indexOf(normalizeSearchText(query))
788
+ return matchIndex < 0 ? 0 : sourceCodePointByCodeUnit[matchIndex] ?? 0
789
+ }
790
+
791
+ /**
792
+ * Build a plain-text excerpt around one literal match under the upstream wire bound.
793
+ * @param text - Complete searchable message text.
794
+ * @param query - Literal normalized match selected by the caller.
795
+ * @param maximum - Maximum Unicode code points in the result.
796
+ * @returns A whitespace-normalized excerpt with edge ellipses when truncated.
797
+ */
798
+ export function searchSnippet(text: string, query: string, maximum: number): string {
799
+ if (!Number.isSafeInteger(maximum) || maximum < 1) {
800
+ throw new TypeError('Search snippet maximum must be a positive safe integer.')
801
+ }
802
+ const clean = collapseSearchWhitespace(text)
803
+ const characters = Array.from(clean)
804
+ if (characters.length <= maximum) return clean
805
+ // Search the same whitespace-collapsed code points that the excerpt slices,
806
+ // retaining their source positions across length-changing Unicode case folds.
807
+ const matchStart = searchMatchStart(characters, query)
808
+ let start = Math.max(0, matchStart - Math.floor(maximum / 3))
809
+ let prefix = start > 0 ? '…' : ''
810
+ let suffix = '…'
811
+ let contentLength = maximum - prefix.length - suffix.length
812
+ if (contentLength < 1) {
813
+ start = matchStart
814
+ prefix = start > 0 ? '…' : ''
815
+ suffix = ''
816
+ contentLength = maximum - prefix.length
817
+ } else if (matchStart >= start + contentLength) {
818
+ start = matchStart - contentLength + 1
819
+ }
820
+ let end = Math.min(characters.length, start + contentLength)
821
+ if (end === characters.length) {
822
+ suffix = ''
823
+ contentLength = maximum - prefix.length
824
+ start = Math.max(0, end - contentLength)
825
+ prefix = start > 0 ? '…' : ''
826
+ }
827
+ end = Math.min(characters.length, start + maximum - prefix.length - suffix.length)
828
+ return `${prefix}${characters.slice(start, end).join('')}${suffix}`
829
+ }
830
+
831
+ function completedForkSeed(
832
+ id: SessionId,
833
+ events: readonly SessionEvent[],
834
+ atSeq: number | undefined,
835
+ ): SessionEvent[] {
836
+ const lastSeq = events.at(-1)?.seq ?? -1
837
+ const anchoredBoundary = atSeq === undefined
838
+ ? undefined
839
+ : events.find(event => event.type === 'turn/end' && event.seq >= atSeq)
840
+ const boundary = anchoredBoundary
841
+ ?? (atSeq === undefined || atSeq > lastSeq
842
+ ? events.findLast(event => event.type === 'turn/end')
843
+ : undefined)
844
+ if (boundary === undefined) {
845
+ throw new EdgeSessionStoreError(
846
+ 'FORK_UNAVAILABLE',
847
+ atSeq !== undefined && atSeq <= lastSeq
848
+ ? `Session ${id} has not completed the turn containing event ${String(atSeq)}.`
849
+ : `Session ${id} has no completed turn to fork from.`,
850
+ )
851
+ }
852
+ let cut = boundary.seq + 1
853
+ while (cut < events.length && events[cut]?.type !== 'turn/start') cut++
854
+ return events.slice(0, cut)
855
+ }
856
+
857
+ function assertForkSeedWithinLimits(id: SessionId, seed: readonly SessionEvent[]): void {
858
+ if (seed.length > MAX_FORK_EVENTS
859
+ || new TextEncoder().encode(JSON.stringify(seed)).byteLength > MAX_FORK_STORED_BYTES) {
860
+ throw new EdgeSessionStoreError(
861
+ 'FORK_UNAVAILABLE',
862
+ `Session ${id} exceeds the Edge fork history limit.`,
863
+ )
864
+ }
865
+ }
866
+
867
+ /** Gate model-visible prompt consumption on the Edge persistence flush barrier. */
868
+ export function createDurablePromptAdmitter(
869
+ ctx: Context,
870
+ agent: Agent,
871
+ flush: () => Promise<unknown>,
872
+ ): { admit: EdgeAgentPromptAdmitter; dispose(): void } {
873
+ const gates = new Map<string, Promise<boolean>>()
874
+ const stop = ctx.on('agent/pre-step', async ({ agent: subject, messages }, next) => {
875
+ if (subject !== agent) return next()
876
+ for (const message of messages) {
877
+ const gate = gates.get(message.id)
878
+ if (gate === undefined) continue
879
+ const durable = await gate
880
+ gates.delete(message.id)
881
+ if (!durable) return { kind: 'reject' as const }
882
+ }
883
+ return next()
884
+ })
885
+ const admit: EdgeAgentPromptAdmitter = async (prompt) => {
886
+ const message = createUserMessage({
887
+ content: prompt.content,
888
+ source: prompt.rpcId === undefined
889
+ ? { kind: 'user' }
890
+ : {
891
+ kind: 'user',
892
+ rpcId: prompt.rpcId,
893
+ ...prompt.clientTimeZone === undefined
894
+ ? {}
895
+ : { clientTimeZone: prompt.clientTimeZone },
896
+ },
897
+ })
898
+ const gate = Promise.withResolvers<boolean>()
899
+ gates.set(message.id, gate.promise)
900
+ try {
901
+ if (prompt.mode === 'steer') agent.steer(message)
902
+ else agent.followup(message)
903
+ } catch (error) {
904
+ gate.resolve(false)
905
+ gates.delete(message.id)
906
+ throw error
907
+ }
908
+ try {
909
+ await flush()
910
+ gate.resolve(true)
911
+ } catch {
912
+ // The inbox mutation already woke the driver, so this prompt remains
913
+ // accepted. Reject model-visible consumption; the turn's delivery
914
+ // barrier reports the persistence failure through host/agent-error.
915
+ gate.resolve(false)
916
+ }
917
+ }
918
+ return {
919
+ admit,
920
+ dispose() {
921
+ stop()
922
+ gates.clear()
923
+ },
924
+ }
925
+ }
926
+
927
+ /** Project both inbox targets, optionally applying the splice currently being emitted. */
928
+ function queueItems(
929
+ agent: Agent,
930
+ splice?: SessionEventMap['agent/inbox/spliced'],
931
+ ): QueuedInboxItem[] {
932
+ const project = (target: 'next-turn' | 'next-step'): readonly UserMessage[] => {
933
+ const messages = target === 'next-turn' ? agent.inbox.nextTurn : agent.inbox.nextStep
934
+ return splice?.target === target
935
+ ? messages.toSpliced(splice.start, splice.removedCount ?? 0, ...splice.inserted)
936
+ : messages
937
+ }
938
+ return [
939
+ ...project('next-turn').map(message => ({
940
+ id: message.id,
941
+ placement: 'queued' as const,
942
+ message,
943
+ })),
944
+ ...project('next-step').map(message => ({
945
+ id: message.id,
946
+ placement: message.source.kind === 'user' ? 'steering' as const : 'context' as const,
947
+ message,
948
+ })),
949
+ ]
950
+ }
951
+
952
+ function collectApiSessions(
953
+ sessions: SessionStore,
954
+ persistence: DurableObjectSessionPersistence,
955
+ ): EdgeApiSessionSummary[] {
956
+ const summaries = new Map<SessionId, EdgeApiSessionSummary>()
957
+ for (const blank of persistence.readAllBlankSessions()) {
958
+ summaries.set(blank.id, summarizeApiLive(blank, []))
959
+ }
960
+ for (const stored of persistence.readAllSessionSummaries()) {
961
+ summaries.set(stored.meta.id, summarizeApiStored(stored))
962
+ }
963
+ for (const session of sessions.list()) {
964
+ summaries.set(session.id, summarizeApiLive(session.header, session.events))
965
+ }
966
+ return [...summaries.values()].sort((left, right) =>
967
+ right.updatedAt - left.updatedAt || left.id.localeCompare(right.id))
968
+ }
969
+
970
+ function collectRecentApiSessions(
971
+ persistence: DurableObjectSessionPersistence,
972
+ limit: number,
973
+ ): EdgeApiSessionSummary[] {
974
+ return [
975
+ ...persistence.readRecentBlankSessions(limit).map(blank => summarizeApiLive(blank, [])),
976
+ ...persistence.readRecentSessionSummaries(limit).map(summarizeApiStored),
977
+ ].sort((left, right) =>
978
+ right.updatedAt - left.updatedAt || left.id.localeCompare(right.id)).slice(0, limit)
979
+ }
980
+
981
+ function summarizeApiLive(
982
+ header: SessionHeader,
983
+ events: readonly SessionEvent[],
984
+ ): EdgeApiSessionSummary {
985
+ const prompt = events.findLast(event =>
986
+ event.type === 'user/message' && event.data.source.kind === 'user')
987
+ return {
988
+ id: header.id,
989
+ title: foldSessionTitle(events)?.title ?? null,
990
+ createdAt: header.createdAt,
991
+ lastPromptAt: prompt?.time ?? null,
992
+ updatedAt: prompt?.time ?? header.createdAt,
993
+ lastSeq: events.at(-1)?.seq ?? -1,
994
+ blank: !events.some(event => event.type === 'turn/start'),
995
+ ...header.parentSession === undefined ? {} : { parentSessionId: header.parentSession },
996
+ ...header.origin === undefined ? {} : { origin: header.origin },
997
+ ...header.cwd === undefined ? {} : { cwd: header.cwd },
998
+ ...header.agentPreset === undefined ? {} : { agentPreset: header.agentPreset },
999
+ }
1000
+ }
1001
+
1002
+ function summarizeApiStored(stored: {
1003
+ meta: SessionHeader
1004
+ titleEvent?: SessionEvent<'session/title'>
1005
+ lastPromptAt: number | null
1006
+ lastSeq: number
1007
+ blank: boolean
1008
+ }): EdgeApiSessionSummary {
1009
+ return {
1010
+ id: stored.meta.id,
1011
+ title: stored.titleEvent === undefined
1012
+ ? null
1013
+ : foldSessionTitle([stored.titleEvent])?.title ?? null,
1014
+ createdAt: stored.meta.createdAt,
1015
+ lastPromptAt: stored.lastPromptAt,
1016
+ updatedAt: stored.lastPromptAt ?? stored.meta.createdAt,
1017
+ lastSeq: stored.lastSeq,
1018
+ blank: stored.blank,
1019
+ ...stored.meta.parentSession === undefined
1020
+ ? {}
1021
+ : { parentSessionId: stored.meta.parentSession },
1022
+ ...stored.meta.origin === undefined ? {} : { origin: stored.meta.origin },
1023
+ ...stored.meta.cwd === undefined ? {} : { cwd: stored.meta.cwd },
1024
+ ...stored.meta.agentPreset === undefined
1025
+ ? {}
1026
+ : { agentPreset: stored.meta.agentPreset },
1027
+ }
1028
+ }
1029
+
1030
+ function summarize(header: SessionHeader, events: readonly SessionEvent[]): EdgeSession {
1031
+ const title = foldSessionTitle(events)?.title ?? null
1032
+ return {
1033
+ id: header.id,
1034
+ title,
1035
+ ...header.agentPreset === undefined ? {} : { agentPreset: header.agentPreset },
1036
+ createdAt: header.createdAt,
1037
+ updatedAt: events.at(-1)?.time ?? header.createdAt,
1038
+ }
1039
+ }
1040
+
1041
+ function summarizeStored(stored: {
1042
+ meta: SessionHeader
1043
+ titleEvent?: SessionEvent
1044
+ updatedAt: number
1045
+ }): EdgeSession {
1046
+ return {
1047
+ id: stored.meta.id,
1048
+ title: stored.titleEvent === undefined
1049
+ ? null
1050
+ : foldSessionTitle([stored.titleEvent])?.title ?? null,
1051
+ ...stored.meta.agentPreset === undefined
1052
+ ? {}
1053
+ : { agentPreset: stored.meta.agentPreset },
1054
+ createdAt: stored.meta.createdAt,
1055
+ updatedAt: stored.updatedAt,
1056
+ }
1057
+ }
1058
+
1059
+ export function paginateHistory(
1060
+ events: readonly SessionEvent[],
1061
+ beforeSeq: number | undefined,
1062
+ maxMessages: number,
1063
+ ): { events: SessionEvent[]; hasMore: boolean } {
1064
+ const boundedMaxMessages = Math.min(maxMessages, EDGE_HISTORY_PAGE_LIMITS.maxMessages)
1065
+ const boundaryIndex = beforeSeq === undefined
1066
+ ? -1
1067
+ : events.findIndex(event => event.seq >= beforeSeq)
1068
+ const end = boundaryIndex < 0 ? events.length : boundaryIndex
1069
+ let count = 0
1070
+ let cut = 0
1071
+ for (let index = end - 1; index >= 0; index--) {
1072
+ const event = events[index] as SessionEvent
1073
+ if (!MESSAGE_TYPES.has(event.type) || !isAppendSurfaceEvent(event)) continue
1074
+ count++
1075
+ const sources = (event as SessionEvent & { sourceEventSeqs?: number[] }).sourceEventSeqs
1076
+ const groupStart = sources === undefined || sources.length === 0
1077
+ ? event.seq
1078
+ : sources.reduce((minimum, value) => Math.min(minimum, value), event.seq)
1079
+ if (count >= boundedMaxMessages) {
1080
+ cut = groupStart
1081
+ break
1082
+ }
1083
+ }
1084
+ const start = cut === 0
1085
+ ? 0
1086
+ : Math.max(0, events.findIndex(event => event.seq >= cut))
1087
+ const eventCount = end - start
1088
+ if (eventCount > EDGE_HISTORY_PAGE_LIMITS.maxEvents) {
1089
+ throw new EdgeSessionStoreError(
1090
+ 'INVALID_DATA',
1091
+ `History page exceeds the Edge limit of ${EDGE_HISTORY_PAGE_LIMITS.maxEvents} events.`,
1092
+ )
1093
+ }
1094
+ const page = events.slice(start, end)
1095
+ let encodedBytes = 2
1096
+ const encoder = new TextEncoder()
1097
+ for (const event of page) {
1098
+ encodedBytes += encoder.encode(JSON.stringify(event)).byteLength + 1
1099
+ if (encodedBytes > EDGE_HISTORY_PAGE_LIMITS.maxStoredBytes) {
1100
+ throw new EdgeSessionStoreError(
1101
+ 'INVALID_DATA',
1102
+ `History page exceeds the Edge limit of ${EDGE_HISTORY_PAGE_LIMITS.maxStoredBytes} encoded bytes.`,
1103
+ )
1104
+ }
1105
+ }
1106
+ return {
1107
+ events: page,
1108
+ hasMore: start > 0,
1109
+ }
1110
+ }