@morlay/ui-conversation-message-actions 0.0.12 → 0.0.13

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/src/plan.ts ADDED
@@ -0,0 +1,322 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { AssistantMessage, ContentBlock, UserMessage } from "@deepseek-ai/dsh-llm";
3
+ import type { SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
4
+ import {
5
+ SESSION_BRANCH_VERSION_SCHEMA,
6
+ SessionBranchError,
7
+ type EditableBlockKind,
8
+ type SessionBranchEffect,
9
+ type SessionBranchVersionEvent,
10
+ } from "@morlay/session-branch";
11
+ import type {
12
+ EditOperation,
13
+ EditableMessageBlock,
14
+ RerollOperation,
15
+ RetryOperation,
16
+ RetryableTurn,
17
+ } from "./types.ts";
18
+
19
+ export interface ClosedTurn {
20
+ turn: number;
21
+ startSeq: number;
22
+
23
+ endSeq?: number;
24
+
25
+ closed: boolean;
26
+ user?: SessionEvent<"user/message">;
27
+ /** 轮内全部 user/message(agent 运行中 followup 追加的输入也计入)。 */
28
+ users: SessionEvent<"user/message">[];
29
+ assistants: SessionEvent<"assistant/message">[];
30
+ }
31
+
32
+ export interface OperationPlan {
33
+ anchorSeq: number;
34
+
35
+ rewindBoundary?: number;
36
+ version: SessionBranchVersionEvent;
37
+
38
+ manualTurn?: { turn: number; user: UserMessage; assistant: AssistantMessage };
39
+
40
+ queuedUsers: UserMessage[];
41
+ }
42
+
43
+ function isTextualBlock(
44
+ block: ContentBlock | undefined,
45
+ ): block is Extract<ContentBlock, { type: "text" | "reasoning" }> {
46
+ return block?.type === "text" || block?.type === "reasoning";
47
+ }
48
+
49
+ function userText(message: UserMessage): string {
50
+ return message.content
51
+ .filter((block): block is Extract<ContentBlock, { type: "text" }> => block.type === "text")
52
+ .map((block) => block.text)
53
+ .join("\n");
54
+ }
55
+
56
+ function cloneUser(
57
+ message: UserMessage,
58
+ content: ContentBlock[] = structuredClone(message.content),
59
+ ): UserMessage {
60
+ return Object.freeze({
61
+ id: randomUUID(),
62
+ role: "user",
63
+ content: Object.freeze(content),
64
+ source: Object.freeze({ kind: "user" }),
65
+ }) as UserMessage;
66
+ }
67
+
68
+ function replaceTextBlock(
69
+ content: readonly ContentBlock[],
70
+ blockIndex: number,
71
+ text: string,
72
+ ): ContentBlock[] {
73
+ const block = content[blockIndex];
74
+ if (!isTextualBlock(block))
75
+ throw new SessionBranchError("所选内容块不是可编辑文本。", "INVALID_BOUNDARY");
76
+ return content.map((candidate, index) =>
77
+ index === blockIndex ? ({ ...candidate, text } as ContentBlock) : structuredClone(candidate),
78
+ );
79
+ }
80
+
81
+ export function closedTurns(events: readonly SessionEvent[]): ClosedTurn[] {
82
+ const result: ClosedTurn[] = [];
83
+ let current: Omit<ClosedTurn, "endSeq" | "closed"> | undefined;
84
+ for (const event of events) {
85
+ if (event.type === "turn/start") {
86
+ current = { turn: event.data.turn, startSeq: event.seq, users: [], assistants: [] };
87
+ continue;
88
+ }
89
+ if (current === undefined) continue;
90
+ if (event.type === "user/message" && event.data.source.kind === "user") {
91
+ // 轮内全部 user/message 都记录:agent 运行中 followup 追加的输入
92
+ // (第二条及之后)也是已落定文本,可编辑。
93
+ if (current.user === undefined) current.user = event;
94
+ current.users.push(event);
95
+ continue;
96
+ }
97
+ if (event.type === "assistant/message" && event.data.turn === current.turn) {
98
+ current.assistants.push(event);
99
+ continue;
100
+ }
101
+ if (event.type === "turn/end" && event.data.turn === current.turn) {
102
+ result.push({ ...current, endSeq: event.seq, closed: true });
103
+ current = undefined;
104
+ }
105
+ }
106
+ // 未闭合轮次(无 turn/end)也保留:user 消息已落定,编辑时 rewind 到该
107
+ // 消息(exclusive drop)重放即可。
108
+ if (current !== undefined) result.push({ ...current, closed: false });
109
+ return result;
110
+ }
111
+
112
+ export function editableMessages(turns: readonly ClosedTurn[]): EditableMessageBlock[] {
113
+ const result: EditableMessageBlock[] = [];
114
+ for (const turn of turns) {
115
+ for (const user of turn.users) {
116
+ for (const [blockIndex, block] of user.data.content.entries()) {
117
+ if (block.type !== "text") continue;
118
+ result.push({
119
+ key: `${String(user.seq)}:${String(blockIndex)}`,
120
+ turn: turn.turn,
121
+ eventSeq: user.seq,
122
+ blockIndex,
123
+ kind: "user",
124
+ text: block.text,
125
+ time: user.time,
126
+ });
127
+ }
128
+ }
129
+ // 未闭合轮次的助手消息是流式 partial,不可编辑。
130
+ if (!turn.closed) continue;
131
+ for (const event of turn.assistants) {
132
+ for (const [blockIndex, block] of event.data.message.content.entries()) {
133
+ if (!isTextualBlock(block)) continue;
134
+ result.push({
135
+ key: `${String(event.seq)}:${String(blockIndex)}`,
136
+ turn: turn.turn,
137
+ eventSeq: event.seq,
138
+ blockIndex,
139
+ kind: block.type === "reasoning" ? "assistant.reasoning" : "assistant.response",
140
+ text: block.text,
141
+ time: event.time,
142
+ });
143
+ }
144
+ }
145
+ }
146
+ return result;
147
+ }
148
+
149
+ export function retryableTurns(turns: readonly ClosedTurn[]): RetryableTurn[] {
150
+ return turns.flatMap((turn): RetryableTurn[] =>
151
+ // 未闭合轮次无已落定回复可重生成,不可重试。
152
+ turn.user === undefined || !turn.closed
153
+ ? []
154
+ : [
155
+ {
156
+ turn: turn.turn,
157
+ userEventSeq: turn.user.seq,
158
+ preview: userText(turn.user.data),
159
+ time: turn.user.time,
160
+ },
161
+ ],
162
+ );
163
+ }
164
+
165
+ export function downstreamUsers(turns: readonly ClosedTurn[], start: number): UserMessage[] {
166
+ return turns
167
+ .slice(start)
168
+ .flatMap((turn): UserMessage[] => turn.users.map((user) => cloneUser(user.data)));
169
+ }
170
+
171
+ function assistantReplacement(
172
+ event: SessionEvent<"assistant/message">,
173
+ blockIndex: number,
174
+ text: string,
175
+ ): AssistantMessage {
176
+ const replaced = replaceTextBlock(event.data.message.content, blockIndex, text).filter(
177
+ (block) => block.type === "text" || block.type === "reasoning",
178
+ );
179
+ return Object.freeze({
180
+ id: randomUUID(),
181
+ role: "assistant",
182
+ content: Object.freeze(replaced),
183
+ source: Object.freeze({
184
+ kind: "model",
185
+ provider: event.data.message.source.provider,
186
+ model: event.data.message.source.model,
187
+ }),
188
+ }) as AssistantMessage;
189
+ }
190
+
191
+ function pairVersionEffect(
192
+ sourceSessionId: SessionId,
193
+ effect: Omit<SessionBranchEffect, "id">,
194
+ ): SessionBranchVersionEvent {
195
+ return {
196
+ schemaVersion: SESSION_BRANCH_VERSION_SCHEMA,
197
+ effect: { ...effect, id: randomUUID() },
198
+ inverse: { kind: "restore-version", sessionId: sourceSessionId },
199
+ };
200
+ }
201
+
202
+ export function editPlan(operation: EditOperation, turns: readonly ClosedTurn[]): OperationPlan {
203
+ const turnIndex = turns.findIndex(
204
+ (turn) =>
205
+ operation.eventSeq > turn.startSeq &&
206
+ (turn.endSeq === undefined || operation.eventSeq < turn.endSeq),
207
+ );
208
+ const turn = turns[turnIndex];
209
+ if (turn === undefined)
210
+ throw new SessionBranchError("所选消息不属于已落定回合。", "INVALID_BOUNDARY");
211
+ const event =
212
+ turn.users.find((candidate) => candidate.seq === operation.eventSeq) ??
213
+ turn.assistants.find((candidate) => candidate.seq === operation.eventSeq);
214
+ if (event === undefined)
215
+ throw new SessionBranchError("所选消息不存在或不可编辑。", "INVALID_BOUNDARY");
216
+
217
+ if (event.type === "user/message") {
218
+ const before = event.data.content[operation.blockIndex];
219
+ if (before?.type !== "text")
220
+ throw new SessionBranchError("所选用户消息块不是文本。", "INVALID_BOUNDARY");
221
+ const edited = cloneUser(
222
+ event.data,
223
+ replaceTextBlock(event.data.content, operation.blockIndex, operation.text),
224
+ );
225
+ // 轮内后续 user/message(agent 运行中 followup 追加的输入)是已落定
226
+ // 输入:rewind 会 drop 它们,重放时保留(truncate 只截断回复,不丢输入)。
227
+ const userIndex = turn.users.findIndex((candidate) => candidate.seq === event.seq);
228
+ const sameTurnFollowups = turn.users.slice(userIndex + 1).map((user) => cloneUser(user.data));
229
+ const later = operation.cascade === "preserve" ? downstreamUsers(turns, turnIndex + 1) : [];
230
+ return {
231
+ anchorSeq: turn.startSeq,
232
+ // 轮首 user 编辑(闭合轮):整轮截断重放(rewind 到前一轮 turn/end);
233
+ // 其余情况(未闭合轮、轮内 followup):rewind 到该消息本身
234
+ // (exclusive drop),保留轮内已落定的前置输入与回复。
235
+ ...(turn.closed && userIndex === 0 ? {} : { rewindBoundary: event.seq }),
236
+ version: pairVersionEffect(operation.sessionId, {
237
+ operation: "edit",
238
+ cascade: operation.cascade,
239
+ targetTurn: turn.turn,
240
+ targetEventSeq: event.seq,
241
+ targetBlockIndex: operation.blockIndex,
242
+ blockKind: "user",
243
+ before: before.text,
244
+ after: operation.text,
245
+ }),
246
+ queuedUsers: [edited, ...sameTurnFollowups, ...later],
247
+ };
248
+ }
249
+
250
+ // 未闭合轮次的助手消息是流式 partial,无最终内容可编辑。
251
+ if (!turn.closed)
252
+ throw new SessionBranchError("未闭合轮次的助手消息不可编辑。", "INVALID_BOUNDARY");
253
+ const before = event.data.message.content[operation.blockIndex];
254
+ if (!isTextualBlock(before))
255
+ throw new SessionBranchError("所选助手消息块不是文本或思考。", "INVALID_BOUNDARY");
256
+ const blockKind: EditableBlockKind =
257
+ before.type === "reasoning" ? "assistant.reasoning" : "assistant.response";
258
+ if (turn.user === undefined)
259
+ throw new SessionBranchError("所选助手消息没有可重建的用户输入。", "INVALID_BOUNDARY");
260
+ return {
261
+ anchorSeq: turn.startSeq,
262
+ version: pairVersionEffect(operation.sessionId, {
263
+ operation: "edit",
264
+ cascade: operation.cascade,
265
+ targetTurn: turn.turn,
266
+ targetEventSeq: event.seq,
267
+ targetBlockIndex: operation.blockIndex,
268
+ blockKind,
269
+ before: before.text,
270
+ after: operation.text,
271
+ }),
272
+ manualTurn: {
273
+ turn: turn.turn,
274
+ user: cloneUser(turn.user.data),
275
+ assistant: assistantReplacement(event, operation.blockIndex, operation.text),
276
+ },
277
+ queuedUsers: operation.cascade === "preserve" ? downstreamUsers(turns, turnIndex + 1) : [],
278
+ };
279
+ }
280
+
281
+ export function retryPlan(operation: RetryOperation, turns: readonly ClosedTurn[]): OperationPlan {
282
+ const turnIndex = turns.findIndex((turn) => turn.turn === operation.turn);
283
+ const turn = turns[turnIndex];
284
+ if (turn?.user === undefined)
285
+ throw new SessionBranchError("所选回合没有可重放的用户输入。", "INVALID_BOUNDARY");
286
+ return {
287
+ anchorSeq: turn.startSeq,
288
+ version: pairVersionEffect(operation.sessionId, {
289
+ operation: "retry",
290
+ cascade: operation.cascade,
291
+ targetTurn: turn.turn,
292
+ targetEventSeq: turn.user.seq,
293
+ }),
294
+ queuedUsers:
295
+ operation.cascade === "preserve"
296
+ ? downstreamUsers(turns, turnIndex)
297
+ : turn.users.map((user) => cloneUser(user.data)),
298
+ };
299
+ }
300
+
301
+ export function rerollPlan(operation: RerollOperation, turns: readonly ClosedTurn[]): OperationPlan {
302
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
303
+ const turn = turns[index];
304
+ // 未闭合轮次无已落定的助手回复可重生成。
305
+ if (turn?.user === undefined || !turn.closed) continue;
306
+ const target = turn.assistants.findLast((event) =>
307
+ event.data.message.content.some(isTextualBlock),
308
+ );
309
+ if (target === undefined) continue;
310
+ return {
311
+ anchorSeq: turn.startSeq,
312
+ version: pairVersionEffect(operation.sessionId, {
313
+ operation: "reroll",
314
+ cascade: "truncate",
315
+ targetTurn: turn.turn,
316
+ targetEventSeq: target.seq,
317
+ }),
318
+ queuedUsers: turn.users.map((user) => cloneUser(user.data)),
319
+ };
320
+ }
321
+ throw new SessionBranchError("当前会话没有可重生成的已落定助手回复。", "INVALID_BOUNDARY");
322
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,151 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ import { TokenMeter } from "@deepseek-ai/dsh-token-meter";
3
+ import {
4
+ Session,
5
+ SessionId as SessionIdBrand,
6
+ SessionSeq,
7
+ SessionStore,
8
+ type SessionEvent,
9
+ type SessionHeader,
10
+ } from "@deepseek-ai/dsh-session";
11
+ import SessionProjectionRegistry from "@deepseek-ai/dsh-session-projection";
12
+ import { type BranchTimeline } from "@morlay/session-branch";
13
+ import SessionPersistenceSqlite from "@morlay/session-rdb";
14
+ import { parseJsonlArtifact } from "@morlay/session-rdb/artifact";
15
+ import { EmptySettings, meta, oneTurnLog } from "@morlay/session-rdb/testing";
16
+ import { SessionEditor } from "@morlay/ui-conversation-message-actions";
17
+
18
+ export {
19
+ BranchTimeline,
20
+ Session,
21
+ SessionIdBrand,
22
+ SessionSeq,
23
+ SessionStore,
24
+ TokenMeter,
25
+ EmptySettings,
26
+ meta,
27
+ oneTurnLog,
28
+ parseJsonlArtifact,
29
+ };
30
+ export type { SessionEvent, SessionHeader };
31
+
32
+ export interface Harness {
33
+ ctx: Context;
34
+ editor: SessionEditor;
35
+ dispose: () => Promise<void>;
36
+ }
37
+
38
+ export async function harness(): Promise<Harness> {
39
+ const ctx = new Context();
40
+ await ctx.plugin(EmptySettings);
41
+ await ctx.plugin(SessionStore);
42
+ new SessionProjectionRegistry(ctx);
43
+ const fiber = await ctx.plugin(SessionPersistenceSqlite, { type: "sqlite", path: ":memory:" });
44
+ await ctx.plugin(SessionEditor);
45
+ return { ctx, editor: ctx.sessionEditor, dispose: () => fiber.dispose() };
46
+ }
47
+
48
+ export function twoTurnLog(): SessionEvent[] {
49
+ const first = oneTurnLog();
50
+ const second: SessionEvent[] = oneTurnLog().map(
51
+ (event) =>
52
+ ({
53
+ ...event,
54
+ seq: event.seq + 6,
55
+ time: event.time + 100,
56
+ data: { ...event.data, turn: 2 },
57
+ }) as SessionEvent,
58
+ );
59
+ return [...first, ...second];
60
+ }
61
+
62
+ export async function createPersisted(
63
+ ctx: Context,
64
+ id: string,
65
+ events: readonly SessionEvent[],
66
+ header: SessionHeader = meta(id),
67
+ ): Promise<void> {
68
+ await ctx.sessionPersistence.create(header);
69
+ await ctx.sessionPersistence.append(SessionIdBrand(id), [...events]);
70
+ }
71
+
72
+ /** 构造一条 user/message 事件。 */
73
+ export function userMessage(
74
+ seq: number,
75
+ id: string,
76
+ text: string,
77
+ time = seq,
78
+ ): SessionEvent {
79
+ return {
80
+ type: "user/message",
81
+ seq: SessionSeq(seq),
82
+ time,
83
+ data: {
84
+ id,
85
+ role: "user",
86
+ content: [{ type: "text", text }],
87
+ source: { kind: "user" },
88
+ },
89
+ surfaceOp: "append",
90
+ } as SessionEvent;
91
+ }
92
+
93
+ /** 构造一条 assistant/message 事件(纯文本回复)。 */
94
+ export function assistantMessage(
95
+ seq: number,
96
+ turn: number,
97
+ step: number,
98
+ id: string,
99
+ text: string,
100
+ time = seq,
101
+ ): SessionEvent {
102
+ return {
103
+ type: "assistant/message",
104
+ seq: SessionSeq(seq),
105
+ time,
106
+ data: {
107
+ turn,
108
+ step,
109
+ message: {
110
+ id,
111
+ role: "assistant",
112
+ content: [{ type: "text", text }],
113
+ source: { kind: "model", provider: "mock", model: "mock" },
114
+ },
115
+ },
116
+ surfaceOp: "append",
117
+ } as SessionEvent;
118
+ }
119
+
120
+ /** 真实 agent-loop 形状的一轮:轮首输入 + 可选 followup,可闭合。 */
121
+ export function turnLog(
122
+ base: number,
123
+ turn: number,
124
+ opts: { users?: Array<{ id: string; text: string }>; closed?: boolean; time?: number } = {},
125
+ ): SessionEvent[] {
126
+ const { closed = true, time = base } = opts;
127
+ const users = opts.users ?? [{ id: `t${turn}-u1`, text: `turn ${turn} input` }];
128
+ const events: SessionEvent[] = [
129
+ { type: "turn/start", seq: SessionSeq(base), time, data: { turn } },
130
+ ];
131
+ let seq = base + 1;
132
+ let step = 1;
133
+ for (const user of users) {
134
+ events.push({ type: "step/start", seq: SessionSeq(seq++), time, data: { turn, step } });
135
+ events.push(userMessage(seq++, user.id, user.text, time));
136
+ events.push(
137
+ assistantMessage(seq++, turn, step, `${user.id}-a`, `answer to ${user.text}`, time),
138
+ );
139
+ events.push({ type: "step/end", seq: SessionSeq(seq++), time, data: { turn, step } });
140
+ step += 1;
141
+ }
142
+ if (closed) {
143
+ events.push({
144
+ type: "turn/end",
145
+ seq: SessionSeq(seq++),
146
+ time,
147
+ data: { turn, reason: { kind: "completed" } },
148
+ });
149
+ }
150
+ return events;
151
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":";;;;;UAGiB;EACf;EACA,WAAW;EAEX;EAEA;EAEA;EACA,SAAS;;UAGM;EACf;EACA,WAAW;;UAGI;EACf;EACA,WAAW;EACX;EACA,SAAS;;UAGM;EACf;EACA,WAAW;EACX;;UAGe;EACf;EACA,WAAW;EACX;EACA,iBAAiB;;KAGP,yBACR,gBACA,kBACA,iBACA,kBACA;UAEa;EACf,WAAW;EAEX;EAEA;;UAKe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;;;;;YCjBU;IACR,eAAe;;;UAIF;WACN,SAAS;EAClB,SAAS,SAAS;;UAGH;WACN,OAAO;EAChB,WAAW;;UAGI;EACf,IAAI,WAAW,YAAY;EAC3B,OAAO;IACL,YAAY;IACZ,gBAAgB;IAChB;MACE;MACA,gBAAgB;MAChB;MACA;;IAEF;MAAiB;MAAkB;MAAe;;MAChD,QAAQ;EAEZ,OAAO;IACL,iBAAiB;IACjB;MAAiB;MAAkB;MAAe;;MAChD,QAAQ;;UAGJ;EACR;EACA;EAEA;EAEA;EACA,OAAO;EACP,YAAY;;iBAGL,YAAY,iBAAiB,iBAAiB;iBAsE9C,iBAAiB,gBAAgB,eAAe;iBAqChD,eAAe,gBAAgB,eAAe;cAmR1C,sBAAsB;SAC1B;EAEP,YAAY,KAAK;EAOjB,iBACE,IAAI,WACJ,gBACA,2BACA,SAAS,cACR,QAAQ;EAIX,KACE,UAAU,WACV,gBACA,iBAAiB,WACjB;IAAS;IAAc;KACvB,SAAS,cACR,QAAQ;EAYX,OAAO,IAAI,WAAW,oBAAoB,SAAS,cAAW,uDAAA;EAI9D,SAAS,WAAW,WAAW,SAAS,cAAc,QAAQ;EAI9D,KAAK,WAAW,eAAe,SAAS,cAAc,QAAQ;EAI9D,OAAO,WAAW,iBAAiB,SAAS,cAAc,QAAQ;EAIlE,MAAM,WAAW,gBAAgB,SAAS,cAAc,QAAQ;EAI1D,iBACJ,WAAW,WACX,SAAS,cACR,QAAQ;EAKL,eAAe,WAAW,WAAW,SAAS,cAAc,QAAQ;UAK5D;UA0EA;UAiBA;UA+BA;;UA2CN;EACR;EACA;EACA,GAAG,eAAe,WAAW,OAAO;EACpC,GAAG,cAAc;EACjB,GAAG,gBAAgB,WAAW;;UAGtB;EACR,UAAU,gBAAgB,UAAU;EACpC,IAAI;;UAGI;EACR,SAAS;IACP;IACA;IACA,UAAU,SAAS,iBAAiB,UAAU,4BAA4B;;;;YAKlE;IACR,WAAW"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":["SESSION_BRANCH_VERSION_SCHEMA"],"sources":["../src/shared.ts","../src/index.ts"],"sourcesContent":["import type { SessionId } from \"@deepseek-ai/dsh-session\";\nimport type { BranchTimeline, CascadePolicy } from \"@morlay/session-branch\";\nimport type { VersionOperation } from \"@morlay/session-branch\";\n\nexport const SESSION_EDITOR_PATH = \"/session-editor\";\n\nexport type { VersionOperation } from \"@morlay/session-branch\";\n\nexport interface EditOperation {\n action: \"edit\";\n sessionId: SessionId;\n eventSeq: number;\n blockIndex: number;\n text: string;\n cascade: CascadePolicy;\n}\n\nexport interface RerollOperation {\n action: \"reroll\";\n sessionId: SessionId;\n}\n\nexport interface RetryOperation {\n action: \"retry\";\n sessionId: SessionId;\n turn: number;\n cascade: CascadePolicy;\n}\n\nexport interface RewindOperation {\n action: \"rewind\";\n sessionId: SessionId;\n toBoundary: number;\n}\n\nexport interface ForkOperation {\n action: \"fork\";\n sessionId: SessionId;\n atSeq?: number;\n childSessionId?: SessionId;\n}\n\nexport type SessionEditorOperation =\n | EditOperation\n | RerollOperation\n | RetryOperation\n | RewindOperation\n | ForkOperation;\n\nexport interface SessionEditorOperationResult {\n sessionId: SessionId;\n queuedTurns: number;\n\n live?: boolean;\n}\n\nexport interface EditableMessageBlock {\n key: string;\n turn: number;\n eventSeq: number;\n blockIndex: number;\n kind: \"user\" | \"assistant.reasoning\" | \"assistant.response\";\n text: string;\n time: number;\n}\n\nexport interface RetryableTurn {\n turn: number;\n userEventSeq: number;\n preview: string;\n time: number;\n}\n\nexport interface VersionSummary {\n sessionId: string;\n parentSessionId?: string;\n effectId?: string;\n inverseSessionId?: string;\n createdAt: number;\n depth: number;\n current: boolean;\n onCurrentEffectPath: boolean;\n operation?: VersionOperation;\n cascade?: CascadePolicy;\n targetTurn?: number;\n blockKind?: EditableMessageBlock[\"kind\"];\n before?: string;\n after?: string;\n}\n\nexport interface SessionEditorTimeline {\n sessionId: string;\n messages: EditableMessageBlock[];\n retryableTurns: RetryableTurn[];\n versions: VersionSummary[];\n\n undoStack: string[];\n\n redoSessionIds: string[];\n}\n\nexport function toTimelinePayload(\n sessionId: SessionId,\n timeline: BranchTimeline,\n messages: EditableMessageBlock[],\n retryableTurns: RetryableTurn[],\n): SessionEditorTimeline {\n const currentPath = new Set<string>();\n for (const node of timeline.nodes) currentPath.add(String(node.sessionId));\n const versions: VersionSummary[] = timeline.nodes.map((node) => ({\n sessionId: String(node.sessionId),\n ...(node.parentSessionId === undefined\n ? {}\n : { parentSessionId: String(node.parentSessionId) }),\n ...(node.effect === undefined\n ? {}\n : {\n effectId: node.effect.id,\n inverseSessionId: String(node.inverseSessionId),\n operation: node.effect.operation,\n cascade: node.effect.cascade,\n targetTurn: node.effect.targetTurn,\n ...(node.effect.blockKind === undefined ? {} : { blockKind: node.effect.blockKind }),\n ...(node.effect.before === undefined ? {} : { before: node.effect.before }),\n ...(node.effect.after === undefined ? {} : { after: node.effect.after }),\n }),\n createdAt: node.createdAt,\n depth: depthOf(timeline, node.sessionId),\n current: String(node.sessionId) === String(sessionId),\n onCurrentEffectPath: currentPath.has(String(node.sessionId)),\n }));\n const versionsById = new Map(versions.map((version) => [version.sessionId, version]));\n const undoStack: string[] = [];\n let cursor = versionsById.get(String(sessionId));\n while (cursor?.inverseSessionId !== undefined) {\n if (undoStack.includes(cursor.inverseSessionId)) break;\n undoStack.push(cursor.inverseSessionId);\n cursor = versionsById.get(cursor.inverseSessionId);\n }\n const redoSessionIds = versions\n .filter((version) => version.inverseSessionId === String(sessionId))\n .map((version) => version.sessionId);\n return {\n sessionId: String(sessionId),\n messages,\n retryableTurns,\n versions,\n undoStack,\n redoSessionIds,\n };\n}\n\nfunction depthOf(\n timeline: BranchTimeline,\n sessionId: import(\"@deepseek-ai/dsh-session\").SessionId,\n): number {\n const byId = new Map(timeline.nodes.map((node) => [String(node.sessionId), node]));\n let depth = 0;\n let cursor = byId.get(String(sessionId));\n const seen = new Set<string>();\n while (cursor?.parentSessionId !== undefined && !seen.has(String(cursor.sessionId))) {\n seen.add(String(cursor.sessionId));\n depth += 1;\n cursor = byId.get(String(cursor.parentSessionId));\n }\n return depth;\n}\n","import { Service, type Context } from \"@deepseek-ai/cordis\";\nimport type { AssistantMessage, ContentBlock, UserMessage } from \"@deepseek-ai/dsh-llm\";\nimport type {\n Session,\n SessionEvent,\n SessionId,\n SurfaceEventType,\n SurfaceIntent,\n} from \"@deepseek-ai/dsh-session\";\nimport { randomUUID } from \"node:crypto\";\nimport {\n SESSION_BRANCH_VERSION_SCHEMA,\n SessionBranchError,\n balanceRewindPrefix,\n type BranchBoundary,\n type BranchTimeline,\n type EditableBlockKind,\n type SessionBranchEffect,\n type SessionBranchVersionEvent,\n} from \"@morlay/session-branch\";\nimport type {\n EditOperation,\n EditableMessageBlock,\n RerollOperation,\n RetryOperation,\n RetryableTurn,\n SessionEditorResult,\n} from \"./types.ts\";\nimport {\n SESSION_EDITOR_PATH,\n type SessionEditorOperation,\n type SessionEditorOperationResult,\n type SessionEditorTimeline,\n toTimelinePayload,\n} from \"./shared.ts\";\n\nexport type { CascadePolicy, EditableBlockKind } from \"@morlay/session-branch\";\nexport type {\n EditableMessageBlock,\n EditOperation,\n RerollOperation,\n RetryOperation,\n RetryableTurn,\n RewindOperation,\n SessionEditorOperation,\n SessionEditorResult,\n} from \"./types.ts\";\n\nexport { closedTurns, editableMessages, retryableTurns };\nexport { SESSION_BRANCH_VERSION_SCHEMA } from \"@morlay/session-branch\";\n\nexport type { BranchTimeline, SessionBranchVersionEvent } from \"@morlay/session-branch\";\n\ndeclare module \"@deepseek-ai/cordis\" {\n interface Context {\n sessionEditor: SessionEditor;\n }\n}\n\nexport interface EditorAgent {\n readonly session: Session;\n followup(message: UserMessage): void;\n}\n\nexport interface EditorAgentHandle {\n readonly agent: EditorAgent;\n dispose(): Promise<void>;\n}\n\nexport interface EditorAgentRegistry {\n get(sessionId: SessionId): EditorAgent | undefined;\n create(options: {\n sessionId?: SessionId;\n seed?: readonly SessionEvent[];\n meta?: {\n cwd?: string;\n parentSession?: SessionId;\n seedLength?: number;\n agentPreset?: string;\n };\n agentOptions?: { provider: string; model: string; maxTokens?: number };\n }): Promise<EditorAgentHandle>;\n\n resume(options: {\n resumeSessionId: SessionId;\n agentOptions?: { provider: string; model: string; maxTokens?: number };\n }): Promise<EditorAgentHandle>;\n}\n\ninterface ClosedTurn {\n turn: number;\n startSeq: number;\n\n endSeq?: number;\n\n closed: boolean;\n user?: SessionEvent<\"user/message\">;\n assistants: SessionEvent<\"assistant/message\">[];\n}\n\nfunction closedTurns(events: readonly SessionEvent[]): ClosedTurn[] {\n const result: ClosedTurn[] = [];\n let current: Omit<ClosedTurn, \"endSeq\" | \"closed\"> | undefined;\n for (const event of events) {\n if (event.type === \"turn/start\") {\n current = { turn: event.data.turn, startSeq: event.seq, assistants: [] };\n continue;\n }\n if (current === undefined) continue;\n if (\n event.type === \"user/message\" &&\n current.user === undefined &&\n event.data.source.kind === \"user\"\n ) {\n current.user = event;\n continue;\n }\n if (event.type === \"assistant/message\" && event.data.turn === current.turn) {\n current.assistants.push(event);\n continue;\n }\n if (event.type === \"turn/end\" && event.data.turn === current.turn) {\n result.push({ ...current, endSeq: event.seq, closed: true });\n current = undefined;\n }\n }\n // 未闭合轮次(无 turn/end)也保留:user 消息已落定,编辑时 rewind 到该\n // 消息(exclusive drop)重放即可。\n if (current !== undefined) result.push({ ...current, closed: false });\n return result;\n}\n\nfunction isTextualBlock(\n block: ContentBlock | undefined,\n): block is Extract<ContentBlock, { type: \"text\" | \"reasoning\" }> {\n return block?.type === \"text\" || block?.type === \"reasoning\";\n}\n\nfunction userText(message: UserMessage): string {\n return message.content\n .filter((block): block is Extract<ContentBlock, { type: \"text\" }> => block.type === \"text\")\n .map((block) => block.text)\n .join(\"\\n\");\n}\n\nfunction cloneUser(\n message: UserMessage,\n content: ContentBlock[] = structuredClone(message.content),\n): UserMessage {\n return Object.freeze({\n id: randomUUID(),\n role: \"user\",\n content: Object.freeze(content),\n source: Object.freeze({ kind: \"user\" }),\n }) as UserMessage;\n}\n\nfunction replaceTextBlock(\n content: readonly ContentBlock[],\n blockIndex: number,\n text: string,\n): ContentBlock[] {\n const block = content[blockIndex];\n if (!isTextualBlock(block))\n throw new SessionBranchError(\"所选内容块不是可编辑文本。\", \"INVALID_BOUNDARY\");\n return content.map((candidate, index) =>\n index === blockIndex ? ({ ...candidate, text } as ContentBlock) : structuredClone(candidate),\n );\n}\n\nfunction editableMessages(turns: readonly ClosedTurn[]): EditableMessageBlock[] {\n const result: EditableMessageBlock[] = [];\n for (const turn of turns) {\n if (turn.user !== undefined) {\n for (const [blockIndex, block] of turn.user.data.content.entries()) {\n if (block.type !== \"text\") continue;\n result.push({\n key: `${String(turn.user.seq)}:${String(blockIndex)}`,\n turn: turn.turn,\n eventSeq: turn.user.seq,\n blockIndex,\n kind: \"user\",\n text: block.text,\n time: turn.user.time,\n });\n }\n }\n // 未闭合轮次的助手消息是流式 partial,不可编辑。\n if (!turn.closed) continue;\n for (const event of turn.assistants) {\n for (const [blockIndex, block] of event.data.message.content.entries()) {\n if (!isTextualBlock(block)) continue;\n result.push({\n key: `${String(event.seq)}:${String(blockIndex)}`,\n turn: turn.turn,\n eventSeq: event.seq,\n blockIndex,\n kind: block.type === \"reasoning\" ? \"assistant.reasoning\" : \"assistant.response\",\n text: block.text,\n time: event.time,\n });\n }\n }\n }\n return result;\n}\n\nfunction retryableTurns(turns: readonly ClosedTurn[]): RetryableTurn[] {\n return turns.flatMap((turn): RetryableTurn[] =>\n // 未闭合轮次无已落定回复可重生成,不可重试。\n turn.user === undefined || !turn.closed\n ? []\n : [\n {\n turn: turn.turn,\n userEventSeq: turn.user.seq,\n preview: userText(turn.user.data),\n time: turn.user.time,\n },\n ],\n );\n}\n\nfunction downstreamUsers(turns: readonly ClosedTurn[], start: number): UserMessage[] {\n return turns\n .slice(start)\n .flatMap((turn): UserMessage[] => (turn.user === undefined ? [] : [cloneUser(turn.user.data)]));\n}\n\nfunction assistantReplacement(\n event: SessionEvent<\"assistant/message\">,\n blockIndex: number,\n text: string,\n): AssistantMessage {\n const replaced = replaceTextBlock(event.data.message.content, blockIndex, text).filter(\n (block) => block.type === \"text\" || block.type === \"reasoning\",\n );\n return Object.freeze({\n id: randomUUID(),\n role: \"assistant\",\n content: Object.freeze(replaced),\n source: Object.freeze({\n kind: \"model\",\n provider: event.data.message.source.provider,\n model: event.data.message.source.model,\n }),\n }) as AssistantMessage;\n}\n\ninterface OperationPlan {\n anchorSeq: number;\n\n rewindBoundary?: number;\n version: SessionBranchVersionEvent;\n\n manualTurn?: { turn: number; user: UserMessage; assistant: AssistantMessage };\n\n queuedUsers: UserMessage[];\n}\n\nfunction pairVersionEffect(\n sourceSessionId: SessionId,\n effect: Omit<SessionBranchEffect, \"id\">,\n): SessionBranchVersionEvent {\n return {\n schemaVersion: SESSION_BRANCH_VERSION_SCHEMA,\n effect: { ...effect, id: randomUUID() },\n inverse: { kind: \"restore-version\", sessionId: sourceSessionId },\n };\n}\n\nfunction editPlan(operation: EditOperation, turns: readonly ClosedTurn[]): OperationPlan {\n const turnIndex = turns.findIndex(\n (turn) =>\n operation.eventSeq > turn.startSeq &&\n (turn.endSeq === undefined || operation.eventSeq < turn.endSeq),\n );\n const turn = turns[turnIndex];\n if (turn === undefined)\n throw new SessionBranchError(\"所选消息不属于已落定回合。\", \"INVALID_BOUNDARY\");\n const event =\n turn.user?.seq === operation.eventSeq\n ? turn.user\n : turn.assistants.find((candidate) => candidate.seq === operation.eventSeq);\n if (event === undefined)\n throw new SessionBranchError(\"所选消息不存在或不可编辑。\", \"INVALID_BOUNDARY\");\n\n if (event.type === \"user/message\") {\n const before = event.data.content[operation.blockIndex];\n if (before?.type !== \"text\")\n throw new SessionBranchError(\"所选用户消息块不是文本。\", \"INVALID_BOUNDARY\");\n const edited = cloneUser(\n event.data,\n replaceTextBlock(event.data.content, operation.blockIndex, operation.text),\n );\n const later = operation.cascade === \"preserve\" ? downstreamUsers(turns, turnIndex + 1) : [];\n return {\n anchorSeq: turn.startSeq,\n // 未闭合轮次:rewind 到该 user 消息(exclusive drop),由编辑版替换;\n // 闭合轮次走前一轮 turn/end 的 inclusive 语义。\n ...(turn.closed ? {} : { rewindBoundary: event.seq }),\n version: pairVersionEffect(operation.sessionId, {\n operation: \"edit\",\n cascade: operation.cascade,\n targetTurn: turn.turn,\n targetEventSeq: event.seq,\n targetBlockIndex: operation.blockIndex,\n blockKind: \"user\",\n before: before.text,\n after: operation.text,\n }),\n queuedUsers: [edited, ...later],\n };\n }\n\n // 未闭合轮次的助手消息是流式 partial,无最终内容可编辑。\n if (!turn.closed)\n throw new SessionBranchError(\"未闭合轮次的助手消息不可编辑。\", \"INVALID_BOUNDARY\");\n const before = event.data.message.content[operation.blockIndex];\n if (!isTextualBlock(before))\n throw new SessionBranchError(\"所选助手消息块不是文本或思考。\", \"INVALID_BOUNDARY\");\n const blockKind: EditableBlockKind =\n before.type === \"reasoning\" ? \"assistant.reasoning\" : \"assistant.response\";\n if (turn.user === undefined)\n throw new SessionBranchError(\"所选助手消息没有可重建的用户输入。\", \"INVALID_BOUNDARY\");\n return {\n anchorSeq: turn.startSeq,\n version: pairVersionEffect(operation.sessionId, {\n operation: \"edit\",\n cascade: operation.cascade,\n targetTurn: turn.turn,\n targetEventSeq: event.seq,\n targetBlockIndex: operation.blockIndex,\n blockKind,\n before: before.text,\n after: operation.text,\n }),\n manualTurn: {\n turn: turn.turn,\n user: cloneUser(turn.user.data),\n assistant: assistantReplacement(event, operation.blockIndex, operation.text),\n },\n queuedUsers: operation.cascade === \"preserve\" ? downstreamUsers(turns, turnIndex + 1) : [],\n };\n}\n\nfunction retryPlan(operation: RetryOperation, turns: readonly ClosedTurn[]): OperationPlan {\n const turnIndex = turns.findIndex((turn) => turn.turn === operation.turn);\n const turn = turns[turnIndex];\n if (turn?.user === undefined)\n throw new SessionBranchError(\"所选回合没有可重放的用户输入。\", \"INVALID_BOUNDARY\");\n return {\n anchorSeq: turn.startSeq,\n version: pairVersionEffect(operation.sessionId, {\n operation: \"retry\",\n cascade: operation.cascade,\n targetTurn: turn.turn,\n targetEventSeq: turn.user.seq,\n }),\n queuedUsers:\n operation.cascade === \"preserve\"\n ? downstreamUsers(turns, turnIndex)\n : [cloneUser(turn.user.data)],\n };\n}\n\nfunction rerollPlan(operation: RerollOperation, turns: readonly ClosedTurn[]): OperationPlan {\n for (let index = turns.length - 1; index >= 0; index -= 1) {\n const turn = turns[index];\n // 未闭合轮次无已落定的助手回复可重生成。\n if (turn?.user === undefined || !turn.closed) continue;\n const target = turn.assistants.findLast((event) =>\n event.data.message.content.some(isTextualBlock),\n );\n if (target === undefined) continue;\n return {\n anchorSeq: turn.startSeq,\n version: pairVersionEffect(operation.sessionId, {\n operation: \"reroll\",\n cascade: \"truncate\",\n targetTurn: turn.turn,\n targetEventSeq: target.seq,\n }),\n queuedUsers: [cloneUser(turn.user.data)],\n };\n }\n throw new SessionBranchError(\"当前会话没有可重生成的已落定助手回复。\", \"INVALID_BOUNDARY\");\n}\n\nfunction appendLogSeedEvent(\n events: SessionEvent[],\n type: string,\n data: unknown,\n ignorable = false,\n): void {\n events.push({\n type: type as SessionEvent[\"type\"],\n seq: events.length,\n time: Date.now(),\n data: data as SessionEvent[\"data\"],\n ...(ignorable ? { ignorable: true as const } : {}),\n } as SessionEvent);\n}\n\nfunction appendSurfaceSeedEvent<T extends SurfaceEventType>(\n events: SessionEvent[],\n type: T,\n data: import(\"@deepseek-ai/dsh-session\").SessionEvent<T>[\"data\"],\n intent: SurfaceIntent,\n): void {\n events.push({\n type,\n seq: events.length,\n time: Date.now(),\n data,\n surfaceOp: intent.surfaceOp,\n ...(intent.sourceEventSeqs === undefined ? {} : { sourceEventSeqs: intent.sourceEventSeqs }),\n } as SessionEvent<T>);\n}\n\nfunction appendManualTurn(\n events: SessionEvent[],\n manual: { turn: number; user: UserMessage; assistant: AssistantMessage },\n): void {\n const { turn, user, assistant } = manual;\n appendLogSeedEvent(events, \"turn/start\", { turn });\n appendSurfaceSeedEvent(events, \"user/message\", user, { surfaceOp: \"append\" });\n appendLogSeedEvent(events, \"step/start\", { turn, step: 1 });\n appendSurfaceSeedEvent(\n events,\n \"assistant/message\",\n { turn, step: 1, message: assistant },\n {\n surfaceOp: \"append\",\n sourceEventSeqs: [],\n },\n );\n appendLogSeedEvent(events, \"step/end\", { turn, step: 1 });\n appendLogSeedEvent(events, \"turn/end\", {\n turn,\n reason: { kind: \"completed\" },\n });\n}\n\nfunction appendSeedSuffixLive(session: Session, seedSuffix: readonly SessionEvent[]): void {\n for (const event of seedSuffix) {\n // 版本效果事件携带 ignorable 标记(上游类型无此字段,duck-type 读取)。\n const ignorable = (event as { ignorable?: boolean }).ignorable === true;\n if (ignorable) {\n const s = session as unknown as {\n log: SessionEvent[];\n eventsSnapshot?: unknown;\n };\n // ignorable 语义:live log 保留、不落 canonical log。session.append 不\n // 保留 ignorable 标记,因此直接 push 内存 log(不发布);seq 按 log\n // 续接重编号(seedSuffix 内部编号从 0 起)。\n s.log.push({ ...event, seq: s.log.length } as SessionEvent);\n s.eventsSnapshot = undefined;\n continue;\n }\n const s = session as unknown as {\n append(\n type: string,\n data: unknown,\n opts?: { surfaceOp?: unknown; sourceEventSeqs?: readonly number[] },\n ): SessionEvent;\n };\n const raw = event as SessionEvent & {\n surfaceOp?: unknown;\n sourceEventSeqs?: readonly number[];\n };\n if (raw.surfaceOp !== undefined) {\n s.append(event.type, event.data, {\n surfaceOp: raw.surfaceOp,\n ...(raw.sourceEventSeqs === undefined ? {} : { sourceEventSeqs: raw.sourceEventSeqs }),\n });\n } else {\n s.append(event.type, event.data);\n }\n }\n}\n\nexport class SessionEditor extends Service {\n static inject = [\"sessionBranch\", \"sessionPersistence\", \"sessions\"];\n\n constructor(ctx: Context) {\n super(ctx, \"sessionEditor\");\n // HTTP 路由随类构造注册(dsh 用 default 类插件,apply 不被调用);\n // bundles 顺序保证 webserver 先于本类实例化。\n registerHttpRoutes(ctx);\n }\n\n readBranchPrefix(\n id: SessionId,\n atSeq?: number,\n mode?: \"after\" | \"before\",\n signal?: AbortSignal,\n ): Promise<BranchBoundary> {\n return this.ctx.sessionBranch.readBranchPrefix(id, atSeq, mode, signal);\n }\n\n fork(\n sourceId: SessionId,\n atSeq?: number,\n childSessionId?: SessionId,\n meta?: { cwd?: string; agentPreset?: string },\n signal?: AbortSignal,\n ): Promise<SessionId> {\n return this.ctx.sessionBranch.forkFrom(\n sourceId,\n {\n ...(atSeq === undefined ? {} : { atSeq }),\n ...(childSessionId === undefined ? {} : { childSessionId }),\n ...(meta === undefined ? {} : { meta }),\n },\n signal,\n );\n }\n\n rewind(id: SessionId, toBoundary: number, signal?: AbortSignal) {\n return this.ctx.sessionBranch.rewind(id, toBoundary, signal);\n }\n\n timeline(sessionId: SessionId, signal?: AbortSignal): Promise<BranchTimeline> {\n return this.ctx.sessionBranch.timeline(sessionId, signal);\n }\n\n edit(operation: EditOperation, signal?: AbortSignal): Promise<SessionEditorResult> {\n return this.branchOperation(operation, signal);\n }\n\n reroll(operation: RerollOperation, signal?: AbortSignal): Promise<SessionEditorResult> {\n return this.branchOperation(operation, signal);\n }\n\n retry(operation: RetryOperation, signal?: AbortSignal): Promise<SessionEditorResult> {\n return this.branchOperation(operation, signal);\n }\n\n async editableMessages(\n sessionId: SessionId,\n signal?: AbortSignal,\n ): Promise<EditableMessageBlock[]> {\n const events = await this.readEvents(sessionId, signal);\n return editableMessages(closedTurns(events));\n }\n\n async retryableTurns(sessionId: SessionId, signal?: AbortSignal): Promise<RetryableTurn[]> {\n const events = await this.readEvents(sessionId, signal);\n return retryableTurns(closedTurns(events));\n }\n\n private async branchOperation(\n operation: EditOperation | RerollOperation | RetryOperation,\n signal?: AbortSignal,\n ): Promise<SessionEditorResult> {\n signal?.throwIfAborted();\n const events = await this.readEvents(operation.sessionId, signal);\n const turns = closedTurns(events);\n const plan =\n operation.action === \"edit\"\n ? editPlan(operation, turns)\n : operation.action === \"retry\"\n ? retryPlan(operation, turns)\n : rerollPlan(operation, turns);\n // rewind 前解析模型配置:就地编辑可能截断最后的 request/header\n // (编辑第一轮 boundary = -1 清空全部),重放 agent 需要 provider/model。\n const headerConfig = events.findLast((event) => event.type === \"request/header\")?.data.header\n .config;\n\n // 派生 seed 后缀:版本效果 + 可选手工回合。版本事件对核心是 ignorable,\n // 保证非 branch 读者可安全跳过。\n const seedSuffix: SessionEvent[] = [];\n appendLogSeedEvent(seedSuffix, \"session-branch/version\", plan.version, true);\n if (plan.manualTurn !== undefined) appendManualTurn(seedSuffix, plan.manualTurn);\n\n // 就地编辑:不创建新会话、不改变 id。先 rewind 截断到目标轮之前的闭合\n // 边界,再 append 版本效果与重放输入,最后(可选)驱动 agent 重放。\n const turnIndex = turns.findIndex((turn) => turn.startSeq === plan.anchorSeq);\n const boundary =\n plan.rewindBoundary !== undefined\n ? plan.rewindBoundary\n : turnIndex <= 0\n ? -1\n : turns[turnIndex - 1]!.endSeq!;\n const live = this.ctx.sessions.get(operation.sessionId);\n await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);\n if (seedSuffix.length > 0) {\n if (live !== undefined) {\n // live:版本效果 push 内存 log(ignorable 保留)、manualTurn 走 append,\n // 同步 cursor 后显式 flush(push 不发布、不进缓冲,cursor 会落后)。\n appendSeedSuffixLive(live, seedSuffix);\n this.ctx.sessionBranch.syncLiveCursor(operation.sessionId);\n await this.ctx.sessions.flush(live);\n } else {\n // cold:续写 seq 从平衡后的保留前缀接续(exclusive 截断可能残留\n // 孤儿 step/start,落盘 log 对 token meter 重放非法)。\n const rawKeepLength = boundary + (plan.rewindBoundary === undefined ? 1 : 0);\n const keepLength = balanceRewindPrefix(events.slice(0, rawKeepLength)).length;\n const renumbered = seedSuffix.map(\n (event, index) =>\n ({\n ...event,\n seq: keepLength + index,\n }) as SessionEvent,\n );\n await this.ctx.sessionPersistence.append(operation.sessionId, renumbered);\n }\n }\n\n // 可选增强:agent 驱动(重放排队用户输入)。缺失 agents 服务时退化为\n // 已 durable 的就地版本。\n const queuedTurns = await this.driveAgent(\n operation.sessionId,\n plan.queuedUsers,\n signal,\n headerConfig,\n );\n return {\n sessionId: operation.sessionId,\n queuedTurns,\n // 操作后是否仍有 live owner(driveAgent 可能 resume 出 agent)。\n live: this.ctx.sessions.get(operation.sessionId) !== undefined,\n };\n }\n\n private async readEvents(\n sessionId: SessionId,\n signal?: AbortSignal,\n ): Promise<readonly SessionEvent[]> {\n const live = this.ctx.sessions.get(sessionId);\n if (live !== undefined) return live.snapshotEvents();\n // cold:读原始事件(loadStored 不补 closers)——inspect 会把未闭合 log\n // 补成闭合,编辑未闭合轮次的 user 消息会走错边界。\n const branch = this.ctx.sessionBranch as unknown as {\n readRawEvents(\n id: SessionId,\n signal?: AbortSignal,\n ): Promise<{ meta: unknown; events: readonly SessionEvent[] }>;\n };\n return (await branch.readRawEvents(sessionId, signal)).events;\n }\n\n private async driveAgent(\n sessionId: SessionId,\n queuedUsers: readonly UserMessage[],\n signal?: AbortSignal,\n headerConfig?: { provider?: string; model?: string; maxTokens?: number },\n ): Promise<number> {\n if (queuedUsers.length === 0) return 0;\n signal?.throwIfAborted();\n const agents = this.ctx.get(\"agents\") as EditorAgentRegistry | undefined;\n if (agents === undefined) return 0;\n const provider = headerConfig?.provider ?? \"\";\n const model = headerConfig?.model ?? \"\";\n if (provider.length === 0 || model.length === 0) {\n // 兜底:从当前会话 events 解析(headerConfig 未提供时)。\n const events = await this.readEvents(sessionId, signal);\n const config = events.findLast((event) => event.type === \"request/header\")?.data.header\n .config;\n const fallbackProvider = config?.provider ?? \"\";\n const fallbackModel = config?.model ?? \"\";\n if (fallbackProvider.length === 0 || fallbackModel.length === 0) return 0;\n return this.queueThroughAgent(\n sessionId,\n queuedUsers,\n signal,\n fallbackProvider,\n fallbackModel,\n );\n }\n return this.queueThroughAgent(sessionId, queuedUsers, signal, provider, model);\n }\n\n private async queueThroughAgent(\n sessionId: SessionId,\n queuedUsers: readonly UserMessage[],\n signal: AbortSignal | undefined,\n provider: string,\n model: string,\n ): Promise<number> {\n const agents = this.ctx.get(\"agents\") as EditorAgentRegistry | undefined;\n if (agents === undefined) return 0;\n // 现有 live agent:直接排队输入(其 session 内存已被 rewind 截断,\n // followup 基于截断后历史重放,不改 id、不重建)。\n const existing = agents.get(sessionId);\n if (existing !== undefined) {\n for (const message of queuedUsers) existing.followup(message);\n await this.ctx.sessions.flush(existing.session);\n return queuedUsers.length;\n }\n // cold:resume 已持久化会话(create 会因「已存在持久化日志」失败)。\n // resume 后 agent 驻留,不 dispose(dispose 会破坏客户端打开的窗口)。\n const handle = await agents\n .resume({ resumeSessionId: sessionId, agentOptions: { provider, model } })\n .catch((error: unknown) => {\n // agent 组合失败不应使已 durable 的版本失效:退化为持久化版本。\n this.ctx.logger.warn(\n \"session-editor: agent resume failed (%s); version remains durable\",\n String(error),\n );\n return undefined;\n });\n if (handle === undefined) return 0;\n for (const message of queuedUsers) handle.agent.followup(message);\n await this.ctx.sessions.flush(handle.agent.session);\n return queuedUsers.length;\n }\n}\n\nexport default SessionEditor;\n\n// ---------------------------------------------------------------------------\n// HTTP 面(host):GET /session-editor(timeline 投影)/ POST /session-editor\n// (edit | reroll | retry | rewind | fork)。\n// ---------------------------------------------------------------------------\n\ninterface HttpRequestLike {\n method?: string;\n url?: string;\n on(event: \"data\", listener: (chunk: Uint8Array | string) => void): this;\n on(event: \"end\", listener: () => void): this;\n on(event: \"error\", listener: (error: unknown) => void): this;\n}\n\ninterface HttpResponseLike {\n writeHead(status: number, headers?: Record<string, string>): unknown;\n end(body?: string): void;\n}\n\ninterface HttpServerLike {\n register(route: {\n kind: \"exact\";\n path: string;\n handler: (request: HttpRequestLike, response: HttpResponseLike) => void | Promise<void>;\n }): () => void;\n}\n\ndeclare module \"@deepseek-ai/cordis\" {\n interface Context {\n webServer: HttpServerLike;\n }\n}\n\nfunction objectValue(value: unknown): Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new TypeError(\"请求体必须是 JSON 对象。\");\n }\n return value as Record<string, unknown>;\n}\n\nfunction sessionIdOf(value: unknown): SessionId {\n if (typeof value !== \"string\" || value.length === 0)\n throw new TypeError(\"sessionId 必须是非空字符串。\");\n return value as SessionId;\n}\n\nfunction integerOf(value: unknown, name: string): number {\n if (!Number.isSafeInteger(value) || (value as number) < 0) {\n throw new TypeError(`${name} 必须是非负安全整数。`);\n }\n return value as number;\n}\n\nfunction cascadeOf(value: unknown): import(\"@morlay/session-branch\").CascadePolicy {\n if (value !== \"truncate\" && value !== \"preserve\")\n throw new TypeError(\"cascade 必须是 truncate 或 preserve。\");\n return value;\n}\n\nfunction decodeOperation(value: unknown): SessionEditorOperation {\n const record = objectValue(value);\n const sessionId = sessionIdOf(record[\"sessionId\"]);\n switch (record[\"action\"]) {\n case \"edit\":\n if (typeof record[\"text\"] !== \"string\") throw new TypeError(\"text 必须是字符串。\");\n return {\n action: \"edit\",\n sessionId,\n eventSeq: integerOf(record[\"eventSeq\"], \"eventSeq\"),\n blockIndex: integerOf(record[\"blockIndex\"], \"blockIndex\"),\n text: record[\"text\"],\n cascade: cascadeOf(record[\"cascade\"]),\n };\n case \"reroll\":\n return { action: \"reroll\", sessionId };\n case \"retry\":\n return {\n action: \"retry\",\n sessionId,\n turn: integerOf(record[\"turn\"], \"turn\"),\n cascade: cascadeOf(record[\"cascade\"]),\n };\n case \"rewind\":\n return {\n action: \"rewind\",\n sessionId,\n toBoundary: integerOf(record[\"toBoundary\"], \"toBoundary\"),\n };\n case \"fork\":\n return {\n action: \"fork\",\n sessionId,\n ...(record[\"atSeq\"] === undefined ? {} : { atSeq: integerOf(record[\"atSeq\"], \"atSeq\") }),\n ...(record[\"childSessionId\"] === undefined\n ? {}\n : { childSessionId: sessionIdOf(record[\"childSessionId\"]) }),\n };\n default:\n throw new TypeError(\"action 必须是 edit、reroll、retry、rewind 或 fork。\");\n }\n}\n\nfunction requestJson(request: HttpRequestLike): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const decoder = new TextDecoder();\n let text = \"\";\n request.on(\"data\", (chunk) => {\n text += typeof chunk === \"string\" ? chunk : decoder.decode(chunk, { stream: true });\n });\n request.on(\"end\", () => {\n try {\n text += decoder.decode();\n resolve(JSON.parse(text) as unknown);\n } catch (error) {\n reject(error);\n }\n });\n request.on(\"error\", reject);\n });\n}\n\nfunction respondJson(response: HttpResponseLike, status: number, value: unknown): void {\n response.writeHead(status, {\n \"content-type\": \"application/json; charset=utf-8\",\n \"cache-control\": \"no-store\",\n });\n response.end(JSON.stringify(value));\n}\n\nasync function readTimeline(\n editor: SessionEditor,\n sessionId: SessionId,\n): Promise<SessionEditorTimeline> {\n const timeline = await editor.timeline(sessionId);\n const messages = await editor.editableMessages(sessionId);\n const retryable = await editor.retryableTurns(sessionId);\n return toTimelinePayload(sessionId, timeline, messages, retryable);\n}\n\nasync function runOperation(\n editor: SessionEditor,\n operation: SessionEditorOperation,\n): Promise<SessionEditorOperationResult> {\n switch (operation.action) {\n case \"edit\": {\n const result = await editor.edit(operation);\n return {\n sessionId: result.sessionId,\n queuedTurns: result.queuedTurns,\n ...(result.live === undefined ? {} : { live: result.live }),\n };\n }\n case \"reroll\": {\n const result = await editor.reroll(operation);\n return {\n sessionId: result.sessionId,\n queuedTurns: result.queuedTurns,\n ...(result.live === undefined ? {} : { live: result.live }),\n };\n }\n case \"retry\": {\n const result = await editor.retry(operation);\n return {\n sessionId: result.sessionId,\n queuedTurns: result.queuedTurns,\n ...(result.live === undefined ? {} : { live: result.live }),\n };\n }\n case \"rewind\":\n await editor.rewind(operation.sessionId, operation.toBoundary);\n return { sessionId: operation.sessionId, queuedTurns: 0 };\n case \"fork\":\n return {\n sessionId: await editor.fork(\n operation.sessionId,\n operation.atSeq,\n operation.childSessionId,\n ),\n queuedTurns: 0,\n };\n }\n}\n\nasync function handleRoute(\n editor: SessionEditor,\n request: HttpRequestLike,\n response: HttpResponseLike,\n): Promise<void> {\n try {\n if (request.method === \"GET\") {\n const url = new URL(request.url ?? SESSION_EDITOR_PATH, \"http://session-editor.local\");\n const sessionId = sessionIdOf(url.searchParams.get(\"sessionId\"));\n respondJson(response, 200, await readTimeline(editor, sessionId));\n return;\n }\n if (request.method === \"POST\") {\n respondJson(\n response,\n 200,\n await runOperation(editor, decodeOperation(await requestJson(request))),\n );\n return;\n }\n response.writeHead(405);\n response.end();\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n respondJson(response, error instanceof TypeError ? 400 : 409, {\n error: message,\n });\n }\n}\n\nfunction registerHttpRoutes(ctx: Context): void {\n const webServer = ctx.get(\"webServer\") as HttpServerLike | undefined;\n if (webServer === undefined) return;\n ctx.effect(() => {\n const editor = ctx.sessionEditor;\n return webServer.register({\n kind: \"exact\",\n path: SESSION_EDITOR_PATH,\n handler: (request, response) => handleRoute(editor, request, response),\n });\n }, \"session-editor: HTTP route\");\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"invariant.d.mts","names":[],"sources":["../src/invariant.ts"],"mappings":";;cAKa;cAEA;cAIA,QAAK,KAAS,YAAU"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"invariant.mjs","names":[],"sources":["../src/invariant.ts"],"sourcesContent":["import type { Context } from \"@deepseek-ai/cordis\";\nimport type { InvariantInstaller } from \"@deepseek-ai/dsh-invariants\";\n\nconst PACKAGE_NAME = \"@morlay/ui-conversation-message-actions\";\n\nexport const name = \"ui-conversation-message-actions-invariant\";\n\nexport const inject = [\"invariants\"];\n\nconst install: InvariantInstaller = () => {};\n\nexport const apply = (ctx: Context): Promise<() => void> =>\n Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));\n"],"mappings":";AAGA,MAAM,eAAe;AAErB,MAAa,OAAO;AAEpB,MAAa,SAAS,CAAC,YAAY;AAEnC,MAAM,gBAAoC,CAAC;AAE3C,MAAa,SAAS,QACpB,QAAQ,QAAQ,IAAI,WAAW,SAAS,cAAc,OAAO,CAAC"}