@morlay/session-branch 0.0.7 → 0.0.9-alpha.0
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/README.md +5 -4
- package/dist/index.d.mts +6 -2
- package/dist/index.mjs +36 -6
- package/package.json +5 -5
- package/src/balance.ts +52 -11
- package/src/index.ts +2 -5
- package/src/timeline.ts +2 -4
- package/src/types.ts +0 -3
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
(rewind / forkFrom / readBranchPrefix)、高层服务 `SessionBranch`
|
|
5
5
|
(`ctx.sessionBranch`)与共享的版本树投影 `buildTimeline`。
|
|
6
6
|
|
|
7
|
+
跨包术语见根 [`.agents/CONTEXT.md`](../../../.agents/CONTEXT.md),本包的决策见 [`.agents/adrs/`](./.agents/adrs)。
|
|
8
|
+
|
|
7
9
|
## 与上游 `SessionHandle` 的关系
|
|
8
10
|
|
|
9
11
|
上游持久化模型是 `SessionHandle`(append-only:`create` / `open` / `append` /
|
|
@@ -34,7 +36,7 @@ interface SessionBranchProvider {
|
|
|
34
36
|
边界前缀 + `seedSuffix`,不触碰源会话。
|
|
35
37
|
- `rewind`:唯一的显式回退原语,事务整体提交或回滚;**支持 live 会话**
|
|
36
38
|
(内存 log 截断 + 派生缓存复位 + handle cursor / 继承前缀对齐,详见
|
|
37
|
-
`@morlay/session-rdb` 的 [
|
|
39
|
+
`@morlay/session-rdb` 的 [分支能力](../session-rdb/.agents/designs/20260917-分支能力.md))。
|
|
38
40
|
|
|
39
41
|
## 版本树
|
|
40
42
|
|
|
@@ -42,6 +44,5 @@ interface SessionBranchProvider {
|
|
|
42
44
|
lineage)+ 每会话自有后缀(`seq >= seedLength` 的 `session-branch/version`
|
|
43
45
|
事件)投影完整版本树。
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
> 详情(见 [ADR 0003](./docs/adr/0003-版本效果以ignorable事件原样落库.md))。
|
|
47
|
+
版本效果事件携带 `ignorable: true` 并**原样落库**,因此 cold 会话也能恢复效果详情
|
|
48
|
+
(见 [ADR-版本效果以ignorable事件原样落库](./.agents/adrs/20260917-版本效果以ignorable事件原样落库.md))。
|
package/dist/index.d.mts
CHANGED
|
@@ -106,7 +106,11 @@ type OwnEventsReader = (id: SessionId, fromSeq: number, signal?: AbortSignal) =>
|
|
|
106
106
|
declare function buildTimeline(snapshots: readonly BranchSnapshot[], readOwnEvents: OwnEventsReader, sessionId: SessionId, signal?: AbortSignal): Promise<BranchTimeline>;
|
|
107
107
|
//#endregion
|
|
108
108
|
//#region src/balance.d.ts
|
|
109
|
-
|
|
109
|
+
interface BalanceRewindPrefixOptions {
|
|
110
|
+
keepOpenTail?: boolean;
|
|
111
|
+
}
|
|
112
|
+
declare function balanceRewindPrefix(events: readonly SessionEvent[], options?: BalanceRewindPrefixOptions): SessionEvent[];
|
|
113
|
+
declare function rewindKeepLength(types: readonly string[], rawKeepLength: number): number;
|
|
110
114
|
//#endregion
|
|
111
115
|
//#region src/index.d.ts
|
|
112
116
|
declare module "@deepseek-ai/cordis" {
|
|
@@ -116,4 +120,4 @@ declare module "@deepseek-ai/cordis" {
|
|
|
116
120
|
}
|
|
117
121
|
declare function apply(_ctx: Context): void;
|
|
118
122
|
//#endregion
|
|
119
|
-
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 };
|
|
123
|
+
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, rewindKeepLength };
|
package/dist/index.mjs
CHANGED
|
@@ -83,11 +83,41 @@ async function buildTimeline(snapshots, readOwnEvents, sessionId, signal) {
|
|
|
83
83
|
}
|
|
84
84
|
//#endregion
|
|
85
85
|
//#region src/balance.ts
|
|
86
|
-
function balanceRewindPrefix(events) {
|
|
86
|
+
function balanceRewindPrefix(events, options = {}) {
|
|
87
87
|
let keep = events.length;
|
|
88
|
+
let openIndex = -1;
|
|
89
|
+
let open;
|
|
90
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
91
|
+
const event = events[index];
|
|
92
|
+
if (event.type === "step/start") {
|
|
93
|
+
if (open !== void 0) {
|
|
94
|
+
keep = index;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
open = {
|
|
98
|
+
turn: event.data.turn,
|
|
99
|
+
step: event.data.step
|
|
100
|
+
};
|
|
101
|
+
openIndex = index;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (event.type === "step/end") {
|
|
105
|
+
if (open === void 0 || open.turn !== event.data.turn || open.step !== event.data.step) {
|
|
106
|
+
keep = index;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
open = void 0;
|
|
110
|
+
openIndex = -1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (options.keepOpenTail !== true && open !== void 0) keep = Math.min(keep, openIndex);
|
|
114
|
+
return events.slice(0, keep);
|
|
115
|
+
}
|
|
116
|
+
function rewindKeepLength(types, rawKeepLength) {
|
|
117
|
+
let relativeKeep = types.length;
|
|
88
118
|
let openStepEnds = 0;
|
|
89
|
-
for (let index =
|
|
90
|
-
const type =
|
|
119
|
+
for (let index = types.length - 1; index >= 0; index -= 1) {
|
|
120
|
+
const type = types[index];
|
|
91
121
|
if (type === "turn/end") break;
|
|
92
122
|
if (type === "step/end") {
|
|
93
123
|
openStepEnds += 1;
|
|
@@ -95,13 +125,13 @@ function balanceRewindPrefix(events) {
|
|
|
95
125
|
}
|
|
96
126
|
if (type === "step/start") {
|
|
97
127
|
if (openStepEnds > 0) openStepEnds -= 1;
|
|
98
|
-
else
|
|
128
|
+
else relativeKeep = index;
|
|
99
129
|
}
|
|
100
130
|
}
|
|
101
|
-
return
|
|
131
|
+
return Math.max(0, rawKeepLength - types.length + relativeKeep);
|
|
102
132
|
}
|
|
103
133
|
//#endregion
|
|
104
134
|
//#region src/index.ts
|
|
105
135
|
function apply(_ctx) {}
|
|
106
136
|
//#endregion
|
|
107
|
-
export { SESSION_BRANCH_VERSION_SCHEMA, SessionBranch, SessionBranchError, _branchKeyVisible, apply, balanceRewindPrefix, buildTimeline, isSessionBranchVersionEvent };
|
|
137
|
+
export { SESSION_BRANCH_VERSION_SCHEMA, SessionBranch, SessionBranchError, _branchKeyVisible, apply, balanceRewindPrefix, buildTimeline, isSessionBranchVersionEvent, rewindKeepLength };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/session-branch",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9-alpha.0",
|
|
4
4
|
"description": "Provider abstraction + high-level service for rewind / retry / fork over DeepSeek Harness event-sourced sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
16
|
-
"url": "https://github.com/morlay/
|
|
16
|
+
"url": "https://github.com/morlay/dsh-plugin.git"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
33
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
34
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
35
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.
|
|
33
|
+
"@deepseek-ai/dsh-invariants": "^0.1.6-alpha.2",
|
|
34
|
+
"@deepseek-ai/dsh-session": "^0.1.6-alpha.2",
|
|
35
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.6-alpha.2"
|
|
36
36
|
},
|
|
37
37
|
"dsh": {
|
|
38
38
|
"bundle": {
|
package/src/balance.ts
CHANGED
|
@@ -1,23 +1,64 @@
|
|
|
1
1
|
import type { SessionEvent } from "@deepseek-ai/dsh-session";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export interface BalanceRewindPrefixOptions {
|
|
4
|
+
// 导出 / 导入的是整段日志:尾部未闭合的 step/start 是中断运行的正常形状,交给上游 resume 补 closers;
|
|
5
|
+
// rewind / fork 之后会紧接着追加新的 step/start,悬空的旧 step/start 会让它报错,必须一并丢弃。
|
|
6
|
+
keepOpenTail?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// 保留前缀配平:每个 step/start 必须与同 turn/step 的 step/end 成对。日志已不平衡(step/end 无配对、
|
|
10
|
+
// step/start 撞未闭合)时从配平点起丢弃尾部(自愈),否则下游 token-meter 折叠会报 step/end 无配对。
|
|
11
|
+
export function balanceRewindPrefix(
|
|
12
|
+
events: readonly SessionEvent[],
|
|
13
|
+
options: BalanceRewindPrefixOptions = {},
|
|
14
|
+
): SessionEvent[] {
|
|
4
15
|
let keep = events.length;
|
|
5
|
-
|
|
16
|
+
let openIndex = -1;
|
|
17
|
+
let open: { turn: number; step: number } | undefined;
|
|
18
|
+
|
|
19
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
20
|
+
const event = events[index]!;
|
|
21
|
+
if (event.type === "step/start") {
|
|
22
|
+
if (open !== undefined) {
|
|
23
|
+
keep = index;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
open = { turn: event.data.turn, step: event.data.step };
|
|
27
|
+
openIndex = index;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (event.type === "step/end") {
|
|
31
|
+
if (open === undefined || open.turn !== event.data.turn || open.step !== event.data.step) {
|
|
32
|
+
keep = index;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
open = undefined;
|
|
36
|
+
openIndex = -1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (options.keepOpenTail !== true && open !== undefined) keep = Math.min(keep, openIndex);
|
|
41
|
+
return events.slice(0, keep);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// rewind 只按 seq 读尾部窗口(`types` 是窗口内升序的事件类型),在窗口上做同样的配对修剪:
|
|
45
|
+
// 从尾部往回扫,遇到 turn/end 即停(它之前的内容与保留前缀的尾部无关);尾部未闭合的
|
|
46
|
+
// step/start 一并丢弃,避免它之后的追加(重放的新 step)撞上未闭合的旧 step。
|
|
47
|
+
// 调用方必须保证窗口覆盖到「最近一个 turn/end」或前缀开头,否则结果不完整。
|
|
48
|
+
export function rewindKeepLength(types: readonly string[], rawKeepLength: number): number {
|
|
49
|
+
let relativeKeep = types.length;
|
|
6
50
|
let openStepEnds = 0;
|
|
7
|
-
for (let index =
|
|
8
|
-
const type =
|
|
9
|
-
if (type === "turn/end") break;
|
|
51
|
+
for (let index = types.length - 1; index >= 0; index -= 1) {
|
|
52
|
+
const type = types[index];
|
|
53
|
+
if (type === "turn/end") break;
|
|
10
54
|
if (type === "step/end") {
|
|
11
55
|
openStepEnds += 1;
|
|
12
56
|
continue;
|
|
13
57
|
}
|
|
14
58
|
if (type === "step/start") {
|
|
15
|
-
if (openStepEnds > 0)
|
|
16
|
-
|
|
17
|
-
} else {
|
|
18
|
-
keep = index; // 孤儿 step/start:剔除它及其后。
|
|
19
|
-
}
|
|
59
|
+
if (openStepEnds > 0) openStepEnds -= 1;
|
|
60
|
+
else relativeKeep = index;
|
|
20
61
|
}
|
|
21
62
|
}
|
|
22
|
-
return
|
|
63
|
+
return Math.max(0, rawKeepLength - types.length + relativeKeep);
|
|
23
64
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { SessionBranch } from "./branch.ts";
|
|
|
5
5
|
export type { SessionBranchProvider, BranchAnchorMode } from "./provider.ts";
|
|
6
6
|
export { buildTimeline } from "./timeline.ts";
|
|
7
7
|
export type { OwnEventsReader } from "./timeline.ts";
|
|
8
|
-
export { balanceRewindPrefix } from "./balance.ts";
|
|
8
|
+
export { balanceRewindPrefix, rewindKeepLength } from "./balance.ts";
|
|
9
9
|
export * from "./types.ts";
|
|
10
10
|
|
|
11
11
|
declare module "@deepseek-ai/cordis" {
|
|
@@ -14,7 +14,4 @@ declare module "@deepseek-ai/cordis" {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function apply(_ctx: Context): void {
|
|
18
|
-
// 契约层不发布服务:ctx.sessionBranch 由实现 provider 的后端插件注册。
|
|
19
|
-
// 保留 apply 仅为 cordis 插件装配兼容,不注入任何服务。
|
|
20
|
-
}
|
|
17
|
+
export function apply(_ctx: Context): void {}
|
package/src/timeline.ts
CHANGED
|
@@ -26,7 +26,6 @@ export async function buildTimeline(
|
|
|
26
26
|
): Promise<BranchTimeline> {
|
|
27
27
|
const byId = new Map(snapshots.map((snapshot) => [snapshot.header.id, snapshot] as const));
|
|
28
28
|
|
|
29
|
-
// 回溯到根(当前会话 → 祖先链)。
|
|
30
29
|
const ancestors: SessionId[] = [];
|
|
31
30
|
let cursor: SessionId | undefined = sessionId;
|
|
32
31
|
const seen = new Set<SessionId>();
|
|
@@ -43,7 +42,6 @@ export async function buildTimeline(
|
|
|
43
42
|
throw new SessionBranchError(`session "${sessionId}" is not persisted`, "SESSION_NOT_FOUND");
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
// 根向下的完整后代(BFS;按 createdAt 稳定排序)。
|
|
47
45
|
const ordered: SessionId[] = [];
|
|
48
46
|
const queue: SessionId[] = [rootId];
|
|
49
47
|
while (queue.length > 0) {
|
|
@@ -73,10 +71,10 @@ export async function buildTimeline(
|
|
|
73
71
|
seedLength: snapshot.inheritedEventCount ?? 0,
|
|
74
72
|
createdAt: header.createdAt,
|
|
75
73
|
};
|
|
76
|
-
|
|
74
|
+
|
|
77
75
|
if (header.parentSession !== undefined) {
|
|
78
76
|
const events = await readOwnEvents(header.id, node.seedLength, signal);
|
|
79
|
-
|
|
77
|
+
|
|
80
78
|
let version: SessionBranchVersionEventEnvelope | undefined;
|
|
81
79
|
for (const event of events) {
|
|
82
80
|
if (isSessionBranchVersionEvent(event)) {
|
package/src/types.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import type { SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
|
|
2
2
|
import type { SessionEventMap } from "@deepseek-ai/dsh-session";
|
|
3
3
|
|
|
4
|
-
// 编译期诊断:module augmentation 后 `keyof SessionEventMap` 可见,但
|
|
5
|
-
// dsh-session 内已解析的 `SessionEventType` 别名不会重求值——因此
|
|
6
|
-
// `SessionEvent<"session-branch/version">` 泛型不可用,守卫/消费走结构化。
|
|
7
4
|
type _BranchKeyCheck = "session-branch/version" extends keyof SessionEventMap ? true : false;
|
|
8
5
|
export const _branchKeyVisible: _BranchKeyCheck = true as const;
|
|
9
6
|
|