@mobius-os/mobius 0.3.43 → 0.3.45
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/package.json +1 -1
- package/src/aimux.ts +1 -1
- package/src/api.ts +13 -0
- package/src/components/Chat.tsx +1 -2
- package/src/hooks/useChat.ts +134 -25
- package/src/sse.ts +13 -7
- package/src/types.ts +12 -3
package/package.json
CHANGED
package/src/aimux.ts
CHANGED
|
@@ -92,7 +92,7 @@ async function pythonForAimux(onProgress?: (p: InstallProgress) => void): Promis
|
|
|
92
92
|
// 系统 python(如被精简掉 ensurepip 的容器镜像)。aimux 全部依赖为纯 Python,
|
|
93
93
|
// 故三平台可共用同一套打包产物,分别按 arch 发布到 CDN。
|
|
94
94
|
const BUNDLE_VER = '3'
|
|
95
|
-
const BUNDLE_AIMUX_VERSION = '0.1.
|
|
95
|
+
const BUNDLE_AIMUX_VERSION = '0.1.29'
|
|
96
96
|
/** Version expected from the installed or bundled AIMUX runtime. */
|
|
97
97
|
export const AIMUX_VERSION = BUNDLE_AIMUX_VERSION
|
|
98
98
|
const bundleDir = () => path.join(mobiusHome(), 'python-bundle')
|
package/src/api.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* bearer token (header `Authorization: Bearer <jwt>`); there is no cookie auth.
|
|
7
7
|
*/
|
|
8
8
|
import type {
|
|
9
|
+
AnyEntry,
|
|
9
10
|
AuthConfig,
|
|
11
|
+
HistoryGroup,
|
|
10
12
|
Issue,
|
|
11
13
|
LoginResponse,
|
|
12
14
|
Memory,
|
|
@@ -160,6 +162,17 @@ export class MobiusClient {
|
|
|
160
162
|
return this.request<SessionRuntimeStatus>(`/api/sessions/${encodeURIComponent(sessionId)}/status`, { signal })
|
|
161
163
|
}
|
|
162
164
|
|
|
165
|
+
// ── agent-history (协议 ①②: 组元数据 + 整组条目) ──────────────────────────
|
|
166
|
+
/** ① 全部组元数据, 一次给全. */
|
|
167
|
+
async listHistoryGroups(sessionId: string): Promise<{ session_version: number; groups: HistoryGroup[] }> {
|
|
168
|
+
return this.request(`/api/sessions/${encodeURIComponent(sessionId)}/groups`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** ② 某组全部条目 (全量, 无分页, 条目不可变). */
|
|
172
|
+
async listHistoryGroupEntries(sessionId: string, groupId: string): Promise<{ group_id: string; version: number; entries: AnyEntry[] }> {
|
|
173
|
+
return this.request(`/api/sessions/${encodeURIComponent(sessionId)}/groups/${encodeURIComponent(groupId)}/entries`)
|
|
174
|
+
}
|
|
175
|
+
|
|
163
176
|
// ── preference lookups ────────────────────────────────────────────────────
|
|
164
177
|
async modelOptions(): Promise<SessionModelOption[]> {
|
|
165
178
|
const r = await this.request<any>('/api/sessions/model-options')
|
package/src/components/Chat.tsx
CHANGED
|
@@ -152,8 +152,7 @@ export function ChatScreen({ client, ready, webUserId, resumeSessionId, onClear,
|
|
|
152
152
|
|
|
153
153
|
// First query of a fresh session triggers the full backend bootstrap (lazy
|
|
154
154
|
// session creation, worker spawn, context load) before any output streams.
|
|
155
|
-
// Label that phase
|
|
156
|
-
// so it reads as startup rather than a stuck agent. Once the first assistant
|
|
155
|
+
// Label that phase as startup rather than a stuck agent. Once the first assistant
|
|
157
156
|
// output is observed (or the session is a resumed one with prior history),
|
|
158
157
|
// the indicator falls back to the normal Working label for every turn.
|
|
159
158
|
const firstQueryInFlight = !resumeSessionId && !chat.entries.some(isAssistantOutput)
|
package/src/hooks/useChat.ts
CHANGED
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
* Lifecycle (per the TUI spec):
|
|
5
5
|
* - lazily create a session (POST /api/issues/:issueId/sessions) on the first
|
|
6
6
|
* submitted message, using the saved preferences;
|
|
7
|
-
* - open the SSE stream (GET /api/sessions/:id/events?token=)
|
|
8
|
-
*
|
|
7
|
+
* - open the SSE stream (GET /api/sessions/:id/events?token=); on subscribe
|
|
8
|
+
* bootstrap the transcript via ① groups + ② tail-group entries, then apply
|
|
9
|
+
* live `entries` batches (watermark + uuid dedup) as they arrive;
|
|
10
|
+
* - reconnects re-run the same ① negotiation (stateless reconciliation):
|
|
11
|
+
* groups whose version changed are refetched whole, transcript rebuilt;
|
|
9
12
|
* - keep the agent's busy state synchronized with the runtime status API.
|
|
10
13
|
* `/clear` remounts the hook (fresh session next time); `/resume` injects a
|
|
11
|
-
* pre-existing sessionId
|
|
14
|
+
* pre-existing sessionId.
|
|
12
15
|
*/
|
|
13
16
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
14
17
|
import { MobiusClient, ApiError } from '../api.js'
|
|
@@ -42,7 +45,7 @@ function nextId(): number { ID += 1; return ID }
|
|
|
42
45
|
/**
|
|
43
46
|
* Does this entry represent the user's just-submitted message? Used to retire the
|
|
44
47
|
* optimistic `pendingUser` placeholder once the real entry is observed — including
|
|
45
|
-
* via a reconnect's history
|
|
48
|
+
* via a reconnect's history reconciliation (the live `entries` path already clears it).
|
|
46
49
|
*
|
|
47
50
|
* Mobius may prepend injected context (project/issue framing) to a user turn, so we
|
|
48
51
|
* match the typed text as a suffix of the entry's normalized text rather than
|
|
@@ -66,6 +69,22 @@ function entryKey(entry: AnyEntry): string | null {
|
|
|
66
69
|
return typeof entry?.uuid === 'string' ? entry.uuid : null
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
// 首次 bootstrap 拉取的末尾组数 (旧 SSE 尾部回放的等价物; 更早的组按需不拉,
|
|
73
|
+
// TUI 是平铺字幕, 没有轮次展开概念, 末尾几组已覆盖活跃对话).
|
|
74
|
+
const BOOTSTRAP_GROUP_COUNT = 3
|
|
75
|
+
// A fresh session can spend several seconds creating the worker and loading
|
|
76
|
+
// context before /status reports alive=true. Keep the first-turn indicator
|
|
77
|
+
// visible during that bootstrap window instead of letting the short generic
|
|
78
|
+
// hint expire and leaving the user with no feedback.
|
|
79
|
+
const FIRST_TURN_BOOTSTRAP_GRACE_MS = 30_000
|
|
80
|
+
|
|
81
|
+
/** Mini group store: 组序 + 水位线 (version) + 组内条目. */
|
|
82
|
+
interface GroupSlot {
|
|
83
|
+
seq: number
|
|
84
|
+
version: number
|
|
85
|
+
entries: AnyEntry[]
|
|
86
|
+
}
|
|
87
|
+
|
|
69
88
|
// Retry transient gateway/transport errors so a brief 502/503/504 (a reverse-
|
|
70
89
|
// proxy blip, a backend worker recycling after a deploy, a transient upstream
|
|
71
90
|
// failure) doesn't immediately fail a message dispatch. 4xx errors are not
|
|
@@ -95,6 +114,8 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
95
114
|
const [sending, setSending] = useState(false)
|
|
96
115
|
const [error, setError] = useState<string | null>(null)
|
|
97
116
|
const sseRef = useRef<SseConnection | null>(null)
|
|
117
|
+
// agent-history mini group store (协议 ①②③ 的 TUI 侧消费形态).
|
|
118
|
+
const groupSlotsRef = useRef<Map<string, GroupSlot>>(new Map())
|
|
98
119
|
const pollNowRef = useRef<(() => void) | null>(null)
|
|
99
120
|
const typingRef = useRef(false)
|
|
100
121
|
const sendingRef = useRef(false)
|
|
@@ -103,7 +124,7 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
103
124
|
// SSE auto-reconnect state. A reverse proxy's idle timeout (or a server
|
|
104
125
|
// restart) drops the stream mid-session; without reconnect the TUI stops
|
|
105
126
|
// receiving new jsonl entries even though the web client keeps updating.
|
|
106
|
-
// On reconnect the
|
|
127
|
+
// On reconnect the stateless ①② reconciliation refills any missed entries.
|
|
107
128
|
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
108
129
|
const reconnectAttemptRef = useRef(0)
|
|
109
130
|
const aliveRef = useRef(true)
|
|
@@ -119,7 +140,7 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
119
140
|
const appendEntries = useCallback((newOnes: AnyEntry[]) => {
|
|
120
141
|
if (!newOnes.length) return
|
|
121
142
|
setEntries(prev => {
|
|
122
|
-
// De-duplicate by uuid so a live
|
|
143
|
+
// De-duplicate by uuid so a live `entries` batch that also appears in a
|
|
123
144
|
// reconnect's history replay is never shown twice.
|
|
124
145
|
const seen = new Set<string>()
|
|
125
146
|
for (const e of prev) { const k = entryKey(e); if (k) seen.add(k) }
|
|
@@ -145,6 +166,63 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
145
166
|
setEntries(out)
|
|
146
167
|
}, [])
|
|
147
168
|
|
|
169
|
+
// ── agent-history (协议 ①②③): bootstrap / 重连对账 ──────────────────────
|
|
170
|
+
|
|
171
|
+
/** 按组序摊平重建 transcript (uuid 去重由 setHistory 兜底). */
|
|
172
|
+
const rebuildEntriesFromGroups = useCallback(() => {
|
|
173
|
+
const slots = [...groupSlotsRef.current.values()].sort((a, b) => a.seq - b.seq)
|
|
174
|
+
const flat: AnyEntry[] = []
|
|
175
|
+
for (const s of slots) flat.push(...s.entries)
|
|
176
|
+
setHistory(flat)
|
|
177
|
+
}, [setHistory])
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Stateless 对账 (订阅时/重连时同一条路径):
|
|
181
|
+
* ① 拿全部组元数据 → 本地没有的或 version 变了的组 ② 整组重拉 → 重建 transcript.
|
|
182
|
+
* 首次 (本地空) 只拉末尾 BOOTSTRAP_GROUP_COUNT 组; 之后每次只补差额, 常态零请求.
|
|
183
|
+
*/
|
|
184
|
+
const reconcileHistory = useCallback(async (sid: string) => {
|
|
185
|
+
try {
|
|
186
|
+
const data = await client.listHistoryGroups(sid)
|
|
187
|
+
const groups: any[] = Array.isArray(data?.groups) ? data.groups : []
|
|
188
|
+
const local = groupSlotsRef.current
|
|
189
|
+
const targets = local.size === 0 ? groups.slice(-BOOTSTRAP_GROUP_COUNT) : groups
|
|
190
|
+
let changed = false
|
|
191
|
+
for (const g of targets) {
|
|
192
|
+
const gid = String(g?.id ?? '')
|
|
193
|
+
if (!gid) continue
|
|
194
|
+
const ver = Number(g?.version) || 0
|
|
195
|
+
const cur = local.get(gid)
|
|
196
|
+
if (cur && cur.version >= ver) continue
|
|
197
|
+
try {
|
|
198
|
+
const r = await client.listHistoryGroupEntries(sid, gid)
|
|
199
|
+
local.set(gid, {
|
|
200
|
+
seq: Number(g?.seq) || (local.size + 1),
|
|
201
|
+
version: Number(r?.version) || 0,
|
|
202
|
+
entries: Array.isArray(r?.entries) ? r.entries : [],
|
|
203
|
+
})
|
|
204
|
+
changed = true
|
|
205
|
+
} catch { /* 单组失败不阻塞其余组 */ }
|
|
206
|
+
}
|
|
207
|
+
// 服务端已不存在的组 → 丢弃 (会话被删/重建的防御).
|
|
208
|
+
const alive = new Set(groups.map((g: any) => String(g?.id ?? '')))
|
|
209
|
+
for (const gid of [...local.keys()]) {
|
|
210
|
+
if (!alive.has(gid)) { local.delete(gid); changed = true }
|
|
211
|
+
}
|
|
212
|
+
if (changed) rebuildEntriesFromGroups()
|
|
213
|
+
// 对账补齐后, 若乐观占位已被真实条目覆盖 → 退掉 (断线期间整轮完成的场景).
|
|
214
|
+
setPendingUser(prev => {
|
|
215
|
+
if (prev === null) return prev
|
|
216
|
+
const slots = [...local.values()].sort((a, b) => a.seq - b.seq)
|
|
217
|
+
const flat: AnyEntry[] = []
|
|
218
|
+
for (const s of slots) flat.push(...s.entries)
|
|
219
|
+
return flat.some(e => entryMatchesPendingUser(e, prev)) ? null : prev
|
|
220
|
+
})
|
|
221
|
+
} catch (e) {
|
|
222
|
+
if (process.env.MOBIUS_TUI_DEBUG) console.error('[history-reconcile]', (e as Error)?.message ?? e)
|
|
223
|
+
}
|
|
224
|
+
}, [client, rebuildEntriesFromGroups])
|
|
225
|
+
|
|
148
226
|
// ── SSE connection ────────────────────────────────────────────────────────
|
|
149
227
|
const connect = useCallback((sid: string) => {
|
|
150
228
|
if (process.env.MOBIUS_TUI_DEBUG) console.error('[connect]', sid)
|
|
@@ -153,23 +231,49 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
153
231
|
if (reconnectTimerRef.current) { clearTimeout(reconnectTimerRef.current); reconnectTimerRef.current = null }
|
|
154
232
|
const url = `${client.server}/api/sessions/${encodeURIComponent(sid)}/events?token=${encodeURIComponent(client.token)}`
|
|
155
233
|
const conn = new SseConnection(url, {
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
setPendingUser(prev => (prev !== null && es.some(e => entryMatchesPendingUser(e, prev)) ? null : prev))
|
|
234
|
+
onGroupCreated: (group) => {
|
|
235
|
+
if (!group || typeof group !== 'object') return
|
|
236
|
+
const gid = String(group.id ?? '')
|
|
237
|
+
if (!gid || groupSlotsRef.current.has(gid)) return // 元数据不可变, 已知即忽略
|
|
238
|
+
groupSlotsRef.current.set(gid, {
|
|
239
|
+
seq: Number(group.seq) || (groupSlotsRef.current.size + 1),
|
|
240
|
+
version: Number(group.version) || 1,
|
|
241
|
+
entries: [],
|
|
242
|
+
})
|
|
166
243
|
},
|
|
167
|
-
|
|
168
|
-
if (process.env.MOBIUS_TUI_DEBUG) console.error('[
|
|
169
|
-
|
|
170
|
-
|
|
244
|
+
onEntries: ({ group_id, group_id_version, entries }) => {
|
|
245
|
+
if (process.env.MOBIUS_TUI_DEBUG) console.error('[onEntries]', group_id, group_id_version, entries.length)
|
|
246
|
+
const gid = String(group_id ?? '')
|
|
247
|
+
if (!gid) return
|
|
248
|
+
let slot = groupSlotsRef.current.get(gid)
|
|
249
|
+
if (!slot) {
|
|
250
|
+
// 事件早到且本地无该组 (错过 group_created): 建槽后按水位线对账.
|
|
251
|
+
slot = { seq: groupSlotsRef.current.size + 1, version: 0, entries: [] }
|
|
252
|
+
groupSlotsRef.current.set(gid, slot)
|
|
253
|
+
}
|
|
254
|
+
const version = Number(group_id_version) || 0
|
|
255
|
+
if (slot.entries.length > 0 && version <= slot.version) return // 水位线: ≤ 本地即丢弃
|
|
256
|
+
// uuid 去重保险丝: 与整组重拉/对账重叠的条目只留一份.
|
|
257
|
+
const known = new Set<string>()
|
|
258
|
+
for (const e of slot.entries) { const k = entryKey(e); if (k) known.add(k) }
|
|
259
|
+
const fresh = entries.filter(e => {
|
|
260
|
+
const k = entryKey(e)
|
|
261
|
+
if (k && known.has(k)) return false
|
|
262
|
+
if (k) known.add(k)
|
|
263
|
+
return true
|
|
264
|
+
})
|
|
265
|
+
slot.entries = slot.entries.concat(fresh)
|
|
266
|
+
slot.version = Math.max(slot.version, version)
|
|
267
|
+
if (fresh.length > 0) {
|
|
268
|
+
appendEntries(fresh)
|
|
269
|
+
setPendingUser(null)
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
onSubscribed: () => {
|
|
273
|
+
reconnectAttemptRef.current = 0
|
|
274
|
+
// 订阅即对账 (首开 = bootstrap 拉末尾几组; 重连 = stateless 补差额).
|
|
275
|
+
void reconcileHistory(sid)
|
|
171
276
|
},
|
|
172
|
-
onSubscribed: () => { reconnectAttemptRef.current = 0 },
|
|
173
277
|
onTyping: (active) => {
|
|
174
278
|
// SSE is a low-latency hint, not the source of truth. A `true` event
|
|
175
279
|
// lights the indicator immediately; either edge requests a fresh
|
|
@@ -179,7 +283,11 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
179
283
|
workingHintUntilRef.current = Date.now() + 1_500
|
|
180
284
|
updateTyping(true)
|
|
181
285
|
} else {
|
|
182
|
-
|
|
286
|
+
// A fresh turn uses a longer bootstrap grace period. Some agents
|
|
287
|
+
// emit an early typing=false edge before their worker is observable;
|
|
288
|
+
// do not let that transient edge erase the first-turn indicator.
|
|
289
|
+
const remaining = workingHintUntilRef.current - Date.now()
|
|
290
|
+
if (remaining < 5_000) workingHintUntilRef.current = 0
|
|
183
291
|
}
|
|
184
292
|
pollNowRef.current?.()
|
|
185
293
|
},
|
|
@@ -204,7 +312,7 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
204
312
|
})
|
|
205
313
|
sseRef.current = conn
|
|
206
314
|
conn.start()
|
|
207
|
-
}, [client.server, client.token, appendEntries, setHistory, updateTyping])
|
|
315
|
+
}, [client.server, client.token, appendEntries, setHistory, updateTyping, reconcileHistory])
|
|
208
316
|
doConnectRef.current = connect
|
|
209
317
|
|
|
210
318
|
const ensureSseForSend = useCallback((sid: string): boolean => {
|
|
@@ -374,7 +482,8 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
374
482
|
setError(null)
|
|
375
483
|
setPendingUser(body)
|
|
376
484
|
statusEpochRef.current += 1
|
|
377
|
-
|
|
485
|
+
const firstTurn = !sessionId && entries.length === 0
|
|
486
|
+
workingHintUntilRef.current = Date.now() + (firstTurn ? FIRST_TURN_BOOTSTRAP_GRACE_MS : 2_000)
|
|
378
487
|
sendingRef.current = true
|
|
379
488
|
updateTyping(true)
|
|
380
489
|
setSending(true)
|
|
@@ -400,7 +509,7 @@ export function useChat({ client, ready, resumeSessionId }: ChatApi): ChatContro
|
|
|
400
509
|
setSending(false)
|
|
401
510
|
pollNowRef.current?.()
|
|
402
511
|
}
|
|
403
|
-
}, [sending, ensureSession, ensureSseForSend, client, updateTyping])
|
|
512
|
+
}, [sending, sessionId, entries.length, ensureSession, ensureSseForSend, client, updateTyping])
|
|
404
513
|
|
|
405
514
|
const stop = useCallback(async () => {
|
|
406
515
|
if (!sessionId) return
|
package/src/sse.ts
CHANGED
|
@@ -15,8 +15,10 @@ import type { AnyEntry } from './types.js'
|
|
|
15
15
|
export interface SseHandlers {
|
|
16
16
|
onOpen?: () => void
|
|
17
17
|
onSubscribed?: (session: any) => void
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/** ③ 新组元数据 (开轮卡本身随随后的 entries 事件到达). */
|
|
19
|
+
onGroupCreated?: (group: any) => void
|
|
20
|
+
/** ③ 组条目增量: version = 应用该批后的组版本 (调用方水位线判据). */
|
|
21
|
+
onEntries?: (payload: { group_id: string; group_id_version: number; entries: AnyEntry[] }) => void
|
|
20
22
|
onTyping?: (active: boolean) => void
|
|
21
23
|
onError?: (message: string, category?: string) => void
|
|
22
24
|
onClose?: () => void
|
|
@@ -105,11 +107,15 @@ export class SseConnection {
|
|
|
105
107
|
const ev = p?.event ?? eventName
|
|
106
108
|
switch (ev) {
|
|
107
109
|
case 'subscribed': this.handlers.onSubscribed?.(p.session); break
|
|
108
|
-
case '
|
|
109
|
-
this.handlers.
|
|
110
|
+
case 'group_created':
|
|
111
|
+
this.handlers.onGroupCreated?.(p.group)
|
|
110
112
|
break
|
|
111
|
-
case '
|
|
112
|
-
this.handlers.
|
|
113
|
+
case 'entries':
|
|
114
|
+
this.handlers.onEntries?.({
|
|
115
|
+
group_id: String(p.group_id ?? ''),
|
|
116
|
+
group_id_version: Number(p.group_id_version) || 0,
|
|
117
|
+
entries: Array.isArray(p.entries) ? p.entries : [],
|
|
118
|
+
})
|
|
113
119
|
break
|
|
114
120
|
case 'typing':
|
|
115
121
|
this.handlers.onTyping?.(!!p.active)
|
|
@@ -119,7 +125,7 @@ export class SseConnection {
|
|
|
119
125
|
this.handlers.onError?.(p.message ?? p.error ?? '未知错误', p.category)
|
|
120
126
|
break
|
|
121
127
|
default:
|
|
122
|
-
// history /
|
|
128
|
+
// history / message / stream / etc. — currently unused by the TUI.
|
|
123
129
|
break
|
|
124
130
|
}
|
|
125
131
|
}
|
package/src/types.ts
CHANGED
|
@@ -193,14 +193,23 @@ export interface ResourceAccess {
|
|
|
193
193
|
// ════════════════════════════════════════════════════════════════════════════
|
|
194
194
|
export type AnyEntry = Record<string, any>
|
|
195
195
|
|
|
196
|
+
// ── agent-history 组元数据 (协议 ① 的载荷) ───────────────────────────────────
|
|
197
|
+
export interface HistoryGroup {
|
|
198
|
+
id: string
|
|
199
|
+
seq: number
|
|
200
|
+
opener_ts: string | null
|
|
201
|
+
user_summary: string
|
|
202
|
+
version: number
|
|
203
|
+
entry_count: number
|
|
204
|
+
}
|
|
205
|
+
|
|
196
206
|
// ── SSE envelope events (GET /api/sessions/:id/events) ───────────────────────
|
|
197
207
|
// Each SSE frame's data is a JSON object with an `event` discriminator.
|
|
198
208
|
export type SseEvent =
|
|
199
209
|
| { event: 'subscribed'; session: Session }
|
|
200
210
|
| { event: 'history'; messages: any[]; total?: number }
|
|
201
|
-
| { event: '
|
|
202
|
-
| { event: '
|
|
203
|
-
| { event: 'jsonl_entry'; session_id: string; entry: AnyEntry }
|
|
211
|
+
| { event: 'group_created'; session_id: string; group: HistoryGroup }
|
|
212
|
+
| { event: 'entries'; session_id: string; group_id: string; group_id_version: number; entries: AnyEntry[] }
|
|
204
213
|
| { event: 'typing'; active: boolean }
|
|
205
214
|
| { event: 'error'; message?: string; category?: string }
|
|
206
215
|
| { event: 'server_error'; message?: string }
|