@morlay/ui-conversation-message-actions 0.0.19-alpha.3 → 0.0.19-alpha.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/ui-conversation-message-actions",
3
- "version": "0.0.19-alpha.3",
3
+ "version": "0.0.19-alpha.4",
4
4
  "description": "Rewind / retry / fork orchestration (host) plus conversation.chat.node renderer replacement (browser) — message edit / retry actions on user bubbles.",
5
5
  "keywords": [
6
6
  "conversation-chat-node",
@@ -38,12 +38,12 @@
38
38
  }
39
39
  },
40
40
  "dependencies": {
41
- "@morlay/session-branch": "0.0.10-alpha.3"
41
+ "@morlay/session-branch": "0.0.10-alpha.4"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@deepseek-ai/dsh-session-stats": "0.1.6-alpha.2",
45
- "@morlay/dsh-client-ui-primitives": "0.0.2-alpha.3",
46
- "@morlay/session-rdb": "0.0.21-alpha.3",
45
+ "@morlay/dsh-client-ui-primitives": "0.0.2-alpha.4",
46
+ "@morlay/session-rdb": "0.0.21-alpha.4",
47
47
  "@types/react": "~18.3.1",
48
48
  "@types/react-dom": "~18.3.0",
49
49
  "react": "^18.2.0",
@@ -67,8 +67,8 @@
67
67
  "@deepseek-ai/dsh-session-persistence": "0.1.6-alpha.2",
68
68
  "@deepseek-ai/dsh-session-projection": "0.1.6-alpha.2",
69
69
  "@deepseek-ai/dsh-token-meter": "0.1.6-alpha.2",
70
- "@morlay/dsh-client-ui-conversation": "0.0.2-alpha.3",
71
- "@morlay/dsh-client-ui-primitives": "0.0.2-alpha.3"
70
+ "@morlay/dsh-client-ui-conversation": "0.0.2-alpha.4",
71
+ "@morlay/dsh-client-ui-primitives": "0.0.2-alpha.4"
72
72
  },
