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,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
|
+
}
|