@morlay/session-branch 0.0.9 → 0.0.11
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/lib/index.d.mts +29 -5
- package/lib/index.d.mts.map +1 -1
- package/lib/index.mjs +34 -1
- package/lib/index.mjs.map +1 -1
- package/lib/invariant.d.mts.map +1 -1
- package/package.json +4 -4
package/lib/index.d.mts
CHANGED
|
@@ -189,12 +189,19 @@ interface SessionBranchProvider {
|
|
|
189
189
|
*/
|
|
190
190
|
forkFrom(sourceId: SessionId, options?: ForkFromOptions, signal?: AbortSignal): Promise<SessionId>;
|
|
191
191
|
/**
|
|
192
|
-
* 显式授权的截断式回退:截断事件 log 至 `toBoundary
|
|
193
|
-
*
|
|
194
|
-
*
|
|
192
|
+
* 显式授权的截断式回退:截断事件 log 至 `toBoundary`。`toBoundary` 必须是
|
|
193
|
+
* 非负整数、事件存在、且为 `turn/end` 或 `user/message`;否则拒绝
|
|
194
|
+
* (`INVALID_BOUNDARY` / `SESSION_NOT_FOUND`)。
|
|
195
|
+
*
|
|
196
|
+
* 边界语义按类型区分:
|
|
197
|
+
* - `turn/end`:**保留到该事件**(inclusive)——轮次完整闭合,后续轮次
|
|
198
|
+
* 被截断;
|
|
199
|
+
* - `user/message`:**drop 该消息及其后**(exclusive)——编辑重放语义:
|
|
200
|
+
* 该消息会被编辑后的版本替换,因此边界消息本身不保留。
|
|
201
|
+
*
|
|
195
202
|
* 返回截断后的快照(header + revision)。
|
|
196
203
|
* @param id - 持久化会话。
|
|
197
|
-
* @param toBoundary - 截断点 seq
|
|
204
|
+
* @param toBoundary - 截断点 seq(`turn/end` inclusive / `user/message` exclusive)。
|
|
198
205
|
* @param signal - 中断时事务整体回滚(不部分截断)。
|
|
199
206
|
*/
|
|
200
207
|
rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<SessionPersistenceSnapshot>;
|
|
@@ -241,6 +248,23 @@ type OwnEventsReader = (id: SessionId, fromSeq: number, signal?: AbortSignal) =>
|
|
|
241
248
|
*/
|
|
242
249
|
declare function buildTimeline(snapshots: readonly SessionPersistenceSnapshot[], readOwnEvents: OwnEventsReader, sessionId: SessionId, signal?: AbortSignal): Promise<BranchTimeline>;
|
|
243
250
|
//#endregion
|
|
251
|
+
//#region src/balance.d.ts
|
|
252
|
+
/**
|
|
253
|
+
* 从尾部剔除未配对的 `step/start`,返回平衡后的保留前缀。
|
|
254
|
+
*
|
|
255
|
+
* 从尾部向前扫描:`step/end` 配对它**之前**的 `step/start`(agent-loop 的
|
|
256
|
+
* finally 保证每个 step/start 都有配对 step/end,除非被 rewind 截断);
|
|
257
|
+
* `turn/end` 闭合其轮次,之前的 step 全部合法。未配对(孤儿)的
|
|
258
|
+
* `step/start` 及其后全部剔除——rewind 截断产生的孤儿只可能出现在保留
|
|
259
|
+
* 前缀的尾部(drop 区是后缀),因此从尾部剔除即完整修复。
|
|
260
|
+
*
|
|
261
|
+
* 只剔除**尾部**未配对的 step/start:log 中间(闭合轮次之间)的 step/start
|
|
262
|
+
* 必然有配对,且剔除中间事件会破坏 seq 连续性。
|
|
263
|
+
* @param events - rewind 的保留前缀(截断后、未平衡)。
|
|
264
|
+
* @returns 平衡后的保留前缀(原数组的尾部切片;不修改入参)。
|
|
265
|
+
*/
|
|
266
|
+
declare function balanceRewindPrefix(events: readonly SessionEvent[]): SessionEvent[];
|
|
267
|
+
//#endregion
|
|
244
268
|
//#region src/index.d.ts
|
|
245
269
|
declare module "@deepseek-ai/cordis" {
|
|
246
270
|
interface Context {
|
|
@@ -251,5 +275,5 @@ declare module "@deepseek-ai/cordis" {
|
|
|
251
275
|
/** 注册类型(无运行时副作用;服务由具体后端插件发布)。 */
|
|
252
276
|
declare function apply(_ctx: Context): void;
|
|
253
277
|
//#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 };
|
|
278
|
+
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, balanceRewindPrefix, buildTimeline, isSessionBranchVersionEvent };
|
|
255
279
|
//# sourceMappingURL=index.d.mts.map
|
package/lib/index.d.mts.map
CHANGED
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/provider.ts","../src/branch.ts","../src/timeline.ts","../src/balance.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;EACf,YAAY,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;;;;;;;;;;;;;;;;;EAkBX,OACE,IAAI,WACJ,oBACA,SAAS,cACR,QAAQ;;;;;;;;;uBC7ES,sBAAsB;EAC1C,YAAY,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;;;;;;;;;;;;;;;;;iBCZK,oBAAoB,iBAAiB,iBAAiB;;;;YCV1D;;IAER,eAAe;;;;iBAKH,MAAM,MAAM"}
|
package/lib/index.mjs
CHANGED
|
@@ -122,10 +122,43 @@ async function buildTimeline(snapshots, readOwnEvents, sessionId, signal) {
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
//#endregion
|
|
125
|
+
//#region src/balance.ts
|
|
126
|
+
/**
|
|
127
|
+
* 从尾部剔除未配对的 `step/start`,返回平衡后的保留前缀。
|
|
128
|
+
*
|
|
129
|
+
* 从尾部向前扫描:`step/end` 配对它**之前**的 `step/start`(agent-loop 的
|
|
130
|
+
* finally 保证每个 step/start 都有配对 step/end,除非被 rewind 截断);
|
|
131
|
+
* `turn/end` 闭合其轮次,之前的 step 全部合法。未配对(孤儿)的
|
|
132
|
+
* `step/start` 及其后全部剔除——rewind 截断产生的孤儿只可能出现在保留
|
|
133
|
+
* 前缀的尾部(drop 区是后缀),因此从尾部剔除即完整修复。
|
|
134
|
+
*
|
|
135
|
+
* 只剔除**尾部**未配对的 step/start:log 中间(闭合轮次之间)的 step/start
|
|
136
|
+
* 必然有配对,且剔除中间事件会破坏 seq 连续性。
|
|
137
|
+
* @param events - rewind 的保留前缀(截断后、未平衡)。
|
|
138
|
+
* @returns 平衡后的保留前缀(原数组的尾部切片;不修改入参)。
|
|
139
|
+
*/
|
|
140
|
+
function balanceRewindPrefix(events) {
|
|
141
|
+
let keep = events.length;
|
|
142
|
+
let openStepEnds = 0;
|
|
143
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
144
|
+
const type = events[index]?.type;
|
|
145
|
+
if (type === "turn/end") break;
|
|
146
|
+
if (type === "step/end") {
|
|
147
|
+
openStepEnds += 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (type === "step/start") {
|
|
151
|
+
if (openStepEnds > 0) openStepEnds -= 1;
|
|
152
|
+
else keep = index;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return events.slice(0, keep);
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
125
158
|
//#region src/index.ts
|
|
126
159
|
/** 注册类型(无运行时副作用;服务由具体后端插件发布)。 */
|
|
127
160
|
function apply(_ctx) {}
|
|
128
161
|
//#endregion
|
|
129
|
-
export { SESSION_BRANCH_VERSION_SCHEMA, SessionBranch, SessionBranchError, _branchKeyVisible, apply, buildTimeline, isSessionBranchVersionEvent };
|
|
162
|
+
export { SESSION_BRANCH_VERSION_SCHEMA, SessionBranch, SessionBranchError, _branchKeyVisible, apply, balanceRewindPrefix, buildTimeline, isSessionBranchVersionEvent };
|
|
130
163
|
|
|
131
164
|
//# sourceMappingURL=index.mjs.map
|
package/lib/index.mjs.map
CHANGED
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/branch.ts","../src/types.ts","../src/timeline.ts","../src/balance.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 * rewind 保留前缀的平衡化:把截断后残留的未配对 step/start 从尾部剔除。\n *\n * 真实 agent-loop 的 append 顺序是 `turn/start → step/start → user/message →\n * ...`(step/start 在 user/message **之前**)。rewind 到 user/message 边界\n * (exclusive——drop 该消息及其后)时,保留前缀会以**未配对的 step/start**\n * 结尾:它的 step/end 落在 drop 区。token meter 重放要求每个 step/start 都有\n * 配对的 step/end(`step/start` 未闭合时遇到下一个 `step/start` 会抛\n * \"arrived before turn ... ended\"),因此保留前缀必须平衡化——从尾部剔除\n * 未配对的 step/start,使截断后的 log 对任何重放者(token meter / 派生 /\n * 续写)都合法。\n *\n * 本函数是契约层共享纯函数:rdb 的 `rewind`(决定 RDB 删除目标)与编排层\n * (cold 续写的 seq 计算)都基于它,保证「保留前缀」在两层完全一致。\n *\n * @module @morlay/session-branch/balance\n */\n\nimport type { SessionEvent } from \"@deepseek-ai/dsh-session\";\n\n/**\n * 从尾部剔除未配对的 `step/start`,返回平衡后的保留前缀。\n *\n * 从尾部向前扫描:`step/end` 配对它**之前**的 `step/start`(agent-loop 的\n * finally 保证每个 step/start 都有配对 step/end,除非被 rewind 截断);\n * `turn/end` 闭合其轮次,之前的 step 全部合法。未配对(孤儿)的\n * `step/start` 及其后全部剔除——rewind 截断产生的孤儿只可能出现在保留\n * 前缀的尾部(drop 区是后缀),因此从尾部剔除即完整修复。\n *\n * 只剔除**尾部**未配对的 step/start:log 中间(闭合轮次之间)的 step/start\n * 必然有配对,且剔除中间事件会破坏 seq 连续性。\n * @param events - rewind 的保留前缀(截断后、未平衡)。\n * @returns 平衡后的保留前缀(原数组的尾部切片;不修改入参)。\n */\nexport function balanceRewindPrefix(events: readonly SessionEvent[]): SessionEvent[] {\n let keep = events.length;\n // 从尾部向前待配对的 step/end 数(每个配对它之前的 step/start)。\n let openStepEnds = 0;\n for (let index = events.length - 1; index >= 0; index -= 1) {\n const type = events[index]?.type;\n if (type === \"turn/end\") break; // 轮次闭合:之前的 step 全部配对。\n if (type === \"step/end\") {\n openStepEnds += 1;\n continue;\n }\n if (type === \"step/start\") {\n if (openStepEnds > 0) {\n openStepEnds -= 1; // 配对成功。\n } else {\n keep = index; // 孤儿 step/start:剔除它及其后。\n }\n }\n }\n return events.slice(0, keep);\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 { balanceRewindPrefix } from \"./balance.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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"}
|
package/lib/invariant.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invariant.d.mts","names":[],"sources":["../src/invariant.ts"],"mappings":";;;cAWa;;cAEA;;;;;;cAcA,
|
|
1
|
+
{"version":3,"file":"invariant.d.mts","names":[],"sources":["../src/invariant.ts"],"mappings":";;;cAWa;;cAEA;;;;;;cAcA,QAAK,KAAS,YAAU"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/session-branch",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Provider abstraction + high-level service for rewind / retry / fork over DeepSeek Harness event-sourced sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
36
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
37
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
38
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.
|
|
36
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
37
|
+
"@deepseek-ai/dsh-session": "^0.1.1-rc.2",
|
|
38
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2"
|
|
39
39
|
},
|
|
40
40
|
"dsh": {
|
|
41
41
|
"bundle": {
|