73
73
  "dsh": {
74
74
  "bundle": {
@@ -26,6 +26,7 @@ type UserImage = Extract<UserMessageNode["content"][number], { type: "image" }>;
26
26
 
27
27
  function contentParts(content: readonly unknown[]): {
28
28
  text: string;
29
+ texts: string[];
29
30
  images: { attachment: UserImage["attachment"] }[];
30
31
  rest: unknown[];
31
32
  } {
@@ -39,8 +40,8 @@ function contentParts(content: readonly unknown[]): {
39
40
  images.push({ attachment: (b as UserImage).attachment });
40
41
  } else rest.push(block);
41
42
  }
42
- // 多块消息按空行拼回 raw markdown(新式提交就是单块原文)。
43
- return { text: texts.join("\n\n"), images, rest };
43
+ // 多块消息按空行拼回 raw markdown(新式提交就是单块原文);撤回回填用的是 texts(全部块)。
44
+ return { text: texts.join("\n\n"), texts, images, rest };
44
45
  }
45
46
 
46
47
  function UserStyleBubble({
@@ -100,6 +101,8 @@ export const UserMessageNodeView = memo(function UserMessageNodeView({
100
101
  openSkill: (name: string) => void;
101
102
  } & InjectFace<SessionEditorFace>) {
102
103
  const data = node.data;
104
+ // 撤回回填的文本:该消息的全部文本块(撤回截断整条消息,只回填首块会丢内容)。
105
+ const recallTexts = contentParts(data.content).texts;
103
106
  const [confirmingRecall, setConfirmingRecall] = useState<EditableMessageBlock | null>(null);
104
107
  const [confirmingRetry, setConfirmingRetry] = useState(false);
105
108
  const turnLocation =
@@ -155,7 +158,7 @@ export const UserMessageNodeView = memo(function UserMessageNodeView({
155
158
  onClick={() => {
156
159
  const target = confirmingRecall;
157
160
  setConfirmingRecall(null);
158
- void recall(target);
161
+ void recall(target, recallTexts);
159
162
  }}
160
163
  >
161
164
  撤回并编辑
@@ -1,282 +1,119 @@
1
1
  import type { Context as ClientContext } from "@deepseek-ai/cordis";
2
- import type {
3
- ISessions,
4
- SessionFace,
5
- SessionSnapshot,
6
- } from "@deepseek-ai/dsh-api-session-controller/client";
7
- import type { ObservableSnapshot, SnapshotStore } from "@deepseek-ai/dsh-client-store";
8
- import type { UiWorkspace } from "@deepseek-ai/dsh-client-ui-workspace/client";
9
- import { createSnapshotStore } from "@deepseek-ai/dsh-client-store";
2
+ import type { ISessions } from "@deepseek-ai/dsh-api-session-controller/client";
10
3
  import type { SessionId } from "@deepseek-ai/dsh-session";
11
4
  import {
12
5
  SESSION_EDITOR_PATH,
13
6
  type EditableMessageBlock,
14
7
  type SessionEditorOperation,
15
- type SessionEditorOperationResult,
16
- type SessionEditorTimeline,
17
8
  type VersionOperation,
18
9
  } from "../shared.ts";
19
10
 
20
- function editorApiPath(): string {
21
- const transport = (globalThis as { __DSH_TRANSPORT__?: { ownsHost?: boolean } })
22
- .__DSH_TRANSPORT__;
23
- return transport?.ownsHost === true ? `/api${SESSION_EDITOR_PATH}` : SESSION_EDITOR_PATH;
24
- }
25
-
26
- export interface SessionEditorState {
27
- status: "idle" | "loading" | "ready" | "error";
28
- error: string | null;
29
- pending: VersionOperation | "recall" | null;
30
- timeline: SessionEditorTimeline | null;
31
- }
32
-
11
+ /** 编辑器的 HTTP 路径:宿主(web 与桌面)在同一张路由表上服务它,页面不再按 ownsHost 加前缀。 */
12
+ const EDITOR_API_PATH = SESSION_EDITOR_PATH;
13
+
14
+ /**
15
+ * 会话编辑的浏览器半门面:只保留消息渲染面真正用到的两个动作。
16
+ *
17
+ * 曾经这里还有一条「订阅会话列表 / 快照 → 拉全量 timeline → 装进 store」的刷新管路,
18
+ * 它没有任何 UI 消费方,却把重放 / 流式期间的每一次快照抖动都换成一次全量 GET
19
+ * (编辑 / 重试 / 撤回后尤甚),订阅本身也从不释放。现在动作成功后只刷新会话列表元数据,
20
+ * 会话窗口交给上游的事件流收敛(不重建窗口、不整页重载)。
21
+ */
33
22
  export interface SessionEditorFace {
34
- hooks: { sessionEditor: ObservableSnapshot<SessionEditorState> };
35
- acquire(): () => void;
36
- load(): void;
37
- edit(
38
- message: EditableMessageBlock,
39
- text: string,
40
- cascade: "truncate" | "preserve",
41
- ): Promise<boolean>;
42
23
  retry(turn: number, cascade: "truncate" | "preserve"): Promise<boolean>;
43
- reroll(): Promise<boolean>;
44
- rewind(toBoundary: number): Promise<boolean>;
45
- recall(message: EditableMessageBlock): Promise<boolean>;
46
- openVersion(sessionId: string): Promise<void>;
47
- }
48
24
 
49
- function messageOf(error: unknown): string {
50
- return error instanceof Error ? error.message : String(error);
51
- }
52
-
53
- function conversationRevision(snapshot: SessionSnapshot): string {
54
- return [snapshot.openState, snapshot.removed, snapshot.hasMore].join("|");
25
+ // 撤回回填:文本由调用方(消息渲染面)给出**该消息的全部文本块**——不再靠 timeline 反查。
26
+ recall(message: EditableMessageBlock, texts: readonly string[]): Promise<boolean>;
55
27
  }
56
28
 
57
29
  export class SessionEditorController {
58
- readonly store: SnapshotStore<SessionEditorState> = createSnapshotStore<SessionEditorState>({
59
- status: "idle",
60
- error: null,
61
- pending: null,
62
- timeline: null,
63
- });
64
-
65
30
  readonly face: SessionEditorFace;
66
- private readonly ctx: ClientContext;
31
+
67
32
  private readonly sessions: ISessions;
68
- private readonly uiWorkspace: UiWorkspace;
69
- private sessionSource: SessionFace | undefined;
70
- private sessionSourceDispose: (() => void) | undefined;
71
- private sessionRevision: string | undefined;
72
- private disposed = false;
73
- private users = 0;
33
+ private pending: VersionOperation | "recall" | null = null;
74
34
 
75
35
  constructor(
76
36
  ctx: ClientContext,
77
37
  private readonly sessionId: SessionId,
78
38
  ) {
79
- this.ctx = ctx;
80
39
  this.sessions = ctx.get("sessions") as unknown as ISessions;
81
- this.uiWorkspace = ctx.get("uiWorkspace") as unknown as UiWorkspace;
82
40
  this.face = {
83
- hooks: { sessionEditor: this.store },
84
- acquire: () => {
85
- this.users += 1;
86
- if (this.users === 1 && this.disposed) this.revive();
87
- return () => this.release();
88
- },
89
- load: () => {
90
- void this.load();
91
- },
92
- edit: (message, text, cascade) =>
93
- this.mutate({
94
- action: "edit",
95
- sessionId: this.sessionId,
96
- eventSeq: message.eventSeq,
97
- blockIndex: message.blockIndex,
98
- text,
99
- cascade,
100
- }),
101
41
  retry: (turn, cascade) =>
102
42
  this.mutate({ action: "retry", sessionId: this.sessionId, turn, cascade }),
103
- reroll: () => this.mutate({ action: "reroll", sessionId: this.sessionId }),
104
- rewind: (toBoundary) =>
105
- this.mutate({ action: "rewind", sessionId: this.sessionId, toBoundary }),
106
- recall: (message) =>
43
+ recall: (message, texts) =>
107
44
  this.mutate(
108
45
  { action: "recall", sessionId: this.sessionId, eventSeq: message.eventSeq },
109
- () => this.setComposerDraft(this.messageTexts(message)),
46
+ () => this.setComposerDraft(texts.join("\n\n")),
110
47
  ),
111
- openVersion: (sessionId) => {
112
- this.uiWorkspace.openSession(sessionId as SessionId);
113
- return Promise.resolve();
114
- },
115
48
  };
116
- this.observe();
117
- }
118
-
119
- private observe(): void {
120
- this.sessionSource = undefined;
121
- this.sessionSourceDispose?.();
122
- this.bindSessionSource();
123
- this.sessions.list.subscribe(() => this.invalidate());
124
- }
125
-
126
- private bindSessionSource(): void {
127
- const source = this.sessions.binding(this.sessionId)?.session;
128
- if (source === this.sessionSource) return;
129
- this.sessionSourceDispose?.();
130
- this.sessionSource = source;
131
- this.sessionRevision =
132
- source === undefined ? undefined : conversationRevision(source.getSnapshot());
133
- this.sessionSourceDispose = source?.subscribe(() => {
134
- this.invalidate();
135
- });
136
- }
137
-
138
- private invalidate(): void {
139
- if (this.disposed || this.store.getSnapshot().status === "idle") return;
140
-
141
- void this.load();
142
- }
143
-
144
- private release(): void {
145
- this.users -= 1;
146
- if (this.users <= 0) this.dispose();
147
- }
148
-
149
- private dispose(): void {
150
- if (this.disposed) return;
151
- this.disposed = true;
152
- this.sessionSourceDispose?.();
153
- this.sessionSourceDispose = undefined;
154
- this.sessionSource = undefined;
155
- this.sessionRevision = undefined;
156
- }
157
-
158
- private revive(): void {
159
- this.disposed = false;
160
- this.observe();
161
- void this.load();
162
- }
163
-
164
- async load(): Promise<void> {
165
- if (this.disposed) return;
166
- this.store.update((state) => {
167
- state.status = "loading";
168
- state.error = null;
169
- });
170
- try {
171
- const response = await fetch(
172
- `${editorApiPath()}?sessionId=${encodeURIComponent(this.sessionId)}`,
173
- {
174
- method: "GET",
175
- headers: { accept: "application/json" },
176
- cache: "no-store",
177
- },
178
- );
179
- const value = (await response.json()) as unknown;
180
- if (this.disposed) return;
181
- if (response.ok) {
182
- this.store.update((state) => {
183
- state.status = "ready";
184
- state.error = null;
185
- state.timeline = value as SessionEditorTimeline;
186
- });
187
- } else {
188
- const error = (value as { error?: unknown })["error"];
189
- this.store.update((state) => {
190
- state.status = "error";
191
- state.error =
192
- typeof error === "string" ? error : `请求失败:HTTP ${String(response.status)}`;
193
- });
194
- }
195
- } catch (error) {
196
- if (this.disposed) return;
197
- this.store.update((state) => {
198
- state.status = "error";
199
- state.error = messageOf(error);
200
- });
201
- }
202
49
  }
203
50
 
204
51
  private async mutate(
205
52
  operation: SessionEditorOperation,
206
53
  onApplied?: () => void,
207
54
  ): Promise<boolean> {
208
- const current = this.store.getSnapshot();
209
- if (current.pending !== null) return false;
210
- this.store.update((state) => {
211
- state.pending = operation.action;
212
- state.error = null;
213
- });
55
+ if (this.pending !== null) return false;
56
+ this.pending = operation.action;
214
57
  try {
215
- const response = await fetch(editorApiPath(), {
58
+ const response = await fetch(EDITOR_API_PATH, {
216
59
  method: "POST",
217
60
  headers: { accept: "application/json", "content-type": "application/json" },
218
61
  body: JSON.stringify(operation),
219
62
  });
220
63
  const value = (await response.json()) as unknown;
221
- if (this.disposed) return true;
222
64
  if (!response.ok) {
223
65
  const error = (value as { error?: unknown })["error"];
224
66
  throw new Error(
225
67
  typeof error === "string" ? error : `请求失败:HTTP ${String(response.status)}`,
226
68
  );
227
69
  }
228
- this.store.update((state) => {
229
- state.pending = null;
230
- });
231
70
 
232
71
  onApplied?.();
233
- const result = value as SessionEditorOperationResult;
234
-
235
- if (String(result.sessionId) !== String(this.sessionId)) {
236
- this.uiWorkspace.openSession(result.sessionId as SessionId);
237
- return true;
238
- }
239
-
240
- const face = this.sessions.binding(this.sessionId)?.session;
241
- const resync = (face as unknown as { resync?: () => Promise<void> }).resync;
242
- if (resync !== undefined) {
243
- try {
244
- await resync.call(face);
245
72
 
246
- const projections = (
247
- face as unknown as { projections?: { truncate?(lastSeq: number): void } }
248
- ).projections;
249
- if (typeof projections?.truncate !== "function") {
250
- location.reload();
251
- return true;
252
- }
253
- projections.truncate(-1);
254
- void this.load();
255
- return true;
256
- } catch {}
73
+ // 会话窗口必须重建:rewind 把日志 seq 回退了,窗口里已渲染的旧节点不会自己消失
74
+ // (实测:被裁剪的消息残留、新消息也追加不进去)。重建入口只在**运行时**对象上
75
+ // (ClientSession 的 `resync()`;类型面 `SessionFace` 并未暴露它),所以逐个探测;
76
+ // 都拿不到时记一条 warn 便于诊断,且不回退整页重载(刷新会打断用户)。
77
+ try {
78
+ await this.rebuildWindow();
79
+ } catch (error: unknown) {
80
+ console.warn("[session-editor] 操作已生效,但会话窗口重建失败:", error);
257
81
  }
258
- location.reload();
259
82
  return true;
260
- } catch (error) {
261
- if (this.disposed) return false;
262
- this.store.update((state) => {
263
- state.pending = null;
264
- state.error = messageOf(error);
265
- });
83
+ } catch {
266
84
  return false;
85
+ } finally {
86
+ this.pending = null;
267
87
  }
268
88
  }
269
89
 
270
- private messageTexts(message: EditableMessageBlock): readonly string[] {
271
- const prefix = `${String(message.eventSeq)}:`;
272
- const blocks = (this.store.getSnapshot().timeline?.messages ?? [])
273
- .filter((row) => row.kind === "user" && row.key.startsWith(prefix))
274
- .sort((left, right) => left.blockIndex - right.blockIndex)
275
- .map((row) => row.text);
276
- return blocks.length === 0 ? [message.text] : blocks;
90
+ /**
91
+ * 让上游重开该会话的历史窗口(rewind 之后客户端窗口的 seq 基线已经失效)。
92
+ *
93
+ * 探测顺序:`binding(id).session.resync()`(上游 ClientSession 的重建入口,类型面没暴露)
94
+ * → `sessions.refresh()`(只刷列表元数据,聊胜于无)→ 都没有就记一条 warn。
95
+ */
96
+ private async rebuildWindow(): Promise<void> {
97
+ const sessions = this.sessions as unknown as {
98
+ binding?: (id: SessionId) => { session?: { resync?: () => Promise<void> } } | undefined;
99
+ refresh?: () => Promise<void>;
100
+ };
101
+ const face = sessions.binding?.(this.sessionId)?.session;
102
+ const resync = face?.resync;
103
+ if (typeof resync === "function") {
104
+ await resync.call(face);
105
+ return;
106
+ }
107
+ if (typeof sessions.refresh === "function") {
108
+ await sessions.refresh();
109
+ return;
110
+ }
111
+ console.warn(
112
+ "[session-editor] 未找到会话窗口重建入口(binding.resync / refresh 都不可用),窗口可能停留在 rewind 之前的状态",
113
+ );
277
114
  }
278
115
 
279
- private setComposerDraft(texts: readonly string[]): void {
116
+ private setComposerDraft(text: string): void {
280
117
  if (this.sessions.binding(this.sessionId) === undefined) return;
281
118
  const scoped = this.sessions.scope(this.sessionId);
282
119
  if (scoped === undefined) return;
@@ -287,6 +124,6 @@ export class SessionEditorController {
287
124
  };
288
125
  }
289
126
  | undefined;
290
- conversation?.input?.for(scoped).restoreDraft(texts.join("\n\n"));
127
+ conversation?.input?.for(scoped).restoreDraft(text);
291
128
  }
292
129
  }
@@ -24,10 +24,6 @@ export function apply(ctx: Context): void {
24
24
  return controller;
25
25
  };
26
26
 
27
- ctx.on("connection/reset", () => {
28
- for (const controller of controllers.values()) void controller.load();
29
- });
30
-
31
27
  registerChatNodeRenderers(ctx, controllerFor);
32
28
 
33
29
  ctx.slots.inject("conversation.composer.dock", () => registerComposerStats(ctx.slots));
package/src/index.ts CHANGED
@@ -13,35 +13,28 @@ import {
13
13
  SessionBranchError,
14
14
  balanceRewindPrefix,
15
15
  type BranchBoundary,
16
- type BranchTimeline,
17
16
  } from "@morlay/session-branch";
18
17
  import type {
19
18
  EditOperation,
20
- EditableMessageBlock,
21
19
  RecallOperation,
22
20
  RerollOperation,
23
21
  RetryOperation,
24
- RetryableTurn,
25
22
  SessionEditorResult,
26
23
  } from "./types.ts";
27
24
  import {
28
25
  SESSION_EDITOR_PATH,
29
26
  type SessionEditorOperation,
30
27
  type SessionEditorOperationResult,
31
- type SessionEditorTimeline,
32
- toTimelinePayload,
33
28
  } from "./shared.ts";
34
29
  import {
35
30
  closedTurns,
36
31
  droppedCompactions,
37
- editableMessages,
38
32
  editPlan,
39
33
  precedingContentIndex,
40
34
  recallBoundary,
41
35
  restoreCompactions,
42
36
  retryPlan,
43
37
  rerollPlan,
44
- retryableTurns,
45
38
  } from "./plan.ts";
46
39
 
47
40
  export type { CascadePolicy, EditableBlockKind } from "@morlay/session-branch";
@@ -57,10 +50,7 @@ export type {
57
50
  SessionEditorResult,
58
51
  } from "./types.ts";
59
52
 
60
- export { closedTurns, editableMessages, retryableTurns };
61
- export { SESSION_BRANCH_VERSION_SCHEMA } from "@morlay/session-branch";
62
-
63
- export type { BranchTimeline, SessionBranchVersionEvent } from "@morlay/session-branch";
53
+ export { closedTurns, editableMessages, retryableTurns } from "./plan.ts";
64
54
 
65
55
  declare module "@deepseek-ai/cordis" {
66
56
  interface Context {
@@ -102,18 +92,12 @@ export interface EditorAgentRegistry {
102
92
  }): Promise<EditorAgentHandle>;
103
93
  }
104
94
 
105
- function appendLogSeedEvent(
106
- events: SessionEvent[],
107
- type: string,
108
- data: unknown,
109
- ignorable = false,
110
- ): void {
95
+ function appendLogSeedEvent(events: SessionEvent[], type: string, data: unknown): void {
111
96
  events.push({
112
97
  type: type as SessionEvent["type"],
113
98
  seq: events.length,
114
99
  time: Date.now(),
115
100
  data: data as SessionEvent["data"],
116
- ...(ignorable ? { ignorable: true as const } : {}),
117
101
  } as SessionEvent);
118
102
  }
119
103
 
@@ -165,22 +149,8 @@ function keepFromOf(events: readonly SessionEvent[], boundary: number): number {
165
149
  async function appendSeedSuffixLive(
166
150
  session: Session,
167
151
  seedSuffix: readonly SessionEvent[],
168
- appendDirect: (events: readonly SessionEvent[]) => Promise<void>,
169
152
  ): Promise<void> {
170
153
  for (const event of seedSuffix) {
171
- const ignorable = (event as { ignorable?: boolean }).ignorable === true;
172
- if (ignorable) {
173
- const s = session as unknown as {
174
- log: SessionEvent[];
175
- eventsSnapshot?: unknown;
176
- };
177
-
178
- const seq = s.log.length;
179
- await appendDirect([{ ...event, seq } as SessionEvent]);
180
- s.log.push({ ...event, seq } as SessionEvent);
181
- s.eventsSnapshot = undefined;
182
- continue;
183
- }
184
154
  const s = session as unknown as {
185
155
  append(
186
156
  type: string,
@@ -244,10 +214,6 @@ export class SessionEditor extends Service {
244
214
  return this.ctx.sessionBranch.rewind(id, toBoundary, signal);
245
215
  }
246
216
 
247
- timeline(sessionId: SessionId, signal?: AbortSignal): Promise<BranchTimeline> {
248
- return this.ctx.sessionBranch.timeline(sessionId, signal);
249
- }
250
-
251
217
  edit(operation: EditOperation, signal?: AbortSignal): Promise<SessionEditorResult> {
252
218
  return this.branchOperation(operation, signal);
253
219
  }
@@ -264,19 +230,6 @@ export class SessionEditor extends Service {
264
230
  return this.recallOperation(operation, signal);
265
231
  }
266
232
 
267
- async editableMessages(
268
- sessionId: SessionId,
269
- signal?: AbortSignal,
270
- ): Promise<EditableMessageBlock[]> {
271
- const events = await this.readEvents(sessionId, signal);
272
- return editableMessages(closedTurns(events));
273
- }
274
-
275
- async retryableTurns(sessionId: SessionId, signal?: AbortSignal): Promise<RetryableTurn[]> {
276
- const events = await this.readEvents(sessionId, signal);
277
- return retryableTurns(closedTurns(events));
278
- }
279
-
280
233
  private async branchOperation(
281
234
  operation: EditOperation | RerollOperation | RetryOperation,
282
235
  signal?: AbortSignal,
@@ -309,8 +262,6 @@ export class SessionEditor extends Service {
309
262
 
310
263
  const replayTurn = preceding < 0 ? 1 : turns[preceding]!.turn + 1;
311
264
 
312
- const versionSeed: SessionEvent[] = [];
313
- appendLogSeedEvent(versionSeed, "session-branch/version", plan.version, true);
314
265
  const manualSeed: SessionEvent[] = [];
315
266
  if (plan.manualTurn !== undefined) {
316
267
  appendManualTurn(manualSeed, { ...plan.manualTurn, turn: replayTurn });
@@ -326,11 +277,10 @@ export class SessionEditor extends Service {
326
277
  await this.stopLoop(operation.sessionId, signal);
327
278
  const live = this.ctx.sessions.get(operation.sessionId);
328
279
  await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
329
- // 补回的压缩落在版本效果之后、重放的输入之前
280
+ // 补回的压缩落在重放的输入之前
330
281
  const seedStart = this.seedStart(live, events, keepFrom);
331
282
  const seedSuffix: SessionEvent[] = [
332
- ...versionSeed,
333
- ...restoreCompactions(compactions, keepFrom, seedStart + versionSeed.length),
283
+ ...restoreCompactions(compactions, keepFrom, seedStart),
334
284
  ...manualSeed,
335
285
  ];
336
286
  await this.appendSeedSuffix(operation.sessionId, live, seedSuffix, events, keepFrom);
@@ -402,22 +352,8 @@ export class SessionEditor extends Service {
402
352
  ): Promise<void> {
403
353
  if (seedSuffix.length === 0) return;
404
354
  if (live !== undefined) {
405
- const persistence = this.ctx.sessionPersistence as unknown as {
406
- tracker: {
407
- writerOf(id: SessionId):
408
- | {
409
- append(events: readonly SessionEvent[]): Promise<void>;
410
- }
411
- | undefined;
412
- };
413
- };
414
- const handle = persistence.tracker.writerOf(sessionId);
415
- if (handle === undefined) {
416
- throw new Error(`session "${sessionId}" has no live write handle for the version effect`);
417
- }
418
-
419
355
  await this.ctx.sessions.flush(live);
420
- await appendSeedSuffixLive(live, seedSuffix, (batch) => handle.append(batch));
356
+ await appendSeedSuffixLive(live, seedSuffix);
421
357
  await this.ctx.sessions.flush(live);
422
358
  return;
423
359
  }
@@ -631,16 +567,6 @@ function respondJson(response: HttpResponseLike, status: number, value: unknown)
631
567
  response.end(JSON.stringify(value));
632
568
  }
633
569
 
634
- async function readTimeline(
635
- editor: SessionEditor,
636
- sessionId: SessionId,
637
- ): Promise<SessionEditorTimeline> {
638
- const timeline = await editor.timeline(sessionId);
639
- const messages = await editor.editableMessages(sessionId);
640
- const retryable = await editor.retryableTurns(sessionId);
641
- return toTimelinePayload(sessionId, timeline, messages, retryable);
642
- }
643
-
644
570
  async function runOperation(
645
571
  editor: SessionEditor,
646
572
  operation: SessionEditorOperation,
@@ -699,12 +625,6 @@ async function handleRoute(
699
625
  response: HttpResponseLike,
700
626
  ): Promise<void> {
701
627
  try {
702
- if (request.method === "GET") {
703
- const url = new URL(request.url ?? SESSION_EDITOR_PATH, "http://session-editor.local");
704
- const sessionId = sessionIdOf(url.searchParams.get("sessionId"));
705
- respondJson(response, 200, await readTimeline(editor, sessionId));
706
- return;
707
- }
708
628
  if (request.method === "POST") {
709
629
  respondJson(
710
630
  response,
@@ -736,62 +656,4 @@ function registerHttpRoutes(ctx: Context): void {
736
656
  handler: (request, response) => handleRoute(editor, request, response),
737
657
  });
738
658
  }, "session-editor: HTTP route");
739
-
740
- const registerConnectionRoute = (): void => {
741
- const connection = ctx.get("connection", false) as
742
- | {
743
- fetch: {
744
- register(route: {
745
- path: string;
746
- methods: readonly ("GET" | "HEAD" | "POST")[];
747
- requestBody: "buffered" | "streaming";
748
- fetch: (request: Request) => Promise<Response>;
749
- }): () => Promise<void>;
750
- };
751
- }
752
- | undefined;
753
- if (connection === undefined) return;
754
- const editor = ctx.sessionEditor;
755
- ctx.effect(
756
- () =>
757
- connection.fetch.register({
758
- path: `/api${SESSION_EDITOR_PATH}`,
759
- methods: ["GET", "POST"],
760
- requestBody: "buffered",
761
- fetch: (request) => handleFetchRoute(editor, request),
762
- }),
763
- "session-editor: connection fetch route",
764
- );
765
- };
766
- ctx.on("internal/service", (name) => {
767
- if (name === "connection") registerConnectionRoute();
768
- });
769
- registerConnectionRoute();
770
- }
771
-
772
- async function handleFetchRoute(editor: SessionEditor, request: Request): Promise<Response> {
773
- try {
774
- if (request.method === "GET") {
775
- const url = new URL(request.url);
776
- const sessionId = sessionIdOf(url.searchParams.get("sessionId"));
777
- return jsonResponse(200, await readTimeline(editor, sessionId));
778
- }
779
- if (request.method === "POST") {
780
- return jsonResponse(200, await runOperation(editor, decodeOperation(await request.json())));
781
- }
782
- return new Response(null, { status: 405 });
783
- } catch (error: unknown) {
784
- const message = error instanceof Error ? error.message : String(error);
785
- return jsonResponse(error instanceof TypeError ? 400 : 409, { error: message });
786
- }
787
- }
788
-
789
- function jsonResponse(status: number, value: unknown): Response {
790
- return new Response(JSON.stringify(value), {
791
- status,
792
- headers: {
793
- "content-type": "application/json; charset=utf-8",
794
- "cache-control": "no-store",
795
- },
796
- });
797
659
  }