@morlay/session-branch 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/lib/index.d.mts +255 -0
- package/lib/index.d.mts.map +1 -0
- package/lib/index.mjs +131 -0
- package/lib/index.mjs.map +1 -0
- package/lib/invariant.d.mts +15 -0
- package/lib/invariant.d.mts.map +1 -0
- package/lib/invariant.mjs +22 -0
- package/lib/invariant.mjs.map +1 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 morlay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @morlay/session-branch
|
|
2
|
+
|
|
3
|
+
分支式会话编辑的**契约层**:定义数据层分支原语 `SessionBranchProvider`
|
|
4
|
+
(rewind / forkFrom / readBranchPrefix)、高层服务 `SessionBranch`
|
|
5
|
+
(`ctx.sessionBranch`)与共享的版本树投影 `buildTimeline`。
|
|
6
|
+
|
|
7
|
+
## 与上游 `SessionPersistence` 的关系
|
|
8
|
+
|
|
9
|
+
| 面 | 上游 `PersistenceBackend` | 本包 `SessionBranchProvider` |
|
|
10
|
+
| ------ | --------------------------------------------- | ------------------------------------------------------ |
|
|
11
|
+
| 覆盖 | 持久读写 + 崩溃修复(append-only) | 显式回退 + 闭合边界派生(分支面) |
|
|
12
|
+
| 原语 | `loadStored` / `appendBatch` / `commitRepair` | `readBranchPrefix` / `forkFrom` / `rewind` |
|
|
13
|
+
| 实现方 | JSONL / RDB 等 | RDB 等(`@morlay/session-rdb` 同时实现两者,形成闭环) |
|
|
14
|
+
|
|
15
|
+
## Provider 抽象
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
interface SessionBranchProvider {
|
|
19
|
+
readBranchPrefix(id, atSeq?, mode?, signal?): Promise<BranchBoundary>;
|
|
20
|
+
forkFrom(sourceId, options?, signal?): Promise<SessionId>;
|
|
21
|
+
rewind(id, toBoundary, signal?): Promise<SessionPersistenceSnapshot>;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- `readBranchPrefix`:定位 `atSeq` 锚定的闭合 `turn/end` 边界并返回前缀;
|
|
26
|
+
`mode: "after"`(包含目标轮,fork 语义)/ `"before"`(排除目标轮,编辑 /
|
|
27
|
+
重掷 / 重试语义)。
|
|
28
|
+
- `forkFrom`:纯 append——新会话(`parentSession` / `seedLength`),seed =
|
|
29
|
+
边界前缀 + `seedSuffix`,不触碰源会话。
|
|
30
|
+
- `rewind`:唯一改写事件 log 的操作,事务整体提交或回滚;**支持 live
|
|
31
|
+
会话**(内存 log 截断 + 派生缓存复位 + coordinator cursor 同步,见
|
|
32
|
+
`@morlay/session-rdb` 的 `SessionBranchRdbProvider`)。
|
|
33
|
+
- `syncLiveCursor(sessionId)`:编排层把 ignorable 版本效果 push 进 live log
|
|
34
|
+
后(不发布、不进 write-behind 缓冲),用其对齐 coordinator cursor,避免
|
|
35
|
+
后续 append 的 seq 校验错位(默认空实现,rdb 覆写)。
|
|
36
|
+
|
|
37
|
+
## 版本树
|
|
38
|
+
|
|
39
|
+
`buildTimeline(snapshots, readOwnEvents, sessionId)` 从会话快照(header
|
|
40
|
+
lineage)+ 每会话自有后缀(`seq >= seedLength` 的 `session-branch/version`
|
|
41
|
+
事件)投影完整版本树。
|
|
42
|
+
|
|
43
|
+
> 版本效果事件携带 `ignorable: true`(对核心可跳过、**不进 canonical log**),
|
|
44
|
+
> live 会话从内存 log 读到效果,cold 会话 timeline 只有 lineage 骨架。
|
|
45
|
+
|
|
46
|
+
## 安装
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
nub install @morlay/session-branch # peer: @deepseek-ai/cordis, dsh-session, dsh-session-persistence
|
|
50
|
+
```
|
package/lib/index.d.mts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { Context, Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import { SessionEvent, SessionEventMap, SessionId } from "@deepseek-ai/dsh-session";
|
|
3
|
+
import { SessionPersistenceSnapshot } from "@deepseek-ai/dsh-session-persistence";
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
type _BranchKeyCheck = "session-branch/version" extends keyof SessionEventMap ? true : false;
|
|
6
|
+
declare const _branchKeyVisible: _BranchKeyCheck;
|
|
7
|
+
/** 版本效果事件的数据结构版本(独立于 session 的 SESSION_FORMAT_VERSION)。 */
|
|
8
|
+
declare const SESSION_BRANCH_VERSION_SCHEMA = 1;
|
|
9
|
+
/** 下游历史策略:目标轮次之后旧后续的去留。 */
|
|
10
|
+
type CascadePolicy = "truncate" | "preserve";
|
|
11
|
+
/** 一个版本效果代表的用户可见操作。 */
|
|
12
|
+
type VersionOperation = "edit" | "reroll" | "retry" | "fork" | "rewind";
|
|
13
|
+
/** 可编辑的模型可见块分类。 */
|
|
14
|
+
type EditableBlockKind = "user" | "assistant.reasoning" | "assistant.response";
|
|
15
|
+
/** 一个版本效果的「正向」半边:记录做了什么、改了什么。 */
|
|
16
|
+
interface SessionBranchEffect {
|
|
17
|
+
/** 效果 id(全局唯一,跨版本树去重用)。 */
|
|
18
|
+
id: string;
|
|
19
|
+
operation: VersionOperation;
|
|
20
|
+
cascade: CascadePolicy;
|
|
21
|
+
/** 被操作的目标轮次(0 基)。 */
|
|
22
|
+
targetTurn: number;
|
|
23
|
+
/** 被操作的目标事件 seq(turn/start 或 user/message 等)。 */
|
|
24
|
+
targetEventSeq: number;
|
|
25
|
+
targetBlockIndex?: number;
|
|
26
|
+
blockKind?: EditableBlockKind;
|
|
27
|
+
/** 编辑前的文本(编辑类操作)。 */
|
|
28
|
+
before?: string;
|
|
29
|
+
/** 编辑后的文本(编辑类操作)。 */
|
|
30
|
+
after?: string;
|
|
31
|
+
}
|
|
32
|
+
/** 一个版本效果的「逆向」半边:恢复目标(父版本会话)。 */
|
|
33
|
+
interface SessionBranchInverse {
|
|
34
|
+
kind: "restore-version";
|
|
35
|
+
sessionId: SessionId;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 每个分支版本在自己的非继承后缀中包含的效果对。父版本链自动导出组合逆;
|
|
39
|
+
* 恢复不是删除事件,而是沿逆链切换到仍然存在的版本。
|
|
40
|
+
*
|
|
41
|
+
* 事件信封必须携带 `ignorable: true`:`session-branch/version` 是 branch 层
|
|
42
|
+
* 的 lineage 事实,上游核心不认识它——`ignorable` 标记让不认识它的读者
|
|
43
|
+
* (core / 非 branch 后端)安全跳过而不拒绝整条 log。branch 层后端
|
|
44
|
+
* (如 `@morlay/session-rdb`)特判保留该类型,确保 lineage 落盘。
|
|
45
|
+
*/
|
|
46
|
+
interface SessionBranchVersionEvent {
|
|
47
|
+
schemaVersion: typeof SESSION_BRANCH_VERSION_SCHEMA;
|
|
48
|
+
effect: SessionBranchEffect;
|
|
49
|
+
inverse: SessionBranchInverse;
|
|
50
|
+
}
|
|
51
|
+
declare module "@deepseek-ai/dsh-session" {
|
|
52
|
+
interface SessionEventMap {
|
|
53
|
+
/** 分支版本 provenance,由 branch provider / editor 追加到新版本会话。 */
|
|
54
|
+
"session-branch/version": SessionBranchVersionEvent;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** 一次闭合边界的定位结果:边界事件(含)及之前的前缀。 */
|
|
58
|
+
interface BranchBoundary {
|
|
59
|
+
/** 边界事件 seq(含;一个 `turn/end`)。 */
|
|
60
|
+
seq: number;
|
|
61
|
+
/** 边界及之前的前缀事件(`events[seq]` 即边界事件)。 */
|
|
62
|
+
events: readonly SessionEvent[];
|
|
63
|
+
}
|
|
64
|
+
/** 派生会话的 header 元数据(header 是「创建事实」,派生时冻结)。 */
|
|
65
|
+
interface BranchForkMeta {
|
|
66
|
+
cwd?: string;
|
|
67
|
+
createdAt?: number;
|
|
68
|
+
agentPreset?: string;
|
|
69
|
+
origin?: "subagent";
|
|
70
|
+
delegationDepth?: number;
|
|
71
|
+
}
|
|
72
|
+
/** `forkFrom` 的派生入参。 */
|
|
73
|
+
interface ForkFromOptions {
|
|
74
|
+
/**
|
|
75
|
+
* 锚定 seq:取 ≥ atSeq 的第一个 `turn/end` 为派生边界;省略或越过日志末尾
|
|
76
|
+
* 回退到源会话最后一个闭合轮次;atSeq 所在轮未闭合则拒绝(OPEN_TURN)。
|
|
77
|
+
* 与 {@link BranchAnchorMode} 配合决定派生点取「目标轮之后」还是
|
|
78
|
+
* 「目标轮之前」。
|
|
79
|
+
*/
|
|
80
|
+
atSeq?: number;
|
|
81
|
+
/**
|
|
82
|
+
* 锚定模式(默认 `"after"`)。分支式编辑/重掷/重试传 `"before"`——派生点
|
|
83
|
+
* 取目标轮之前的闭合边界(排除目标轮,目标轮由后续 agent 重新驱动)。
|
|
84
|
+
*/
|
|
85
|
+
anchorMode?: BranchAnchorMode;
|
|
86
|
+
/**
|
|
87
|
+
* 在边界前缀之后追加的事件(版本效果事件、手工闭合回合等)。这些事件
|
|
88
|
+
* 成为派生会话自己的非继承后缀,`seedLength` 只计边界前缀。
|
|
89
|
+
*/
|
|
90
|
+
seedSuffix?: readonly SessionEvent[];
|
|
91
|
+
/** 派生会话 id;省略由后端按自身 id 策略 mint。 */
|
|
92
|
+
childSessionId?: SessionId;
|
|
93
|
+
/** 派生 header 元数据。 */
|
|
94
|
+
meta?: BranchForkMeta;
|
|
95
|
+
}
|
|
96
|
+
/** 版本树节点投影(值级,供 Timeline / 标题栏消费)。 */
|
|
97
|
+
interface BranchVersionNode {
|
|
98
|
+
sessionId: SessionId;
|
|
99
|
+
parentSessionId?: SessionId;
|
|
100
|
+
/** durable fork 边界:继承前缀长度。 */
|
|
101
|
+
seedLength: number;
|
|
102
|
+
createdAt: number;
|
|
103
|
+
/** 本会话自有的版本效果(非继承);根节点无。 */
|
|
104
|
+
effect?: SessionBranchEffect;
|
|
105
|
+
/** 恢复目标(= parentSession 时与 inverse 一致)。 */
|
|
106
|
+
inverseSessionId?: SessionId;
|
|
107
|
+
}
|
|
108
|
+
/** 完整版本树:根 + 全部已知节点(含根)。 */
|
|
109
|
+
interface BranchTimeline {
|
|
110
|
+
root: BranchVersionNode;
|
|
111
|
+
nodes: BranchVersionNode[];
|
|
112
|
+
}
|
|
113
|
+
/** 分支操作的拒绝码。 */
|
|
114
|
+
type SessionBranchErrorCode = "SESSION_NOT_FOUND" | "INVALID_BOUNDARY" | "OPEN_TURN" | "FORK_UNAVAILABLE" | "REWIND_CONFLICT";
|
|
115
|
+
/** 分支操作的 typed error。 */
|
|
116
|
+
declare class SessionBranchError extends Error {
|
|
117
|
+
readonly code: SessionBranchErrorCode;
|
|
118
|
+
constructor(message: string, code: SessionBranchErrorCode);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 版本效果事件的结构化信封(守卫的返回类型;不依赖 `SessionEvent<T>` 泛型)。
|
|
122
|
+
*/
|
|
123
|
+
interface SessionBranchVersionEventEnvelope {
|
|
124
|
+
type: "session-branch/version";
|
|
125
|
+
seq: number;
|
|
126
|
+
time: number;
|
|
127
|
+
ignorable?: true;
|
|
128
|
+
data: SessionBranchVersionEvent;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 版本效果事件守卫:事件确实是 `session-branch/version` 且结构受支持。
|
|
132
|
+
*
|
|
133
|
+
* 结构化守卫(参数宽化 + 独立返回类型):augmentation 对 `keyof SessionEventMap`
|
|
134
|
+
* 可见,但对 dsh-session 内 `SessionEventType` 别名的重求值在 workspace+peer
|
|
135
|
+
* 解析下不可靠(`SessionEvent<"session-branch/version">` 泛型约束失败),
|
|
136
|
+
* 因此不依赖该泛型。
|
|
137
|
+
* @param event - 待判定事件。
|
|
138
|
+
* @returns 是当前 schema 的版本效果事件。
|
|
139
|
+
*/
|
|
140
|
+
declare function isSessionBranchVersionEvent(event: SessionEvent | {
|
|
141
|
+
type: string;
|
|
142
|
+
data: unknown;
|
|
143
|
+
}): event is SessionBranchVersionEventEnvelope;
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/provider.d.ts
|
|
146
|
+
/** 边界锚定模式。 */
|
|
147
|
+
type BranchAnchorMode = "after" | "before";
|
|
148
|
+
/**
|
|
149
|
+
* 分支数据层的后端契约。实现方必须保证:
|
|
150
|
+
*
|
|
151
|
+
* - `readBranchPrefix`:只读、不提交修复、不发布;返回的边界必是闭合
|
|
152
|
+
* `turn/end`,且 `events` 与其 seq 连续(与持久化读取语义一致)。
|
|
153
|
+
* - `forkFrom`:纯 append——新会话(新 id / `parentSession` / `seedLength`),
|
|
154
|
+
* 不触碰源会话;seed = 边界前缀 + `seedSuffix`。
|
|
155
|
+
* - `rewind`:唯一改写事件 log 的服务面操作;**独占条件**(无 live owner、
|
|
156
|
+
* 无 in-flight append、无 prepared reservation)由实现方校验;事务整体
|
|
157
|
+
* 提交或回滚(Abort 不部分截断);成功后 revision 变化,后续 append 从
|
|
158
|
+
* 新尾部继续。
|
|
159
|
+
*/
|
|
160
|
+
interface SessionBranchProvider {
|
|
161
|
+
/** 后端名(rewind 冲突诊断用)。 */
|
|
162
|
+
readonly name: string;
|
|
163
|
+
/**
|
|
164
|
+
* 定位 `atSeq` 锚定的闭合边界并返回其前缀(含边界事件)。
|
|
165
|
+
*
|
|
166
|
+
* - `mode: "after"`(默认):边界 = **≥ atSeq** 的第一个 `turn/end`——
|
|
167
|
+
* 「从目标轮结束处派生」(proposal 的 forkFrom 语义,包含目标轮)。
|
|
168
|
+
* - `mode: "before"`:边界 = **< atSeq** 的最后一个 `turn/end`——
|
|
169
|
+
* 「从目标轮之前分支」(分支式编辑/重掷/重试语义,排除目标轮)。
|
|
170
|
+
*
|
|
171
|
+
* 省略 `atSeq` 或越过日志末尾 → 最后一个闭合轮次;`atSeq` 锚定所在轮
|
|
172
|
+
* 未闭合(after 模式)→ 拒绝(`OPEN_TURN`);会话不存在 → 拒绝
|
|
173
|
+
* (`SESSION_NOT_FOUND`)。
|
|
174
|
+
* @param id - 持久化源会话。
|
|
175
|
+
* @param atSeq - 锚定 seq(inclusive);省略取最后闭合轮次。
|
|
176
|
+
* @param mode - 锚定模式(见上)。
|
|
177
|
+
* @param signal - 读取消。
|
|
178
|
+
*/
|
|
179
|
+
readBranchPrefix(id: SessionId, atSeq?: number, mode?: BranchAnchorMode, signal?: AbortSignal): Promise<BranchBoundary>;
|
|
180
|
+
/**
|
|
181
|
+
* 从持久化源派生新会话:seed = 边界前缀 + `options.seedSuffix`,meta 记录
|
|
182
|
+
* `parentSession` / `seedLength`。纯 append(派生不修改源),派生会话在
|
|
183
|
+
* 返回前已 durable(沿用 lazily-materialized 语义——空 seed 派生仍注册
|
|
184
|
+
* header,首个 append 才落盘)。
|
|
185
|
+
* @param sourceId - 持久化源会话。
|
|
186
|
+
* @param options - 边界锚定、seed 后缀、派生 id 与 meta。
|
|
187
|
+
* @param signal - 派生过程取消。
|
|
188
|
+
* @returns 派生的会话 id(`options.childSessionId` 或后端 mint)。
|
|
189
|
+
*/
|
|
190
|
+
forkFrom(sourceId: SessionId, options?: ForkFromOptions, signal?: AbortSignal): Promise<SessionId>;
|
|
191
|
+
/**
|
|
192
|
+
* 显式授权的截断式回退:截断事件 log 至 `toBoundary`(含该事件,保留其前
|
|
193
|
+
* 历史)。`toBoundary` 必须是非负整数、事件存在、且为闭合 `turn/end`
|
|
194
|
+
* 边界;否则拒绝(`INVALID_BOUNDARY` / `OPEN_TURN` / `SESSION_NOT_FOUND`)。
|
|
195
|
+
* 返回截断后的快照(header + revision)。
|
|
196
|
+
* @param id - 持久化会话。
|
|
197
|
+
* @param toBoundary - 截断点 seq(inclusive,`turn/end`)。
|
|
198
|
+
* @param signal - 中断时事务整体回滚(不部分截断)。
|
|
199
|
+
*/
|
|
200
|
+
rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<SessionPersistenceSnapshot>;
|
|
201
|
+
}
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/branch.d.ts
|
|
204
|
+
/**
|
|
205
|
+
* 抽象服务:组合分支数据层(rewind / forkFrom)与版本树投影,暴露统一的
|
|
206
|
+
* 服务面。`timeline` 的共享实现见 {@link buildTimeline}(后端组合
|
|
207
|
+
* `sessionPersistence` 快照 + 自有后缀读取后调用)。
|
|
208
|
+
*/
|
|
209
|
+
declare abstract class SessionBranch extends Service {
|
|
210
|
+
constructor(ctx: import("@deepseek-ai/cordis").Context);
|
|
211
|
+
/** 定位 `atSeq` 锚定的闭合边界并返回其前缀(含边界事件)。 */
|
|
212
|
+
abstract readBranchPrefix(id: SessionId, atSeq?: number, mode?: BranchAnchorMode, signal?: AbortSignal): Promise<BranchBoundary>;
|
|
213
|
+
/** 从持久化源派生新会话(纯 append;返回派生会话 id)。 */
|
|
214
|
+
abstract forkFrom(sourceId: SessionId, options?: ForkFromOptions, signal?: AbortSignal): Promise<SessionId>;
|
|
215
|
+
/** 显式授权的截断式回退;返回截断后的快照(header + revision)。 */
|
|
216
|
+
abstract rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<SessionPersistenceSnapshot>;
|
|
217
|
+
/** 完整版本树投影(根 + 全部已知节点)。 */
|
|
218
|
+
abstract timeline(sessionId: SessionId, signal?: AbortSignal): Promise<BranchTimeline>;
|
|
219
|
+
/**
|
|
220
|
+
* 同步 live 会话的 coordinator 内存 cursor 到其 log 长度。就地编辑时,
|
|
221
|
+
* 编排层会把 ignorable 的版本效果直接 push 进 live log(不发布
|
|
222
|
+
* `session/event`,不进 write-behind 缓冲),导致 coordinator 的 cursor
|
|
223
|
+
* 落后于 log——后续 manualTurn 的 append(seq 从 log 续接)会在 flush 的
|
|
224
|
+
* seq 校验上错位。默认无操作;rdb 后端覆写(访问 coordinator 的 states)。
|
|
225
|
+
*/
|
|
226
|
+
syncLiveCursor(_sessionId: SessionId): void;
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/timeline.d.ts
|
|
230
|
+
/**
|
|
231
|
+
* 读取一个会话「自有后缀」事件的函数——live 会话传 `events.slice(seedLength)`,
|
|
232
|
+
* 持久化会话传 `sessionPersistence.readFrom(id, seedLength)`。
|
|
233
|
+
*/
|
|
234
|
+
type OwnEventsReader = (id: SessionId, fromSeq: number, signal?: AbortSignal) => Promise<readonly import("@deepseek-ai/dsh-session").SessionEvent[]>;
|
|
235
|
+
/**
|
|
236
|
+
* 从会话快照集合构建 `sessionId` 的完整版本树。
|
|
237
|
+
* @param snapshots - 全部持久化会话的轻量快照(header + revision)。
|
|
238
|
+
* @param readOwnEvents - 按会话读取自有后缀事件。
|
|
239
|
+
* @param sessionId - 当前会话 id(树中标记为 current 由调用方负责)。
|
|
240
|
+
* @param signal - 读取取消。
|
|
241
|
+
*/
|
|
242
|
+
declare function buildTimeline(snapshots: readonly SessionPersistenceSnapshot[], readOwnEvents: OwnEventsReader, sessionId: SessionId, signal?: AbortSignal): Promise<BranchTimeline>;
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/index.d.ts
|
|
245
|
+
declare module "@deepseek-ai/cordis" {
|
|
246
|
+
interface Context {
|
|
247
|
+
/** 分支式会话编辑服务(rewind / forkFrom / timeline)。 */
|
|
248
|
+
sessionBranch: SessionBranch;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** 注册类型(无运行时副作用;服务由具体后端插件发布)。 */
|
|
252
|
+
declare function apply(_ctx: Context): void;
|
|
253
|
+
//#endregion
|
|
254
|
+
export { type BranchAnchorMode, BranchBoundary, BranchForkMeta, BranchTimeline, BranchVersionNode, CascadePolicy, EditableBlockKind, ForkFromOptions, type OwnEventsReader, SESSION_BRANCH_VERSION_SCHEMA, SessionBranch, SessionBranchEffect, SessionBranchError, SessionBranchErrorCode, SessionBranchInverse, type SessionBranchProvider, SessionBranchVersionEvent, SessionBranchVersionEventEnvelope, VersionOperation, _branchKeyVisible, apply, buildTimeline, isSessionBranchVersionEvent };
|
|
255
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/provider.ts","../src/branch.ts","../src/timeline.ts","../src/index.ts"],"mappings":";;;;KAiBK,yDAAyD;cACjD,mBAAmB;;cAGnB;;KAGD;;KAGA;;KAGA;;UAGK;;EAEf;EACA,WAAW;EACX,SAAS;;EAET;;EAEA;EACA;EACA,YAAY;;EAEZ;;EAEA;;;UAIe;EACf;EACA,WAAW;;;;;;;;;;;UAYI;EACf,sBAAsB;EACtB,QAAQ;EACR,SAAS;;;YAIC;;IAER,0BAA0B;;;;UAKb;;EAEf;;EAEA,iBAAiB;;;UAIF;EACf;EACA;EACA;EACA;EACA;;;UAIe;;;;;;;EAOf;;;;;EAKA,aAZe;;;;;EAiBf,sBAAsB;;EAEtB,iBAAiB;;EAEjB,OAAO;;;UAIQ;EACf,WAAW;EACX,kBAAkB;;EAElB;EACA;;EAEA,SAAS;;EAET,mBAAmB;;;UAIJ;EACf,MAAM;EACN,OAAO;;;KAIG;;cAQC,2BAA2B;WAC7B,MAAM;EACH,YAAA,iBAAiB,MAAM;;;;;UAUpB;EACf;EACA;EACA;EACA;EACA,MAAM;;;;;;;;;;;;iBAaQ,4BACd,OAAO;EAAiB;EAAc;IACrC,SAAS;;;;KCjKA;;;;;;;;;;;;;UAcK;;WAEN;;;;;;;;;;;;;;;;;EAkBT,iBACE,IAAI,WACJ,gBACA,OAAO,kBACP,SAAS,cACR,QAAQ;;;;;;;;;;;EAYX,SACE,UAAU,WACV,UAAU,iBACV,SAAS,cACR,QAAQ;;;;;;;;;;EAWX,OACE,IAAI,WACJ,oBACA,SAAS,cACR,QAAQ;;;;;;;;;uBCtES,sBAAsB;EAC9B,YAAA,mCAAmC;;WAKtC,iBACP,IAAI,WACJ,gBACA,OAFI,kBAGJ,SAAS,cACR,QAAQ;;WAGF,SACP,UAAU,WACV,UAAU,iBACV,SAAS,cACR,QAAQ;;WAGF,OACP,IAAI,WACJ,oBACA,SAAS,cACR,QAAQ;;WAGF,SAAS,WAAW,WAAW,SAAS,cAAc,QAAQ;;;;;;;;EASvE,eAAe,YAAY;;;;;;;;KC5BjB,mBACV,IAAI,WACJ,iBACA,SAAS,gBACN,oDAAoD;;;;;;;;iBASnC,cACpB,oBAAoB,8BACpB,eAAe,iBACf,WAAW,WACX,SAAS,cACR,QAAQ;;;;YCvBC;;IAER,eAAe;;;;iBAKH,MAAM,MAAM"}
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
//#region src/branch.ts
|
|
3
|
+
/**
|
|
4
|
+
* 分支式会话编辑的高层服务(`ctx.sessionBranch`):面向数据层
|
|
5
|
+
* {@link SessionBranchProvider} 的抽象服务面。后端(如
|
|
6
|
+
* `@morlay/session-rdb`)继承本类并提供 provider 实现,
|
|
7
|
+
* 编排层(`@morlay/ui-conversation-message-actions`)只依赖本服务。
|
|
8
|
+
*
|
|
9
|
+
* @module @morlay/session-branch
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* 抽象服务:组合分支数据层(rewind / forkFrom)与版本树投影,暴露统一的
|
|
13
|
+
* 服务面。`timeline` 的共享实现见 {@link buildTimeline}(后端组合
|
|
14
|
+
* `sessionPersistence` 快照 + 自有后缀读取后调用)。
|
|
15
|
+
*/
|
|
16
|
+
var SessionBranch = class extends Service {
|
|
17
|
+
constructor(ctx) {
|
|
18
|
+
super(ctx, "sessionBranch");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 同步 live 会话的 coordinator 内存 cursor 到其 log 长度。就地编辑时,
|
|
22
|
+
* 编排层会把 ignorable 的版本效果直接 push 进 live log(不发布
|
|
23
|
+
* `session/event`,不进 write-behind 缓冲),导致 coordinator 的 cursor
|
|
24
|
+
* 落后于 log——后续 manualTurn 的 append(seq 从 log 续接)会在 flush 的
|
|
25
|
+
* seq 校验上错位。默认无操作;rdb 后端覆写(访问 coordinator 的 states)。
|
|
26
|
+
*/
|
|
27
|
+
syncLiveCursor(_sessionId) {}
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/types.ts
|
|
31
|
+
const _branchKeyVisible = true;
|
|
32
|
+
/** 版本效果事件的数据结构版本(独立于 session 的 SESSION_FORMAT_VERSION)。 */
|
|
33
|
+
const SESSION_BRANCH_VERSION_SCHEMA = 1;
|
|
34
|
+
/** 分支操作的 typed error。 */
|
|
35
|
+
var SessionBranchError = class extends Error {
|
|
36
|
+
code;
|
|
37
|
+
constructor(message, code) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "SessionBranchError";
|
|
40
|
+
this.code = code;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* 版本效果事件守卫:事件确实是 `session-branch/version` 且结构受支持。
|
|
45
|
+
*
|
|
46
|
+
* 结构化守卫(参数宽化 + 独立返回类型):augmentation 对 `keyof SessionEventMap`
|
|
47
|
+
* 可见,但对 dsh-session 内 `SessionEventType` 别名的重求值在 workspace+peer
|
|
48
|
+
* 解析下不可靠(`SessionEvent<"session-branch/version">` 泛型约束失败),
|
|
49
|
+
* 因此不依赖该泛型。
|
|
50
|
+
* @param event - 待判定事件。
|
|
51
|
+
* @returns 是当前 schema 的版本效果事件。
|
|
52
|
+
*/
|
|
53
|
+
function isSessionBranchVersionEvent(event) {
|
|
54
|
+
return event.type === "session-branch/version" && event.data.schemaVersion === 1;
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/timeline.ts
|
|
58
|
+
/**
|
|
59
|
+
* 从会话快照集合构建 `sessionId` 的完整版本树。
|
|
60
|
+
* @param snapshots - 全部持久化会话的轻量快照(header + revision)。
|
|
61
|
+
* @param readOwnEvents - 按会话读取自有后缀事件。
|
|
62
|
+
* @param sessionId - 当前会话 id(树中标记为 current 由调用方负责)。
|
|
63
|
+
* @param signal - 读取取消。
|
|
64
|
+
*/
|
|
65
|
+
async function buildTimeline(snapshots, readOwnEvents, sessionId, signal) {
|
|
66
|
+
const byId = new Map(snapshots.map((snapshot) => [snapshot.header.id, snapshot]));
|
|
67
|
+
const ancestors = [];
|
|
68
|
+
let cursor = sessionId;
|
|
69
|
+
const seen = /* @__PURE__ */ new Set();
|
|
70
|
+
while (cursor !== void 0) {
|
|
71
|
+
if (seen.has(cursor)) throw new SessionBranchError("lineage contains a cycle", "INVALID_BOUNDARY");
|
|
72
|
+
seen.add(cursor);
|
|
73
|
+
ancestors.push(cursor);
|
|
74
|
+
cursor = byId.get(cursor)?.header.parentSession;
|
|
75
|
+
}
|
|
76
|
+
const rootId = ancestors.at(-1);
|
|
77
|
+
if (rootId === void 0) throw new SessionBranchError(`session "${sessionId}" is not persisted`, "SESSION_NOT_FOUND");
|
|
78
|
+
const ordered = [];
|
|
79
|
+
const queue = [rootId];
|
|
80
|
+
while (queue.length > 0) {
|
|
81
|
+
const id = queue.shift();
|
|
82
|
+
if (id === void 0) continue;
|
|
83
|
+
ordered.push(id);
|
|
84
|
+
const children = snapshots.filter((snapshot) => snapshot.header.parentSession === id).sort((left, right) => left.header.createdAt - right.header.createdAt || String(left.header.id).localeCompare(String(right.header.id))).map((snapshot) => snapshot.header.id);
|
|
85
|
+
queue.push(...children);
|
|
86
|
+
}
|
|
87
|
+
const nodes = [];
|
|
88
|
+
const effectIds = /* @__PURE__ */ new Set();
|
|
89
|
+
for (const id of ordered) {
|
|
90
|
+
const snapshot = byId.get(id);
|
|
91
|
+
if (snapshot === void 0) continue;
|
|
92
|
+
const header = snapshot.header;
|
|
93
|
+
const node = {
|
|
94
|
+
sessionId: header.id,
|
|
95
|
+
...header.parentSession === void 0 ? {} : { parentSessionId: header.parentSession },
|
|
96
|
+
seedLength: header.seedLength ?? 0,
|
|
97
|
+
createdAt: header.createdAt
|
|
98
|
+
};
|
|
99
|
+
if (header.parentSession !== void 0) {
|
|
100
|
+
const events = await readOwnEvents(header.id, node.seedLength, signal);
|
|
101
|
+
let version;
|
|
102
|
+
for (const event of events) if (isSessionBranchVersionEvent(event)) {
|
|
103
|
+
if (version !== void 0) throw new SessionBranchError(`session ${header.id} carries multiple own version effects`, "INVALID_BOUNDARY");
|
|
104
|
+
version = event;
|
|
105
|
+
}
|
|
106
|
+
if (version !== void 0) {
|
|
107
|
+
const data = version.data;
|
|
108
|
+
if (data.inverse.kind !== "restore-version" || data.inverse.sessionId !== header.parentSession) throw new SessionBranchError(`session ${header.id} version inverse does not match its parent`, "INVALID_BOUNDARY");
|
|
109
|
+
if (effectIds.has(data.effect.id)) throw new SessionBranchError(`version effect ${data.effect.id} is duplicated`, "INVALID_BOUNDARY");
|
|
110
|
+
effectIds.add(data.effect.id);
|
|
111
|
+
node.effect = data.effect;
|
|
112
|
+
node.inverseSessionId = data.inverse.sessionId;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
nodes.push(node);
|
|
116
|
+
}
|
|
117
|
+
const root = nodes.find((node) => node.parentSessionId === void 0);
|
|
118
|
+
if (root === void 0) throw new SessionBranchError(`session "${sessionId}" lineage has no root`, "SESSION_NOT_FOUND");
|
|
119
|
+
return {
|
|
120
|
+
root,
|
|
121
|
+
nodes
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/index.ts
|
|
126
|
+
/** 注册类型(无运行时副作用;服务由具体后端插件发布)。 */
|
|
127
|
+
function apply(_ctx) {}
|
|
128
|
+
//#endregion
|
|
129
|
+
export { SESSION_BRANCH_VERSION_SCHEMA, SessionBranch, SessionBranchError, _branchKeyVisible, apply, buildTimeline, isSessionBranchVersionEvent };
|
|
130
|
+
|
|
131
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/branch.ts","../src/types.ts","../src/timeline.ts","../src/index.ts"],"sourcesContent":["/**\n * 分支式会话编辑的高层服务(`ctx.sessionBranch`):面向数据层\n * {@link SessionBranchProvider} 的抽象服务面。后端(如\n * `@morlay/session-rdb`)继承本类并提供 provider 实现,\n * 编排层(`@morlay/ui-conversation-message-actions`)只依赖本服务。\n *\n * @module @morlay/session-branch\n */\n\nimport { Service } from \"@deepseek-ai/cordis\";\nimport type { SessionId } from \"@deepseek-ai/dsh-session\";\nimport type { SessionPersistenceSnapshot } from \"@deepseek-ai/dsh-session-persistence\";\nimport type { BranchBoundary, BranchTimeline, ForkFromOptions } from \"./types.ts\";\n\n/**\n * 抽象服务:组合分支数据层(rewind / forkFrom)与版本树投影,暴露统一的\n * 服务面。`timeline` 的共享实现见 {@link buildTimeline}(后端组合\n * `sessionPersistence` 快照 + 自有后缀读取后调用)。\n */\nexport abstract class SessionBranch extends Service {\n constructor(ctx: import(\"@deepseek-ai/cordis\").Context) {\n super(ctx, \"sessionBranch\");\n }\n\n /** 定位 `atSeq` 锚定的闭合边界并返回其前缀(含边界事件)。 */\n abstract readBranchPrefix(\n id: SessionId,\n atSeq?: number,\n mode?: import(\"./provider.ts\").BranchAnchorMode,\n signal?: AbortSignal,\n ): Promise<BranchBoundary>;\n\n /** 从持久化源派生新会话(纯 append;返回派生会话 id)。 */\n abstract forkFrom(\n sourceId: SessionId,\n options?: ForkFromOptions,\n signal?: AbortSignal,\n ): Promise<SessionId>;\n\n /** 显式授权的截断式回退;返回截断后的快照(header + revision)。 */\n abstract rewind(\n id: SessionId,\n toBoundary: number,\n signal?: AbortSignal,\n ): Promise<SessionPersistenceSnapshot>;\n\n /** 完整版本树投影(根 + 全部已知节点)。 */\n abstract timeline(sessionId: SessionId, signal?: AbortSignal): Promise<BranchTimeline>;\n\n /**\n * 同步 live 会话的 coordinator 内存 cursor 到其 log 长度。就地编辑时,\n * 编排层会把 ignorable 的版本效果直接 push 进 live log(不发布\n * `session/event`,不进 write-behind 缓冲),导致 coordinator 的 cursor\n * 落后于 log——后续 manualTurn 的 append(seq 从 log 续接)会在 flush 的\n * seq 校验上错位。默认无操作;rdb 后端覆写(访问 coordinator 的 states)。\n */\n syncLiveCursor(_sessionId: SessionId): void {\n // 默认无操作。\n }\n}\n","/**\n * 分支式会话编辑的共享类型:版本效果事件(`session-branch/version`)、\n * 闭合边界定位、派生元数据与版本树投影。\n *\n * 设计对齐 `@deepseek-ai/dsh-session` 的 merge-extensible `SessionEventMap`:\n * 版本事件是插件合并进事件映射的**持久**事件(占 seq、入 canonical log),\n * `parentSession` + `seedLength`(durable lineage)区分「继承」与「自有」。\n *\n * @module @morlay/session-branch/types\n */\n\nimport type { SessionEvent, SessionId } from \"@deepseek-ai/dsh-session\";\nimport type { SessionEventMap } from \"@deepseek-ai/dsh-session\";\n\n// 编译期诊断:module augmentation 后 `keyof SessionEventMap` 可见,但 dsh-session\n// 内已解析的 `SessionEventType` 别名(= keyof SessionEventMap)不会重求值——\n// 因此 `SessionEvent<\"session-branch/version\">` 泛型不可用,守卫/消费走结构化。\ntype _BranchKeyCheck = \"session-branch/version\" extends keyof SessionEventMap ? true : false;\nexport const _branchKeyVisible: _BranchKeyCheck = true as const;\n\n/** 版本效果事件的数据结构版本(独立于 session 的 SESSION_FORMAT_VERSION)。 */\nexport const SESSION_BRANCH_VERSION_SCHEMA = 1;\n\n/** 下游历史策略:目标轮次之后旧后续的去留。 */\nexport type CascadePolicy = \"truncate\" | \"preserve\";\n\n/** 一个版本效果代表的用户可见操作。 */\nexport type VersionOperation = \"edit\" | \"reroll\" | \"retry\" | \"fork\" | \"rewind\";\n\n/** 可编辑的模型可见块分类。 */\nexport type EditableBlockKind = \"user\" | \"assistant.reasoning\" | \"assistant.response\";\n\n/** 一个版本效果的「正向」半边:记录做了什么、改了什么。 */\nexport interface SessionBranchEffect {\n /** 效果 id(全局唯一,跨版本树去重用)。 */\n id: string;\n operation: VersionOperation;\n cascade: CascadePolicy;\n /** 被操作的目标轮次(0 基)。 */\n targetTurn: number;\n /** 被操作的目标事件 seq(turn/start 或 user/message 等)。 */\n targetEventSeq: number;\n targetBlockIndex?: number;\n blockKind?: EditableBlockKind;\n /** 编辑前的文本(编辑类操作)。 */\n before?: string;\n /** 编辑后的文本(编辑类操作)。 */\n after?: string;\n}\n\n/** 一个版本效果的「逆向」半边:恢复目标(父版本会话)。 */\nexport interface SessionBranchInverse {\n kind: \"restore-version\";\n sessionId: SessionId;\n}\n\n/**\n * 每个分支版本在自己的非继承后缀中包含的效果对。父版本链自动导出组合逆;\n * 恢复不是删除事件,而是沿逆链切换到仍然存在的版本。\n *\n * 事件信封必须携带 `ignorable: true`:`session-branch/version` 是 branch 层\n * 的 lineage 事实,上游核心不认识它——`ignorable` 标记让不认识它的读者\n * (core / 非 branch 后端)安全跳过而不拒绝整条 log。branch 层后端\n * (如 `@morlay/session-rdb`)特判保留该类型,确保 lineage 落盘。\n */\nexport interface SessionBranchVersionEvent {\n schemaVersion: typeof SESSION_BRANCH_VERSION_SCHEMA;\n effect: SessionBranchEffect;\n inverse: SessionBranchInverse;\n}\n\ndeclare module \"@deepseek-ai/dsh-session\" {\n interface SessionEventMap {\n /** 分支版本 provenance,由 branch provider / editor 追加到新版本会话。 */\n \"session-branch/version\": SessionBranchVersionEvent;\n }\n}\n\n/** 一次闭合边界的定位结果:边界事件(含)及之前的前缀。 */\nexport interface BranchBoundary {\n /** 边界事件 seq(含;一个 `turn/end`)。 */\n seq: number;\n /** 边界及之前的前缀事件(`events[seq]` 即边界事件)。 */\n events: readonly SessionEvent[];\n}\n\n/** 派生会话的 header 元数据(header 是「创建事实」,派生时冻结)。 */\nexport interface BranchForkMeta {\n cwd?: string;\n createdAt?: number;\n agentPreset?: string;\n origin?: \"subagent\";\n delegationDepth?: number;\n}\n\n/** `forkFrom` 的派生入参。 */\nexport interface ForkFromOptions {\n /**\n * 锚定 seq:取 ≥ atSeq 的第一个 `turn/end` 为派生边界;省略或越过日志末尾\n * 回退到源会话最后一个闭合轮次;atSeq 所在轮未闭合则拒绝(OPEN_TURN)。\n * 与 {@link BranchAnchorMode} 配合决定派生点取「目标轮之后」还是\n * 「目标轮之前」。\n */\n atSeq?: number;\n /**\n * 锚定模式(默认 `\"after\"`)。分支式编辑/重掷/重试传 `\"before\"`——派生点\n * 取目标轮之前的闭合边界(排除目标轮,目标轮由后续 agent 重新驱动)。\n */\n anchorMode?: import(\"./provider.ts\").BranchAnchorMode;\n /**\n * 在边界前缀之后追加的事件(版本效果事件、手工闭合回合等)。这些事件\n * 成为派生会话自己的非继承后缀,`seedLength` 只计边界前缀。\n */\n seedSuffix?: readonly SessionEvent[];\n /** 派生会话 id;省略由后端按自身 id 策略 mint。 */\n childSessionId?: SessionId;\n /** 派生 header 元数据。 */\n meta?: BranchForkMeta;\n}\n\n/** 版本树节点投影(值级,供 Timeline / 标题栏消费)。 */\nexport interface BranchVersionNode {\n sessionId: SessionId;\n parentSessionId?: SessionId;\n /** durable fork 边界:继承前缀长度。 */\n seedLength: number;\n createdAt: number;\n /** 本会话自有的版本效果(非继承);根节点无。 */\n effect?: SessionBranchEffect;\n /** 恢复目标(= parentSession 时与 inverse 一致)。 */\n inverseSessionId?: SessionId;\n}\n\n/** 完整版本树:根 + 全部已知节点(含根)。 */\nexport interface BranchTimeline {\n root: BranchVersionNode;\n nodes: BranchVersionNode[];\n}\n\n/** 分支操作的拒绝码。 */\nexport type SessionBranchErrorCode =\n | \"SESSION_NOT_FOUND\"\n | \"INVALID_BOUNDARY\"\n | \"OPEN_TURN\"\n | \"FORK_UNAVAILABLE\"\n | \"REWIND_CONFLICT\";\n\n/** 分支操作的 typed error。 */\nexport class SessionBranchError extends Error {\n readonly code: SessionBranchErrorCode;\n constructor(message: string, code: SessionBranchErrorCode) {\n super(message);\n this.name = \"SessionBranchError\";\n this.code = code;\n }\n}\n\n/**\n * 版本效果事件的结构化信封(守卫的返回类型;不依赖 `SessionEvent<T>` 泛型)。\n */\nexport interface SessionBranchVersionEventEnvelope {\n type: \"session-branch/version\";\n seq: number;\n time: number;\n ignorable?: true;\n data: SessionBranchVersionEvent;\n}\n\n/**\n * 版本效果事件守卫:事件确实是 `session-branch/version` 且结构受支持。\n *\n * 结构化守卫(参数宽化 + 独立返回类型):augmentation 对 `keyof SessionEventMap`\n * 可见,但对 dsh-session 内 `SessionEventType` 别名的重求值在 workspace+peer\n * 解析下不可靠(`SessionEvent<\"session-branch/version\">` 泛型约束失败),\n * 因此不依赖该泛型。\n * @param event - 待判定事件。\n * @returns 是当前 schema 的版本效果事件。\n */\nexport function isSessionBranchVersionEvent(\n event: SessionEvent | { type: string; data: unknown },\n): event is SessionBranchVersionEventEnvelope {\n return (\n event.type === \"session-branch/version\" &&\n (event.data as { schemaVersion?: unknown }).schemaVersion === SESSION_BRANCH_VERSION_SCHEMA\n );\n}\n","/**\n * 版本树投影的共享组合逻辑:从持久化快照列表 + 每会话「自有后缀」读版本\n * 效果事件,导出完整 lineage 树。具体后端(`SessionBranch` 实现)提供数据\n * 读取,本模块做纯投影——根与后代的确定、自有版本效果的扫描、节点归一。\n *\n * 规则(对齐 `dsh-message-edit` 的数据模型):\n * - `parentSession` 构成版本树;`seedLength` 区分继承与自有后缀。\n * - 每个会话至多一个**自有** `session-branch/version` 事件(seq ≥ seedLength)。\n * - 根节点(无 `parentSession`)不带版本效果。\n * - 版本效果 id 全局唯一;逆链(`inverse.sessionId`)必须指向树内父节点。\n *\n * @module @morlay/session-branch/timeline\n */\n\nimport type { SessionId } from \"@deepseek-ai/dsh-session\";\nimport type { SessionPersistenceSnapshot } from \"@deepseek-ai/dsh-session-persistence\";\nimport {\n SessionBranchError,\n isSessionBranchVersionEvent,\n type BranchTimeline,\n type BranchVersionNode,\n type SessionBranchVersionEventEnvelope,\n} from \"./types.ts\";\n\n/**\n * 读取一个会话「自有后缀」事件的函数——live 会话传 `events.slice(seedLength)`,\n * 持久化会话传 `sessionPersistence.readFrom(id, seedLength)`。\n */\nexport type OwnEventsReader = (\n id: SessionId,\n fromSeq: number,\n signal?: AbortSignal,\n) => Promise<readonly import(\"@deepseek-ai/dsh-session\").SessionEvent[]>;\n\n/**\n * 从会话快照集合构建 `sessionId` 的完整版本树。\n * @param snapshots - 全部持久化会话的轻量快照(header + revision)。\n * @param readOwnEvents - 按会话读取自有后缀事件。\n * @param sessionId - 当前会话 id(树中标记为 current 由调用方负责)。\n * @param signal - 读取取消。\n */\nexport async function buildTimeline(\n snapshots: readonly SessionPersistenceSnapshot[],\n readOwnEvents: OwnEventsReader,\n sessionId: SessionId,\n signal?: AbortSignal,\n): Promise<BranchTimeline> {\n const byId = new Map(snapshots.map((snapshot) => [snapshot.header.id, snapshot] as const));\n\n // 回溯到根(当前会话 → 祖先链)。\n const ancestors: SessionId[] = [];\n let cursor: SessionId | undefined = sessionId;\n const seen = new Set<SessionId>();\n while (cursor !== undefined) {\n if (seen.has(cursor))\n throw new SessionBranchError(\"lineage contains a cycle\", \"INVALID_BOUNDARY\");\n seen.add(cursor);\n ancestors.push(cursor);\n const snapshot = byId.get(cursor);\n cursor = snapshot?.header.parentSession;\n }\n const rootId = ancestors.at(-1);\n if (rootId === undefined) {\n throw new SessionBranchError(`session \"${sessionId}\" is not persisted`, \"SESSION_NOT_FOUND\");\n }\n\n // 根向下的完整后代(BFS;按 createdAt 稳定排序)。\n const ordered: SessionId[] = [];\n const queue: SessionId[] = [rootId];\n while (queue.length > 0) {\n const id = queue.shift();\n if (id === undefined) continue;\n ordered.push(id);\n const children = snapshots\n .filter((snapshot) => snapshot.header.parentSession === id)\n .sort(\n (left, right) =>\n left.header.createdAt - right.header.createdAt ||\n String(left.header.id).localeCompare(String(right.header.id)),\n )\n .map((snapshot) => snapshot.header.id);\n queue.push(...children);\n }\n\n const nodes: BranchVersionNode[] = [];\n const effectIds = new Set<string>();\n for (const id of ordered) {\n const snapshot = byId.get(id);\n if (snapshot === undefined) continue;\n const header = snapshot.header;\n const node: BranchVersionNode = {\n sessionId: header.id,\n ...(header.parentSession === undefined ? {} : { parentSessionId: header.parentSession }),\n seedLength: header.seedLength ?? 0,\n createdAt: header.createdAt,\n };\n // 根节点不可能带版本效果;其余节点读自有后缀。\n if (header.parentSession !== undefined) {\n const events = await readOwnEvents(header.id, node.seedLength, signal);\n // 结构化守卫 + find(避免 filter 的 `S extends T` 约束——版本事件类型不在\n // SessionEvent 判别联合的固化 `SessionEventType` 中)。\n let version: SessionBranchVersionEventEnvelope | undefined;\n for (const event of events) {\n if (isSessionBranchVersionEvent(event)) {\n if (version !== undefined) {\n throw new SessionBranchError(\n `session ${header.id} carries multiple own version effects`,\n \"INVALID_BOUNDARY\",\n );\n }\n version = event;\n }\n }\n if (version !== undefined) {\n const data = version.data;\n if (\n data.inverse.kind !== \"restore-version\" ||\n data.inverse.sessionId !== header.parentSession\n ) {\n throw new SessionBranchError(\n `session ${header.id} version inverse does not match its parent`,\n \"INVALID_BOUNDARY\",\n );\n }\n if (effectIds.has(data.effect.id)) {\n throw new SessionBranchError(\n `version effect ${data.effect.id} is duplicated`,\n \"INVALID_BOUNDARY\",\n );\n }\n effectIds.add(data.effect.id);\n node.effect = data.effect;\n node.inverseSessionId = data.inverse.sessionId;\n }\n }\n nodes.push(node);\n }\n\n const root = nodes.find((node) => node.parentSessionId === undefined);\n if (root === undefined) {\n throw new SessionBranchError(`session \"${sessionId}\" lineage has no root`, \"SESSION_NOT_FOUND\");\n }\n return { root, nodes };\n}\n","/**\n * @morlay/session-branch —— 分支式会话编辑的 provider 抽象 + 高层服务。\n *\n * 本包是 better-session monorepo 的**契约层**:定义数据层分支原语\n * {@link SessionBranchProvider}(rewind / forkFrom)、高层服务\n * {@link SessionBranch}(`ctx.sessionBranch`)与共享的版本树投影\n * {@link buildTimeline}。具体持久化后端实现 provider 后,编排层\n * (`@morlay/ui-conversation-message-actions`)即可在不修改上游 `@deepseek-ai/*` 代码的\n * 前提下提供完整的 rewind / retry / fork 功能。\n *\n * @module @morlay/session-branch\n */\n\nimport type { Context } from \"@deepseek-ai/cordis\";\nimport { SessionBranch } from \"./branch.ts\";\n\nexport { SessionBranch } from \"./branch.ts\";\nexport type { SessionBranchProvider, BranchAnchorMode } from \"./provider.ts\";\nexport { buildTimeline } from \"./timeline.ts\";\nexport type { OwnEventsReader } from \"./timeline.ts\";\nexport * from \"./types.ts\";\n\ndeclare module \"@deepseek-ai/cordis\" {\n interface Context {\n /** 分支式会话编辑服务(rewind / forkFrom / timeline)。 */\n sessionBranch: SessionBranch;\n }\n}\n\n/** 注册类型(无运行时副作用;服务由具体后端插件发布)。 */\nexport function apply(_ctx: Context): void {\n // 契约层不发布服务——`ctx.sessionBranch` 由实现 provider 的后端插件\n // (如 @morlay/session-rdb)在启动时注册为 `SessionBranch`\n // 子类实例。此处保留 apply 是为了 cordis 插件装配兼容(类型声明 + 可空\n // 生命周期),不注入任何服务。\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
//#region src/invariant.d.ts
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
declare const name = "session-branch-invariant";
|
|
5
|
+
/** Service required before the companion can reserve package ownership. */
|
|
6
|
+
declare const inject: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Register this package's invariant companion.
|
|
9
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
10
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
11
|
+
*/
|
|
12
|
+
declare const apply: (ctx: Context) => Promise<() => void>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { apply, inject, name };
|
|
15
|
+
//# sourceMappingURL=invariant.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invariant.d.mts","names":[],"sources":["../src/invariant.ts"],"mappings":";;;cAWa;;cAEA;;;;;;cAcA,QAAS,KAAK,YAAU"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/invariant.ts
|
|
2
|
+
const PACKAGE_NAME = "@morlay/session-branch";
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
const name = "session-branch-invariant";
|
|
5
|
+
/** Service required before the companion can reserve package ownership. */
|
|
6
|
+
const inject = ["invariants"];
|
|
7
|
+
/**
|
|
8
|
+
* No runtime invariant: branch semantics (rewind / forkFrom / boundary
|
|
9
|
+
* anchoring) are validated by contract tests; this package exposes no
|
|
10
|
+
* continuously observable in-process relation.
|
|
11
|
+
*/
|
|
12
|
+
const install = () => {};
|
|
13
|
+
/**
|
|
14
|
+
* Register this package's invariant companion.
|
|
15
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
16
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
17
|
+
*/
|
|
18
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
19
|
+
//#endregion
|
|
20
|
+
export { apply, inject, name };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=invariant.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invariant.mjs","names":[],"sources":["../src/invariant.ts"],"sourcesContent":["/**\n * Package-owned invariant companion for `@morlay/session-branch`.\n * @module @morlay/session-branch/invariant\n */\n\nimport type { Context } from \"@deepseek-ai/cordis\";\nimport type { InvariantInstaller } from \"@deepseek-ai/dsh-invariants\";\n\nconst PACKAGE_NAME = \"@morlay/session-branch\";\n\n/** Cordis companion plugin name. */\nexport const name = \"session-branch-invariant\";\n/** Service required before the companion can reserve package ownership. */\nexport const inject = [\"invariants\"];\n\n/**\n * No runtime invariant: branch semantics (rewind / forkFrom / boundary\n * anchoring) are validated by contract tests; this package exposes no\n * continuously observable in-process relation.\n */\nconst install: InvariantInstaller = () => {};\n\n/**\n * Register this package's invariant companion.\n * @param ctx - Cordis context carrying the invariant service.\n * @returns the installed registration's disposer after setup succeeds.\n */\nexport const apply = (ctx: Context): Promise<() => void> =>\n Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));\n"],"mappings":";AAQA,MAAM,eAAe;;AAGrB,MAAa,OAAO;;AAEpB,MAAa,SAAS,CAAC,YAAY;;;;;;AAOnC,MAAM,gBAAoC,CAAC;;;;;;AAO3C,MAAa,SAAS,QACpB,QAAQ,QAAQ,IAAI,WAAW,SAAS,cAAc,OAAO,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@morlay/session-branch",
|
|
3
|
+
"version": "0.0.9",
|
|
4
|
+
"description": "Provider abstraction + high-level service for rewind / retry / fork over DeepSeek Harness event-sourced sessions.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"dsh-plugin",
|
|
8
|
+
"fork",
|
|
9
|
+
"retry",
|
|
10
|
+
"rewind",
|
|
11
|
+
"session-branch"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/morlay/better-session.git"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"lib"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./lib/index.d.mts",
|
|
25
|
+
"default": "./lib/index.mjs"
|
|
26
|
+
},
|
|
27
|
+
"./invariant": {
|
|
28
|
+
"types": "./lib/invariant.d.mts",
|
|
29
|
+
"default": "./lib/invariant.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json",
|
|
32
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
36
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
|
|
37
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.8",
|
|
38
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.0-rc.8"
|
|
39
|
+
},
|
|
40
|
+
"dsh": {
|
|
41
|
+
"bundle": {
|
|
42
|
+
"patch": "./cordis.patch.yml"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"scripts": {}
|
|
46
|
+
}
|