@morlay/session-branch 0.0.8 → 0.0.9-alpha.1

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 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` 的 [docs/branch.md](../session-rdb/docs/branch.md))。
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
- > 版本效果事件携带 `ignorable: true`:非 branch 读者凭信封安全跳过,事件
46
- > 本身原样落库——live 会话从内存 log 读到效果,cold 会话经读路径恢复效果
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
- declare function balanceRewindPrefix(events: readonly SessionEvent[]): SessionEvent[];
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 = events.length - 1; index >= 0; index -= 1) {
90
- const type = events[index]?.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 keep = index;
128
+ else relativeKeep = index;
99
129
  }
100
130
  }
101
- return events.slice(0, keep);
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.8",
3
+ "version": "0.0.9-alpha.1",
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/better-session.git"
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.5-rc.2",
34
- "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
35
- "@deepseek-ai/dsh-session-persistence": "^0.1.5-rc.2"
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 function balanceRewindPrefix(events: readonly SessionEvent[]): SessionEvent[] {
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
- // 从尾部向前待配对的 step/end 数(每个配对它之前的 step/start)。
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 = events.length - 1; index >= 0; index -= 1) {
8
- const type = events[index]?.type;
9
- if (type === "turn/end") break; // 轮次闭合:之前的 step 全部配对。
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
- openStepEnds -= 1; // 配对成功。
17
- } else {
18
- keep = index; // 孤儿 step/start:剔除它及其后。
19
- }
59
+ if (openStepEnds > 0) openStepEnds -= 1;
60
+ else relativeKeep = index;
20
61
  }
21
62
  }
22
- return events.slice(0, keep);
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
- // 结构化守卫 + find(版本事件类型不在固化的 SessionEventType 中)。
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