@morlay/session-branch 0.0.9 → 0.0.10-alpha.2
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 +18 -20
- package/cordis.patch.yml +3 -0
- package/dist/index.d.mts +123 -0
- package/{lib → dist}/index.mjs +51 -45
- package/dist/invariant.d.mts +7 -0
- package/dist/invariant.mjs +8 -0
- package/package.json +15 -16
- package/src/balance.ts +64 -0
- package/src/branch.ts +31 -0
- package/src/index.ts +17 -0
- package/src/invariant.ts +13 -0
- package/src/provider.ts +28 -0
- package/src/timeline.ts +120 -0
- package/src/types.ts +122 -0
- package/lib/index.d.mts +0 -255
- package/lib/index.d.mts.map +0 -1
- package/lib/index.mjs.map +0 -1
- package/lib/invariant.d.mts +0 -15
- package/lib/invariant.d.mts.map +0 -1
- package/lib/invariant.mjs +0 -22
- package/lib/invariant.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -4,18 +4,25 @@
|
|
|
4
4
|
(rewind / forkFrom / readBranchPrefix)、高层服务 `SessionBranch`
|
|
5
5
|
(`ctx.sessionBranch`)与共享的版本树投影 `buildTimeline`。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
跨包术语见根 [`.agents/CONTEXT.md`](../../../.agents/CONTEXT.md),本包的决策见 [`.agents/adrs/`](./.agents/adrs)。
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
## 与上游 `SessionHandle` 的关系
|
|
10
|
+
|
|
11
|
+
上游持久化模型是 `SessionHandle`(append-only:`create` / `open` / `append` /
|
|
12
|
+
`flush` / `stat` / `list`),没有显式回退原语;本包在其旁边定义**分支面**
|
|
13
|
+
provider,负责显式回退与闭合边界派生:
|
|
14
|
+
|
|
15
|
+
| 面 | 上游 `SessionHandle` | 本包 `SessionBranchProvider` |
|
|
16
|
+
| ------ | -------------------------------------- | ------------------------------------------------------ |
|
|
17
|
+
| 覆盖 | 持久读写(append-only) | 显式回退 + 闭合边界派生(分支面) |
|
|
18
|
+
| 原语 | `create` / `open` / `append` / `flush` | `readBranchPrefix` / `forkFrom` / `rewind` |
|
|
19
|
+
| 实现方 | JSONL / RDB 等 | RDB 等(`@morlay/session-rdb` 同时实现两者,形成闭环) |
|
|
14
20
|
|
|
15
21
|
## Provider 抽象
|
|
16
22
|
|
|
17
23
|
```ts
|
|
18
24
|
interface SessionBranchProvider {
|
|
25
|
+
readonly name: string;
|
|
19
26
|
readBranchPrefix(id, atSeq?, mode?, signal?): Promise<BranchBoundary>;
|
|
20
27
|
forkFrom(sourceId, options?, signal?): Promise<SessionId>;
|
|
21
28
|
rewind(id, toBoundary, signal?): Promise<SessionPersistenceSnapshot>;
|
|
@@ -27,12 +34,9 @@ interface SessionBranchProvider {
|
|
|
27
34
|
重掷 / 重试语义)。
|
|
28
35
|
- `forkFrom`:纯 append——新会话(`parentSession` / `seedLength`),seed =
|
|
29
36
|
边界前缀 + `seedSuffix`,不触碰源会话。
|
|
30
|
-
- `rewind
|
|
31
|
-
|
|
32
|
-
`@morlay/session-rdb` 的
|
|
33
|
-
- `syncLiveCursor(sessionId)`:编排层把 ignorable 版本效果 push 进 live log
|
|
34
|
-
后(不发布、不进 write-behind 缓冲),用其对齐 coordinator cursor,避免
|
|
35
|
-
后续 append 的 seq 校验错位(默认空实现,rdb 覆写)。
|
|
37
|
+
- `rewind`:唯一的显式回退原语,事务整体提交或回滚;**支持 live 会话**
|
|
38
|
+
(内存 log 截断 + 派生缓存复位 + handle cursor / 继承前缀对齐,详见
|
|
39
|
+
`@morlay/session-rdb` 的 [分支能力](../session-rdb/.agents/designs/20260917-分支能力.md))。
|
|
36
40
|
|
|
37
41
|
## 版本树
|
|
38
42
|
|
|
@@ -40,11 +44,5 @@ interface SessionBranchProvider {
|
|
|
40
44
|
lineage)+ 每会话自有后缀(`seq >= seedLength` 的 `session-branch/version`
|
|
41
45
|
事件)投影完整版本树。
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
## 安装
|
|
47
|
-
|
|
48
|
-
```sh
|
|
49
|
-
nub install @morlay/session-branch # peer: @deepseek-ai/cordis, dsh-session, dsh-session-persistence
|
|
50
|
-
```
|
|
47
|
+
版本效果事件携带 `ignorable: true` 并**原样落库**,因此 cold 会话也能恢复效果详情
|
|
48
|
+
(见 [ADR-版本效果以ignorable事件原样落库](./.agents/adrs/20260917-版本效果以ignorable事件原样落库.md))。
|
package/cordis.patch.yml
ADDED
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
declare const SESSION_BRANCH_VERSION_SCHEMA = 1;
|
|
8
|
+
type CascadePolicy = "truncate" | "preserve";
|
|
9
|
+
type VersionOperation = "edit" | "reroll" | "retry" | "fork" | "rewind";
|
|
10
|
+
type EditableBlockKind = "user" | "assistant.reasoning" | "assistant.response";
|
|
11
|
+
interface SessionBranchEffect {
|
|
12
|
+
id: string;
|
|
13
|
+
operation: VersionOperation;
|
|
14
|
+
cascade: CascadePolicy;
|
|
15
|
+
targetTurn: number;
|
|
16
|
+
targetEventSeq: number;
|
|
17
|
+
targetBlockIndex?: number;
|
|
18
|
+
blockKind?: EditableBlockKind;
|
|
19
|
+
before?: string;
|
|
20
|
+
after?: string;
|
|
21
|
+
}
|
|
22
|
+
interface SessionBranchInverse {
|
|
23
|
+
kind: "restore-version";
|
|
24
|
+
sessionId: SessionId;
|
|
25
|
+
}
|
|
26
|
+
interface SessionBranchVersionEvent {
|
|
27
|
+
schemaVersion: typeof SESSION_BRANCH_VERSION_SCHEMA;
|
|
28
|
+
effect: SessionBranchEffect;
|
|
29
|
+
inverse: SessionBranchInverse;
|
|
30
|
+
}
|
|
31
|
+
declare module "@deepseek-ai/dsh-session" {
|
|
32
|
+
interface SessionEventMap {
|
|
33
|
+
"session-branch/version": SessionBranchVersionEvent;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
interface BranchBoundary {
|
|
37
|
+
seq: number;
|
|
38
|
+
events: readonly SessionEvent[];
|
|
39
|
+
}
|
|
40
|
+
interface BranchForkMeta {
|
|
41
|
+
cwd?: string;
|
|
42
|
+
createdAt?: number;
|
|
43
|
+
agentPreset?: string;
|
|
44
|
+
origin?: "subagent";
|
|
45
|
+
delegationDepth?: number;
|
|
46
|
+
}
|
|
47
|
+
interface ForkFromOptions {
|
|
48
|
+
atSeq?: number;
|
|
49
|
+
anchorMode?: BranchAnchorMode;
|
|
50
|
+
seedSuffix?: readonly SessionEvent[];
|
|
51
|
+
childSessionId?: SessionId;
|
|
52
|
+
meta?: BranchForkMeta;
|
|
53
|
+
}
|
|
54
|
+
interface BranchVersionNode {
|
|
55
|
+
sessionId: SessionId;
|
|
56
|
+
parentSessionId?: SessionId;
|
|
57
|
+
seedLength: number;
|
|
58
|
+
createdAt: number;
|
|
59
|
+
effect?: SessionBranchEffect;
|
|
60
|
+
inverseSessionId?: SessionId;
|
|
61
|
+
}
|
|
62
|
+
interface BranchTimeline {
|
|
63
|
+
root: BranchVersionNode;
|
|
64
|
+
nodes: BranchVersionNode[];
|
|
65
|
+
}
|
|
66
|
+
type SessionBranchErrorCode = "SESSION_NOT_FOUND" | "INVALID_BOUNDARY" | "OPEN_TURN" | "FORK_UNAVAILABLE" | "REWIND_CONFLICT";
|
|
67
|
+
declare class SessionBranchError extends Error {
|
|
68
|
+
readonly code: SessionBranchErrorCode;
|
|
69
|
+
constructor(message: string, code: SessionBranchErrorCode);
|
|
70
|
+
}
|
|
71
|
+
interface SessionBranchVersionEventEnvelope {
|
|
72
|
+
type: "session-branch/version";
|
|
73
|
+
seq: number;
|
|
74
|
+
time: number;
|
|
75
|
+
ignorable?: true;
|
|
76
|
+
data: SessionBranchVersionEvent;
|
|
77
|
+
}
|
|
78
|
+
declare function isSessionBranchVersionEvent(event: SessionEvent | {
|
|
79
|
+
type: string;
|
|
80
|
+
data: unknown;
|
|
81
|
+
}): event is SessionBranchVersionEventEnvelope;
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/provider.d.ts
|
|
84
|
+
type BranchAnchorMode = "after" | "before";
|
|
85
|
+
interface SessionBranchProvider {
|
|
86
|
+
readonly name: string;
|
|
87
|
+
readBranchPrefix(id: SessionId, atSeq?: number, mode?: BranchAnchorMode, signal?: AbortSignal): Promise<BranchBoundary>;
|
|
88
|
+
forkFrom(sourceId: SessionId, options?: ForkFromOptions, signal?: AbortSignal): Promise<SessionId>;
|
|
89
|
+
rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<SessionPersistenceSnapshot>;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/branch.d.ts
|
|
93
|
+
declare abstract class SessionBranch extends Service {
|
|
94
|
+
constructor(ctx: import("@deepseek-ai/cordis").Context);
|
|
95
|
+
abstract readBranchPrefix(id: SessionId, atSeq?: number, mode?: BranchAnchorMode, signal?: AbortSignal): Promise<BranchBoundary>;
|
|
96
|
+
abstract forkFrom(sourceId: SessionId, options?: ForkFromOptions, signal?: AbortSignal): Promise<SessionId>;
|
|
97
|
+
abstract rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<SessionPersistenceSnapshot>;
|
|
98
|
+
abstract timeline(sessionId: SessionId, signal?: AbortSignal): Promise<BranchTimeline>;
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/timeline.d.ts
|
|
102
|
+
type BranchSnapshot = SessionPersistenceSnapshot & {
|
|
103
|
+
inheritedEventCount: number;
|
|
104
|
+
};
|
|
105
|
+
type OwnEventsReader = (id: SessionId, fromSeq: number, signal?: AbortSignal) => Promise<readonly import("@deepseek-ai/dsh-session").SessionEvent[]>;
|
|
106
|
+
declare function buildTimeline(snapshots: readonly BranchSnapshot[], readOwnEvents: OwnEventsReader, sessionId: SessionId, signal?: AbortSignal): Promise<BranchTimeline>;
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/balance.d.ts
|
|
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;
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/index.d.ts
|
|
116
|
+
declare module "@deepseek-ai/cordis" {
|
|
117
|
+
interface Context {
|
|
118
|
+
sessionBranch: SessionBranch;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
declare function apply(_ctx: Context): void;
|
|
122
|
+
//#endregion
|
|
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/{lib → dist}/index.mjs
RENAMED
|
@@ -1,37 +1,14 @@
|
|
|
1
1
|
import { Service } from "@deepseek-ai/cordis";
|
|
2
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
3
|
var SessionBranch = class extends Service {
|
|
17
4
|
constructor(ctx) {
|
|
18
5
|
super(ctx, "sessionBranch");
|
|
19
6
|
}
|
|
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
7
|
};
|
|
29
8
|
//#endregion
|
|
30
9
|
//#region src/types.ts
|
|
31
10
|
const _branchKeyVisible = true;
|
|
32
|
-
/** 版本效果事件的数据结构版本(独立于 session 的 SESSION_FORMAT_VERSION)。 */
|
|
33
11
|
const SESSION_BRANCH_VERSION_SCHEMA = 1;
|
|
34
|
-
/** 分支操作的 typed error。 */
|
|
35
12
|
var SessionBranchError = class extends Error {
|
|
36
13
|
code;
|
|
37
14
|
constructor(message, code) {
|
|
@@ -40,28 +17,11 @@ var SessionBranchError = class extends Error {
|
|
|
40
17
|
this.code = code;
|
|
41
18
|
}
|
|
42
19
|
};
|
|
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
20
|
function isSessionBranchVersionEvent(event) {
|
|
54
21
|
return event.type === "session-branch/version" && event.data.schemaVersion === 1;
|
|
55
22
|
}
|
|
56
23
|
//#endregion
|
|
57
24
|
//#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
25
|
async function buildTimeline(snapshots, readOwnEvents, sessionId, signal) {
|
|
66
26
|
const byId = new Map(snapshots.map((snapshot) => [snapshot.header.id, snapshot]));
|
|
67
27
|
const ancestors = [];
|
|
@@ -93,7 +53,7 @@ async function buildTimeline(snapshots, readOwnEvents, sessionId, signal) {
|
|
|
93
53
|
const node = {
|
|
94
54
|
sessionId: header.id,
|
|
95
55
|
...header.parentSession === void 0 ? {} : { parentSessionId: header.parentSession },
|
|
96
|
-
seedLength:
|
|
56
|
+
seedLength: snapshot.inheritedEventCount ?? 0,
|
|
97
57
|
createdAt: header.createdAt
|
|
98
58
|
};
|
|
99
59
|
if (header.parentSession !== void 0) {
|
|
@@ -122,10 +82,56 @@ async function buildTimeline(snapshots, readOwnEvents, sessionId, signal) {
|
|
|
122
82
|
};
|
|
123
83
|
}
|
|
124
84
|
//#endregion
|
|
85
|
+
//#region src/balance.ts
|
|
86
|
+
function balanceRewindPrefix(events, options = {}) {
|
|
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;
|
|
118
|
+
let openStepEnds = 0;
|
|
119
|
+
for (let index = types.length - 1; index >= 0; index -= 1) {
|
|
120
|
+
const type = types[index];
|
|
121
|
+
if (type === "turn/end") break;
|
|
122
|
+
if (type === "step/end") {
|
|
123
|
+
openStepEnds += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (type === "step/start") {
|
|
127
|
+
if (openStepEnds > 0) openStepEnds -= 1;
|
|
128
|
+
else relativeKeep = index;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return Math.max(0, rawKeepLength - types.length + relativeKeep);
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
125
134
|
//#region src/index.ts
|
|
126
|
-
/** 注册类型(无运行时副作用;服务由具体后端插件发布)。 */
|
|
127
135
|
function apply(_ctx) {}
|
|
128
136
|
//#endregion
|
|
129
|
-
export { SESSION_BRANCH_VERSION_SCHEMA, SessionBranch, SessionBranchError, _branchKeyVisible, apply, buildTimeline, isSessionBranchVersionEvent };
|
|
130
|
-
|
|
131
|
-
//# sourceMappingURL=index.mjs.map
|
|
137
|
+
export { SESSION_BRANCH_VERSION_SCHEMA, SessionBranch, SessionBranchError, _branchKeyVisible, apply, balanceRewindPrefix, buildTimeline, isSessionBranchVersionEvent, rewindKeepLength };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
//#region src/invariant.d.ts
|
|
3
|
+
declare const name = "session-branch-invariant";
|
|
4
|
+
declare const inject: string[];
|
|
5
|
+
declare const apply: (ctx: Context) => Promise<() => void>;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/invariant.ts
|
|
2
|
+
const PACKAGE_NAME = "@morlay/session-branch";
|
|
3
|
+
const name = "session-branch-invariant";
|
|
4
|
+
const inject = ["invariants"];
|
|
5
|
+
const install = () => {};
|
|
6
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
7
|
+
//#endregion
|
|
8
|
+
export { apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/session-branch",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10-alpha.2",
|
|
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,34 +13,33 @@
|
|
|
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",
|
|
20
|
+
"src",
|
|
21
|
+
"cordis.patch.yml",
|
|
22
|
+
"!**/__tests__"
|
|
20
23
|
],
|
|
21
24
|
"type": "module",
|
|
22
25
|
"exports": {
|
|
23
|
-
".":
|
|
24
|
-
|
|
25
|
-
"default": "./lib/index.mjs"
|
|
26
|
-
},
|
|
27
|
-
"./invariant": {
|
|
28
|
-
"types": "./lib/invariant.d.mts",
|
|
29
|
-
"default": "./lib/invariant.mjs"
|
|
30
|
-
},
|
|
26
|
+
".": "./dist/index.mjs",
|
|
27
|
+
"./invariant": "./dist/invariant.mjs",
|
|
31
28
|
"./package.json": "./package.json",
|
|
32
29
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
33
30
|
},
|
|
34
31
|
"peerDependencies": {
|
|
35
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
36
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
37
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
38
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.
|
|
32
|
+
"@deepseek-ai/cordis": "^4.0.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"
|
|
39
36
|
},
|
|
40
37
|
"dsh": {
|
|
41
38
|
"bundle": {
|
|
42
39
|
"patch": "./cordis.patch.yml"
|
|
43
40
|
}
|
|
44
41
|
},
|
|
45
|
-
"scripts": {
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "pnpm exec tsdown"
|
|
44
|
+
}
|
|
46
45
|
}
|
package/src/balance.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { SessionEvent } from "@deepseek-ai/dsh-session";
|
|
2
|
+
|
|
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[] {
|
|
15
|
+
let keep = events.length;
|
|
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;
|
|
50
|
+
let openStepEnds = 0;
|
|
51
|
+
for (let index = types.length - 1; index >= 0; index -= 1) {
|
|
52
|
+
const type = types[index];
|
|
53
|
+
if (type === "turn/end") break;
|
|
54
|
+
if (type === "step/end") {
|
|
55
|
+
openStepEnds += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (type === "step/start") {
|
|
59
|
+
if (openStepEnds > 0) openStepEnds -= 1;
|
|
60
|
+
else relativeKeep = index;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return Math.max(0, rawKeepLength - types.length + relativeKeep);
|
|
64
|
+
}
|
package/src/branch.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
3
|
+
import type { SessionPersistenceSnapshot } from "@deepseek-ai/dsh-session-persistence";
|
|
4
|
+
import type { BranchBoundary, BranchTimeline, ForkFromOptions } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export abstract class SessionBranch extends Service {
|
|
7
|
+
constructor(ctx: import("@deepseek-ai/cordis").Context) {
|
|
8
|
+
super(ctx, "sessionBranch");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
abstract readBranchPrefix(
|
|
12
|
+
id: SessionId,
|
|
13
|
+
atSeq?: number,
|
|
14
|
+
mode?: import("./provider.ts").BranchAnchorMode,
|
|
15
|
+
signal?: AbortSignal,
|
|
16
|
+
): Promise<BranchBoundary>;
|
|
17
|
+
|
|
18
|
+
abstract forkFrom(
|
|
19
|
+
sourceId: SessionId,
|
|
20
|
+
options?: ForkFromOptions,
|
|
21
|
+
signal?: AbortSignal,
|
|
22
|
+
): Promise<SessionId>;
|
|
23
|
+
|
|
24
|
+
abstract rewind(
|
|
25
|
+
id: SessionId,
|
|
26
|
+
toBoundary: number,
|
|
27
|
+
signal?: AbortSignal,
|
|
28
|
+
): Promise<SessionPersistenceSnapshot>;
|
|
29
|
+
|
|
30
|
+
abstract timeline(sessionId: SessionId, signal?: AbortSignal): Promise<BranchTimeline>;
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import { SessionBranch } from "./branch.ts";
|
|
3
|
+
|
|
4
|
+
export { SessionBranch } from "./branch.ts";
|
|
5
|
+
export type { SessionBranchProvider, BranchAnchorMode } from "./provider.ts";
|
|
6
|
+
export { buildTimeline } from "./timeline.ts";
|
|
7
|
+
export type { OwnEventsReader } from "./timeline.ts";
|
|
8
|
+
export { balanceRewindPrefix, rewindKeepLength } from "./balance.ts";
|
|
9
|
+
export * from "./types.ts";
|
|
10
|
+
|
|
11
|
+
declare module "@deepseek-ai/cordis" {
|
|
12
|
+
interface Context {
|
|
13
|
+
sessionBranch: SessionBranch;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function apply(_ctx: Context): void {}
|
package/src/invariant.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import type { InvariantInstaller } from "@deepseek-ai/dsh-invariants";
|
|
3
|
+
|
|
4
|
+
const PACKAGE_NAME = "@morlay/session-branch";
|
|
5
|
+
|
|
6
|
+
export const name = "session-branch-invariant";
|
|
7
|
+
|
|
8
|
+
export const inject = ["invariants"];
|
|
9
|
+
|
|
10
|
+
const install: InvariantInstaller = () => {};
|
|
11
|
+
|
|
12
|
+
export const apply = (ctx: Context): Promise<() => void> =>
|
|
13
|
+
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
2
|
+
import type { SessionPersistenceSnapshot } from "@deepseek-ai/dsh-session-persistence";
|
|
3
|
+
import type { BranchBoundary, ForkFromOptions } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export type BranchAnchorMode = "after" | "before";
|
|
6
|
+
|
|
7
|
+
export interface SessionBranchProvider {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
|
|
10
|
+
readBranchPrefix(
|
|
11
|
+
id: SessionId,
|
|
12
|
+
atSeq?: number,
|
|
13
|
+
mode?: BranchAnchorMode,
|
|
14
|
+
signal?: AbortSignal,
|
|
15
|
+
): Promise<BranchBoundary>;
|
|
16
|
+
|
|
17
|
+
forkFrom(
|
|
18
|
+
sourceId: SessionId,
|
|
19
|
+
options?: ForkFromOptions,
|
|
20
|
+
signal?: AbortSignal,
|
|
21
|
+
): Promise<SessionId>;
|
|
22
|
+
|
|
23
|
+
rewind(
|
|
24
|
+
id: SessionId,
|
|
25
|
+
toBoundary: number,
|
|
26
|
+
signal?: AbortSignal,
|
|
27
|
+
): Promise<SessionPersistenceSnapshot>;
|
|
28
|
+
}
|
package/src/timeline.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
2
|
+
import type { SessionPersistenceSnapshot } from "@deepseek-ai/dsh-session-persistence";
|
|
3
|
+
import {
|
|
4
|
+
SessionBranchError,
|
|
5
|
+
isSessionBranchVersionEvent,
|
|
6
|
+
type BranchTimeline,
|
|
7
|
+
type BranchVersionNode,
|
|
8
|
+
type SessionBranchVersionEventEnvelope,
|
|
9
|
+
} from "./types.ts";
|
|
10
|
+
|
|
11
|
+
export type BranchSnapshot = SessionPersistenceSnapshot & {
|
|
12
|
+
inheritedEventCount: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type OwnEventsReader = (
|
|
16
|
+
id: SessionId,
|
|
17
|
+
fromSeq: number,
|
|
18
|
+
signal?: AbortSignal,
|
|
19
|
+
) => Promise<readonly import("@deepseek-ai/dsh-session").SessionEvent[]>;
|
|
20
|
+
|
|
21
|
+
export async function buildTimeline(
|
|
22
|
+
snapshots: readonly BranchSnapshot[],
|
|
23
|
+
readOwnEvents: OwnEventsReader,
|
|
24
|
+
sessionId: SessionId,
|
|
25
|
+
signal?: AbortSignal,
|
|
26
|
+
): Promise<BranchTimeline> {
|
|
27
|
+
const byId = new Map(snapshots.map((snapshot) => [snapshot.header.id, snapshot] as const));
|
|
28
|
+
|
|
29
|
+
const ancestors: SessionId[] = [];
|
|
30
|
+
let cursor: SessionId | undefined = sessionId;
|
|
31
|
+
const seen = new Set<SessionId>();
|
|
32
|
+
while (cursor !== undefined) {
|
|
33
|
+
if (seen.has(cursor))
|
|
34
|
+
throw new SessionBranchError("lineage contains a cycle", "INVALID_BOUNDARY");
|
|
35
|
+
seen.add(cursor);
|
|
36
|
+
ancestors.push(cursor);
|
|
37
|
+
const snapshot = byId.get(cursor);
|
|
38
|
+
cursor = snapshot?.header.parentSession;
|
|
39
|
+
}
|
|
40
|
+
const rootId = ancestors.at(-1);
|
|
41
|
+
if (rootId === undefined) {
|
|
42
|
+
throw new SessionBranchError(`session "${sessionId}" is not persisted`, "SESSION_NOT_FOUND");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ordered: SessionId[] = [];
|
|
46
|
+
const queue: SessionId[] = [rootId];
|
|
47
|
+
while (queue.length > 0) {
|
|
48
|
+
const id = queue.shift();
|
|
49
|
+
if (id === undefined) continue;
|
|
50
|
+
ordered.push(id);
|
|
51
|
+
const children = snapshots
|
|
52
|
+
.filter((snapshot) => snapshot.header.parentSession === id)
|
|
53
|
+
.sort(
|
|
54
|
+
(left, right) =>
|
|
55
|
+
left.header.createdAt - right.header.createdAt ||
|
|
56
|
+
String(left.header.id).localeCompare(String(right.header.id)),
|
|
57
|
+
)
|
|
58
|
+
.map((snapshot) => snapshot.header.id);
|
|
59
|
+
queue.push(...children);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const nodes: BranchVersionNode[] = [];
|
|
63
|
+
const effectIds = new Set<string>();
|
|
64
|
+
for (const id of ordered) {
|
|
65
|
+
const snapshot = byId.get(id);
|
|
66
|
+
if (snapshot === undefined) continue;
|
|
67
|
+
const header = snapshot.header;
|
|
68
|
+
const node: BranchVersionNode = {
|
|
69
|
+
sessionId: header.id,
|
|
70
|
+
...(header.parentSession === undefined ? {} : { parentSessionId: header.parentSession }),
|
|
71
|
+
seedLength: snapshot.inheritedEventCount ?? 0,
|
|
72
|
+
createdAt: header.createdAt,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (header.parentSession !== undefined) {
|
|
76
|
+
const events = await readOwnEvents(header.id, node.seedLength, signal);
|
|
77
|
+
|
|
78
|
+
let version: SessionBranchVersionEventEnvelope | undefined;
|
|
79
|
+
for (const event of events) {
|
|
80
|
+
if (isSessionBranchVersionEvent(event)) {
|
|
81
|
+
if (version !== undefined) {
|
|
82
|
+
throw new SessionBranchError(
|
|
83
|
+
`session ${header.id} carries multiple own version effects`,
|
|
84
|
+
"INVALID_BOUNDARY",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
version = event;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (version !== undefined) {
|
|
91
|
+
const data = version.data;
|
|
92
|
+
if (
|
|
93
|
+
data.inverse.kind !== "restore-version" ||
|
|
94
|
+
data.inverse.sessionId !== header.parentSession
|
|
95
|
+
) {
|
|
96
|
+
throw new SessionBranchError(
|
|
97
|
+
`session ${header.id} version inverse does not match its parent`,
|
|
98
|
+
"INVALID_BOUNDARY",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (effectIds.has(data.effect.id)) {
|
|
102
|
+
throw new SessionBranchError(
|
|
103
|
+
`version effect ${data.effect.id} is duplicated`,
|
|
104
|
+
"INVALID_BOUNDARY",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
effectIds.add(data.effect.id);
|
|
108
|
+
node.effect = data.effect;
|
|
109
|
+
node.inverseSessionId = data.inverse.sessionId;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
nodes.push(node);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const root = nodes.find((node) => node.parentSessionId === undefined);
|
|
116
|
+
if (root === undefined) {
|
|
117
|
+
throw new SessionBranchError(`session "${sessionId}" lineage has no root`, "SESSION_NOT_FOUND");
|
|
118
|
+
}
|
|
119
|
+
return { root, nodes };
|
|
120
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
|
|
2
|
+
import type { SessionEventMap } from "@deepseek-ai/dsh-session";
|
|
3
|
+
|
|
4
|
+
type _BranchKeyCheck = "session-branch/version" extends keyof SessionEventMap ? true : false;
|
|
5
|
+
export const _branchKeyVisible: _BranchKeyCheck = true as const;
|
|
6
|
+
|
|
7
|
+
export const SESSION_BRANCH_VERSION_SCHEMA = 1;
|
|
8
|
+
|
|
9
|
+
export type CascadePolicy = "truncate" | "preserve";
|
|
10
|
+
|
|
11
|
+
export type VersionOperation = "edit" | "reroll" | "retry" | "fork" | "rewind";
|
|
12
|
+
|
|
13
|
+
export type EditableBlockKind = "user" | "assistant.reasoning" | "assistant.response";
|
|
14
|
+
|
|
15
|
+
export interface SessionBranchEffect {
|
|
16
|
+
id: string;
|
|
17
|
+
operation: VersionOperation;
|
|
18
|
+
cascade: CascadePolicy;
|
|
19
|
+
|
|
20
|
+
targetTurn: number;
|
|
21
|
+
|
|
22
|
+
targetEventSeq: number;
|
|
23
|
+
targetBlockIndex?: number;
|
|
24
|
+
blockKind?: EditableBlockKind;
|
|
25
|
+
|
|
26
|
+
before?: string;
|
|
27
|
+
|
|
28
|
+
after?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SessionBranchInverse {
|
|
32
|
+
kind: "restore-version";
|
|
33
|
+
sessionId: SessionId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SessionBranchVersionEvent {
|
|
37
|
+
schemaVersion: typeof SESSION_BRANCH_VERSION_SCHEMA;
|
|
38
|
+
effect: SessionBranchEffect;
|
|
39
|
+
inverse: SessionBranchInverse;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare module "@deepseek-ai/dsh-session" {
|
|
43
|
+
interface SessionEventMap {
|
|
44
|
+
"session-branch/version": SessionBranchVersionEvent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BranchBoundary {
|
|
49
|
+
seq: number;
|
|
50
|
+
|
|
51
|
+
events: readonly SessionEvent[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface BranchForkMeta {
|
|
55
|
+
cwd?: string;
|
|
56
|
+
createdAt?: number;
|
|
57
|
+
agentPreset?: string;
|
|
58
|
+
origin?: "subagent";
|
|
59
|
+
delegationDepth?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ForkFromOptions {
|
|
63
|
+
atSeq?: number;
|
|
64
|
+
|
|
65
|
+
anchorMode?: import("./provider.ts").BranchAnchorMode;
|
|
66
|
+
|
|
67
|
+
seedSuffix?: readonly SessionEvent[];
|
|
68
|
+
|
|
69
|
+
childSessionId?: SessionId;
|
|
70
|
+
|
|
71
|
+
meta?: BranchForkMeta;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface BranchVersionNode {
|
|
75
|
+
sessionId: SessionId;
|
|
76
|
+
parentSessionId?: SessionId;
|
|
77
|
+
|
|
78
|
+
seedLength: number;
|
|
79
|
+
createdAt: number;
|
|
80
|
+
|
|
81
|
+
effect?: SessionBranchEffect;
|
|
82
|
+
|
|
83
|
+
inverseSessionId?: SessionId;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface BranchTimeline {
|
|
87
|
+
root: BranchVersionNode;
|
|
88
|
+
nodes: BranchVersionNode[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type SessionBranchErrorCode =
|
|
92
|
+
| "SESSION_NOT_FOUND"
|
|
93
|
+
| "INVALID_BOUNDARY"
|
|
94
|
+
| "OPEN_TURN"
|
|
95
|
+
| "FORK_UNAVAILABLE"
|
|
96
|
+
| "REWIND_CONFLICT";
|
|
97
|
+
|
|
98
|
+
export class SessionBranchError extends Error {
|
|
99
|
+
readonly code: SessionBranchErrorCode;
|
|
100
|
+
constructor(message: string, code: SessionBranchErrorCode) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.name = "SessionBranchError";
|
|
103
|
+
this.code = code;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface SessionBranchVersionEventEnvelope {
|
|
108
|
+
type: "session-branch/version";
|
|
109
|
+
seq: number;
|
|
110
|
+
time: number;
|
|
111
|
+
ignorable?: true;
|
|
112
|
+
data: SessionBranchVersionEvent;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function isSessionBranchVersionEvent(
|
|
116
|
+
event: SessionEvent | { type: string; data: unknown },
|
|
117
|
+
): event is SessionBranchVersionEventEnvelope {
|
|
118
|
+
return (
|
|
119
|
+
event.type === "session-branch/version" &&
|
|
120
|
+
(event.data as { schemaVersion?: unknown }).schemaVersion === SESSION_BRANCH_VERSION_SCHEMA
|
|
121
|
+
);
|
|
122
|
+
}
|
package/lib/index.d.mts
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
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
|
package/lib/index.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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":";;;;;;;;;;;;;;;AAmBA,IAAsB,gBAAtB,cAA4C,QAAQ;CAClD,YAAY,KAA4C;EACtD,MAAM,KAAK,eAAe;CAC5B;;;;;;;;CAkCA,eAAe,YAA6B,CAE5C;AACF;;;ACzCA,MAAa,oBAAqC;;AAGlD,MAAa,gCAAgC;;AA+H7C,IAAa,qBAAb,cAAwC,MAAM;CAC5C;CACA,YAAY,SAAiB,MAA8B;EACzD,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;;;;;;;;;;;AAuBA,SAAgB,4BACd,OAC4C;CAC5C,OACE,MAAM,SAAS,4BACd,MAAM,KAAqC,kBAAA;AAEhD;;;;;;;;;;AChJA,eAAsB,cACpB,WACA,eACA,WACA,QACyB;CACzB,MAAM,OAAO,IAAI,IAAI,UAAU,KAAK,aAAa,CAAC,SAAS,OAAO,IAAI,QAAQ,CAAU,CAAC;CAGzF,MAAM,YAAyB,CAAC;CAChC,IAAI,SAAgC;CACpC,MAAM,uBAAO,IAAI,IAAe;CAChC,OAAO,WAAW,KAAA,GAAW;EAC3B,IAAI,KAAK,IAAI,MAAM,GACjB,MAAM,IAAI,mBAAmB,4BAA4B,kBAAkB;EAC7E,KAAK,IAAI,MAAM;EACf,UAAU,KAAK,MAAM;EAErB,SADiB,KAAK,IAAI,MACV,CAAC,EAAE,OAAO;CAC5B;CACA,MAAM,SAAS,UAAU,GAAG,EAAE;CAC9B,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,mBAAmB,YAAY,UAAU,qBAAqB,mBAAmB;CAI7F,MAAM,UAAuB,CAAC;CAC9B,MAAM,QAAqB,CAAC,MAAM;CAClC,OAAO,MAAM,SAAS,GAAG;EACvB,MAAM,KAAK,MAAM,MAAM;EACvB,IAAI,OAAO,KAAA,GAAW;EACtB,QAAQ,KAAK,EAAE;EACf,MAAM,WAAW,UACd,QAAQ,aAAa,SAAS,OAAO,kBAAkB,EAAE,CAAC,CAC1D,MACE,MAAM,UACL,KAAK,OAAO,YAAY,MAAM,OAAO,aACrC,OAAO,KAAK,OAAO,EAAE,CAAC,CAAC,cAAc,OAAO,MAAM,OAAO,EAAE,CAAC,CAChE,CAAC,CACA,KAAK,aAAa,SAAS,OAAO,EAAE;EACvC,MAAM,KAAK,GAAG,QAAQ;CACxB;CAEA,MAAM,QAA6B,CAAC;CACpC,MAAM,4BAAY,IAAI,IAAY;CAClC,KAAK,MAAM,MAAM,SAAS;EACxB,MAAM,WAAW,KAAK,IAAI,EAAE;EAC5B,IAAI,aAAa,KAAA,GAAW;EAC5B,MAAM,SAAS,SAAS;EACxB,MAAM,OAA0B;GAC9B,WAAW,OAAO;GAClB,GAAI,OAAO,kBAAkB,KAAA,IAAY,CAAC,IAAI,EAAE,iBAAiB,OAAO,cAAc;GACtF,YAAY,OAAO,cAAc;GACjC,WAAW,OAAO;EACpB;EAEA,IAAI,OAAO,kBAAkB,KAAA,GAAW;GACtC,MAAM,SAAS,MAAM,cAAc,OAAO,IAAI,KAAK,YAAY,MAAM;GAGrE,IAAI;GACJ,KAAK,MAAM,SAAS,QAClB,IAAI,4BAA4B,KAAK,GAAG;IACtC,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,mBACR,WAAW,OAAO,GAAG,wCACrB,kBACF;IAEF,UAAU;GACZ;GAEF,IAAI,YAAY,KAAA,GAAW;IACzB,MAAM,OAAO,QAAQ;IACrB,IACE,KAAK,QAAQ,SAAS,qBACtB,KAAK,QAAQ,cAAc,OAAO,eAElC,MAAM,IAAI,mBACR,WAAW,OAAO,GAAG,6CACrB,kBACF;IAEF,IAAI,UAAU,IAAI,KAAK,OAAO,EAAE,GAC9B,MAAM,IAAI,mBACR,kBAAkB,KAAK,OAAO,GAAG,iBACjC,kBACF;IAEF,UAAU,IAAI,KAAK,OAAO,EAAE;IAC5B,KAAK,SAAS,KAAK;IACnB,KAAK,mBAAmB,KAAK,QAAQ;GACvC;EACF;EACA,MAAM,KAAK,IAAI;CACjB;CAEA,MAAM,OAAO,MAAM,MAAM,SAAS,KAAK,oBAAoB,KAAA,CAAS;CACpE,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,mBAAmB,YAAY,UAAU,wBAAwB,mBAAmB;CAEhG,OAAO;EAAE;EAAM;CAAM;AACvB;;;;ACjHA,SAAgB,MAAM,MAAqB,CAK3C"}
|
package/lib/invariant.d.mts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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
|
package/lib/invariant.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"invariant.d.mts","names":[],"sources":["../src/invariant.ts"],"mappings":";;;cAWa;;cAEA;;;;;;cAcA,QAAS,KAAK,YAAU"}
|
package/lib/invariant.mjs
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
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
|
package/lib/invariant.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"}
|