@x-otto/session 0.0.1-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 +121 -0
- package/dist/index.d.ts +1285 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1285 @@
|
|
|
1
|
+
import { DiskPersistence, MemoryPersistenceBase, Migration, PaginatedResult, PersistenceShape, RemotePersistence, RemotePersistenceError, RemoteSnapshotConflictError } from "@x-otto/persistence";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
type SessionEntry<T = unknown> = {
|
|
5
|
+
type: 'message';
|
|
6
|
+
id: string;
|
|
7
|
+
parentId?: string;
|
|
8
|
+
message: T;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
} | {
|
|
11
|
+
type: 'compaction';
|
|
12
|
+
id: string;
|
|
13
|
+
parentId?: string;
|
|
14
|
+
summary: string;
|
|
15
|
+
compactedCount: number;
|
|
16
|
+
/**
|
|
17
|
+
* (replacement-history 模型):压缩后保留的"近期消息"快照。
|
|
18
|
+
* 自动压缩(memory keep-recent)写回时填充;buildContext 命中即返回 {summary, [...replacement, ...其后]},
|
|
19
|
+
* 重启原样恢复、不重算。缺省(手动 /compact 的 fold-all,或旧快照)走位置式语义(仅返回压缩条目之后的消息)。
|
|
20
|
+
* **additive optional——不升 SESSION_SNAPSHOT_VERSION**(旧快照无此字段即回退位置式)。
|
|
21
|
+
*/
|
|
22
|
+
replacement?: T[];
|
|
23
|
+
/**
|
|
24
|
+
* RFC-160 D1:**内存态专有**标记——本条目的 replacement 已被驻留治理剥离
|
|
25
|
+
* (历史 compaction 的 replacement 仅末位边界被 buildContext 消费,非末位是纯死重)。
|
|
26
|
+
* 剥离态严禁落库(appendEntry 首行断言拒写)、严禁进 checkpoint/fork 产物(出口断言)、
|
|
27
|
+
* 严禁成为 buildContext 边界(branch() fail-fast);真身按需经 loadEntriesByIds hydrate。
|
|
28
|
+
*/
|
|
29
|
+
replacementStripped?: true;
|
|
30
|
+
timestamp: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 清除边界(`/clear`)。其后 buildContext() 返回真空上下文(无 summary/无消息),
|
|
34
|
+
* visibleMessages() 返回空——但 messages()(完整转录)保留全部,可时间旅行回溯。
|
|
35
|
+
* 与 compaction 正交:compaction 只切投影(转录仍可滚回看),clear 同时切投影+可见转录。
|
|
36
|
+
* **additive optional variant——不升 SESSION_SNAPSHOT_VERSION**(旧快照无此条目即 visibleMessages==messages)。
|
|
37
|
+
*/
|
|
38
|
+
| {
|
|
39
|
+
type: 'clear';
|
|
40
|
+
id: string;
|
|
41
|
+
parentId?: string;
|
|
42
|
+
timestamp: number;
|
|
43
|
+
};
|
|
44
|
+
interface SessionContext<T = unknown> {
|
|
45
|
+
summary?: string;
|
|
46
|
+
messages: T[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 随快照持久化的 per-session 配置子集(全 primitive,不依赖上层 ai 类型)。
|
|
50
|
+
* restore 时优先于共享 configProvider 模板,缺失字段回退模板。**additive optional——不升
|
|
51
|
+
* SESSION_SNAPSHOT_VERSION**(版本门会跳过旧快照;旧快照无此字段即全回退模板)。
|
|
52
|
+
*/
|
|
53
|
+
interface PersistedSessionConfig {
|
|
54
|
+
interactive?: boolean;
|
|
55
|
+
depth?: number;
|
|
56
|
+
agentName?: string;
|
|
57
|
+
thinkingLevel?: string;
|
|
58
|
+
maxToolTurnExtensions?: number;
|
|
59
|
+
transient?: boolean;
|
|
60
|
+
/** 仅记 model.id(forward-compat);restore 重解析需 model 注册表,暂保留模板 model。 */
|
|
61
|
+
modelId?: string;
|
|
62
|
+
/**
|
|
63
|
+
* 引擎 review Tier-2:会话工作目录必须持久化——否则 restore 退回宿主全局 cwd,文件工具
|
|
64
|
+
* 读写错目录(子目录/子会话场景静默数据损坏)。
|
|
65
|
+
*/
|
|
66
|
+
workspaceDir?: string;
|
|
67
|
+
/** 会话级工具回合上限;不存则 restore 退回模板默认。 */
|
|
68
|
+
maxToolTurns?: number;
|
|
69
|
+
/**
|
|
70
|
+
* 会话恒定的 volatile system 尾段(append 子代理角色块等)。随快照持久化——否则 sticky
|
|
71
|
+
* 子代理会话重启后角色块蒸发。全 primitive(string[]),additive-optional 不升版本。
|
|
72
|
+
* 注:基础 systemPrompt 仍不持久化(restore 从模板重建,仍退回 main——既有 restore 保真度限制,
|
|
73
|
+
* 见 docs/design/rfc020-followup-hardening;本字段只补 systemTail 这一新增数据的丢失)。
|
|
74
|
+
*/
|
|
75
|
+
systemTail?: string[];
|
|
76
|
+
/**
|
|
77
|
+
* 会话级「始终允许」工具名(弹窗点「始终允许」的运行时授权)。跟随会话持久化——
|
|
78
|
+
* 一次性信任决策不再泄漏到项目其它会话/永久污染 config,--continue 时回灌 PermissionRegistry
|
|
79
|
+
* 的会话级集合(见 hooks/policy-registry setSessionAlwaysAllow)。additive-optional 不升版本。
|
|
80
|
+
*/
|
|
81
|
+
permissionAlwaysAllow?: string[];
|
|
82
|
+
/**
|
|
83
|
+
* 会话级「账号可用模型 id」集合(实时拉取/刷新确认)。跟随会话持久化、--continue 回灌;
|
|
84
|
+
* list_models 工具据此标记/过滤可用模型供 subagent 选型。additive-optional 不升版本。
|
|
85
|
+
*/
|
|
86
|
+
availableModels?: string[];
|
|
87
|
+
/**
|
|
88
|
+
* 持久化输入历史(PromptInput 的 ↑/↓ 历史导航条目)。
|
|
89
|
+
* max 100 条(最近在末尾);历史搜索面板(/history · Ctrl+R)+ ↑/↓ 导航共用。
|
|
90
|
+
* @deprecated RFC-088 M4b:移入 cli InputHistoryStore 独立落盘。
|
|
91
|
+
* 仅保留读取以兼容旧快照(`--continue` 一次性迁移),新代码不写此字段。
|
|
92
|
+
* additive optional——不升 SESSION_SNAPSHOT_VERSION。
|
|
93
|
+
*/
|
|
94
|
+
inputHistory?: string[];
|
|
95
|
+
/**
|
|
96
|
+
* 持久化绝对回合总数(reduce currentTurn)。resume 时据此把
|
|
97
|
+
* 派生的 turnBoundaries 末尾对齐到绝对回合号,并续接 currentTurn。**不存 turnBoundaries
|
|
98
|
+
* 索引**(resume 从消息流派生,见 cli deriveTurnBoundaries)。additive optional——不升版本。
|
|
99
|
+
*/
|
|
100
|
+
turnCount?: number;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 草稿持久化条目。全 primitive,不依赖上层 tui/ai 类型。
|
|
104
|
+
*/
|
|
105
|
+
interface DraftEntry {
|
|
106
|
+
id: string;
|
|
107
|
+
name: string;
|
|
108
|
+
text: string;
|
|
109
|
+
savedAt: number;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 持久化任务面板条目(原 PersistedSessionConfig.todoList 内联类型)。
|
|
113
|
+
* RFC-108 D3:从 PersistedSessionConfig 分离为独立命名类型,供 Session 接口的 get/set 方法签名继续引用。
|
|
114
|
+
*/
|
|
115
|
+
interface TodoItem {
|
|
116
|
+
id?: string;
|
|
117
|
+
title?: string;
|
|
118
|
+
status?: 'pending' | 'in_progress' | 'done' | 'skipped' | 'failed';
|
|
119
|
+
turn?: number;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 持久化编辑文件条目(原 PersistedSessionConfig.editedFiles 内联类型)。
|
|
123
|
+
* RFC-108 D3:从 PersistedSessionConfig 分离为独立命名类型。
|
|
124
|
+
*/
|
|
125
|
+
interface EditedFile {
|
|
126
|
+
path: string;
|
|
127
|
+
operation: 'edit' | 'write' | 'delete';
|
|
128
|
+
addedLines: number;
|
|
129
|
+
removedLines: number;
|
|
130
|
+
timestamp: number;
|
|
131
|
+
turnId?: string;
|
|
132
|
+
toolCallId?: string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 子代理面板持久化条目(stat-only)。全 primitive,不依赖上层 tui/ai 类型。
|
|
136
|
+
* status 取终态快照;resume 时真实运行态由 runtime 决定(持久化只为面板可见性,非可恢复子代理)。
|
|
137
|
+
*/
|
|
138
|
+
interface SubagentEntry {
|
|
139
|
+
id: string;
|
|
140
|
+
/** agentName / 作业 title。 */
|
|
141
|
+
name: string;
|
|
142
|
+
status: 'running' | 'done' | 'error' | 'killed';
|
|
143
|
+
/** 首次出现时间(ms)。 */
|
|
144
|
+
startedAt: number;
|
|
145
|
+
/** 所属回合(供 UI 分组,可选)。 */
|
|
146
|
+
turnId?: string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 持久化回合完成摘要条目(TUI 底部"✓ 完成 · N 个工具 · 耗时 · tokens · 模型"行)。
|
|
150
|
+
* 全 primitive,不依赖上层 tui/ai 类型;与 `messages()`/`AgentMessage[]`(发给模型的
|
|
151
|
+
* 真实历史)物理隔离存储——只供 resume 时重绘对话流用,不进 provider 请求。
|
|
152
|
+
* 每回合累加(不像 EditedFile/SubagentEntry 那样只留当前回合),因为 resume 需要在
|
|
153
|
+
* 对话流里逐回合重现历史摘要行。
|
|
154
|
+
*/
|
|
155
|
+
interface TurnSummaryEntry {
|
|
156
|
+
turn: number;
|
|
157
|
+
status: 'done' | 'error' | 'interrupted' | 'incomplete';
|
|
158
|
+
toolCount: number;
|
|
159
|
+
elapsedMs: number;
|
|
160
|
+
tokensUsed?: number;
|
|
161
|
+
cacheHitRatio?: number;
|
|
162
|
+
model?: string;
|
|
163
|
+
/** RFC-235:本回合实际 effort 档(纯数据,渲染层用 formatEffortLabel 拼 ·Effort;不含展示格式)。 */
|
|
164
|
+
effort?: string;
|
|
165
|
+
tasksDone?: number;
|
|
166
|
+
tasksTotal?: number;
|
|
167
|
+
completedAt: number;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 标题来源等级(RFC-078 D1)。覆盖优先级 custom > ai > derived:
|
|
171
|
+
* - `derived`:deriveSessionTitle 启发式(首条 user 消息)。
|
|
172
|
+
* - `ai`:LLM 便宜模型生成(可覆盖 derived,不可覆盖 custom)。
|
|
173
|
+
* - `custom`:用户 `/rename` 指定(永不被 ai/derived 覆盖)。
|
|
174
|
+
* setTitle 据此实施等级守卫(见 InMemorySession.setTitle)。
|
|
175
|
+
*/
|
|
176
|
+
type TitleSource = 'custom' | 'ai' | 'derived';
|
|
177
|
+
interface SessionMetadata {
|
|
178
|
+
createdAt: number;
|
|
179
|
+
lastActiveAt: number;
|
|
180
|
+
messageCount: number;
|
|
181
|
+
compactionCount: number;
|
|
182
|
+
parentSessionId?: string;
|
|
183
|
+
forkPoint?: number;
|
|
184
|
+
tags: string[];
|
|
185
|
+
title?: string;
|
|
186
|
+
/** 当前 title 的来源等级(RFC-078 D1)。缺省视为未设置(可被任意来源写入)。 */
|
|
187
|
+
titleSource?: TitleSource;
|
|
188
|
+
/** 创建此会话的 SDK 版本(作为 additive optional 不升 SESSION_SNAPSHOT_VERSION)。 */
|
|
189
|
+
sdkVersion?: string;
|
|
190
|
+
/**
|
|
191
|
+
* 是否已归档(禁止后续输入)的持久化镜像。运行时唯一权威仍是 Agent 状态机的
|
|
192
|
+
* `status==='archived'`;本字段只用于跨重启/驱逐恢复时还原状态机(见
|
|
193
|
+
* `PersistenceSync.restore` 恢复后 `agent.archive()` 回放)。additive optional,
|
|
194
|
+
* 不升 SESSION_SNAPSHOT_VERSION——旧快照缺此字段按 false 处理。
|
|
195
|
+
*/
|
|
196
|
+
archived?: boolean;
|
|
197
|
+
/** 是否置顶(/resume 列表排序权重,不影响是否可输入)。additive optional。 */
|
|
198
|
+
pinned?: boolean;
|
|
199
|
+
/** 置顶分组名(RFC-158)。仅 pinned=true 时生效;缺省落入默认组。additive optional。 */
|
|
200
|
+
pinGroup?: string;
|
|
201
|
+
config?: PersistedSessionConfig;
|
|
202
|
+
}
|
|
203
|
+
interface Session<T = unknown> {
|
|
204
|
+
id: string;
|
|
205
|
+
readonly leafId: string | undefined;
|
|
206
|
+
append(message: T): void;
|
|
207
|
+
/** RFC-283 D2:移除最后一条 message entry(内存态回滚,prep 阶段失败时用)。 */
|
|
208
|
+
removeLast(): void;
|
|
209
|
+
compact(summary: string, compactedCount: number): void;
|
|
210
|
+
/** 自动压缩(keep-recent)写回——记录携带 replacement 的压缩条目(见 SessionEntry.replacement)。 */
|
|
211
|
+
recordCompaction(summary: string, replacement: T[]): void;
|
|
212
|
+
/**
|
|
213
|
+
* `/clear`——append 一个 clear 边界条目。其后 buildContext() 真空、visibleMessages() 空;
|
|
214
|
+
* messages()(完整转录)保留全部。不换 sessionId、不删历史。
|
|
215
|
+
*
|
|
216
|
+
* RFC-151 D2:可选 `{ purge: true }` 物理释放该边界之前的所有 message 条目。
|
|
217
|
+
*/
|
|
218
|
+
clearContext(options?: {
|
|
219
|
+
purge?: boolean;
|
|
220
|
+
}): void;
|
|
221
|
+
/**
|
|
222
|
+
* RFC-194 D3:物理释放最后一个 clear 边界之前的内存驻留(message 条目 splice 移除 +
|
|
223
|
+
* 非活跃 compaction 的 replacement 剥离;DB append-forever 不动)。幂等;无边界 no-op。
|
|
224
|
+
* 调用前提由调用方保证:clear 边界已确认落盘成功(RFC-194 D2 顺序,防丢未落盘消息)。
|
|
225
|
+
*/
|
|
226
|
+
purgeClearedHistory(): {
|
|
227
|
+
removedCount: number;
|
|
228
|
+
strippedReplacements: number;
|
|
229
|
+
};
|
|
230
|
+
entries(): ReadonlyArray<SessionEntry<T>>;
|
|
231
|
+
branchEntries(): ReadonlyArray<SessionEntry<T>>;
|
|
232
|
+
buildContext(): SessionContext<T>;
|
|
233
|
+
messages(): ReadonlyArray<T>;
|
|
234
|
+
/**
|
|
235
|
+
* 可见转录——最后一个 clear 边界**之后**的消息(无 clear 则等于 messages())。
|
|
236
|
+
* 仅被 clear 边界切断;compaction 边界不影响可见性(compact 后转录仍可滚回看)。
|
|
237
|
+
* 供 TUI / `--continue` 重绘:曾 `/clear` 的会话只重绘 clear 之后。
|
|
238
|
+
*/
|
|
239
|
+
visibleMessages(): ReadonlyArray<T>;
|
|
240
|
+
metadata(): SessionMetadata;
|
|
241
|
+
branch(entryId: string): void;
|
|
242
|
+
/**
|
|
243
|
+
* 设置标题(RFC-078 R1 单写路径 / R2 等级守卫)。
|
|
244
|
+
* 仅当新来源等级 ≥ 当前 title 来源等级时才覆盖(custom 永不被 ai/derived 盖)。
|
|
245
|
+
* @returns 是否实际写入(被等级守卫拦截时返回 false)。
|
|
246
|
+
*/
|
|
247
|
+
setTitle(title: string, source: TitleSource): boolean;
|
|
248
|
+
/**
|
|
249
|
+
* 归档状态的持久化镜像写入(`metadata.archived`)。运行时权威是 Agent 状态机,本方法
|
|
250
|
+
* 仅供 `AgentSession.archive()/unarchive()` 落一份镜像值供 restore 时回放。
|
|
251
|
+
*/
|
|
252
|
+
setArchived(archived: boolean): void;
|
|
253
|
+
/**
|
|
254
|
+
* 置顶状态写入(`metadata.pinned`/`metadata.pinGroup`)。纯持久化字段,无状态机联动。
|
|
255
|
+
* `group` 仅 `pinned=true` 时生效;`pinned=false` 时实现必须无条件清空 `pinGroup`
|
|
256
|
+
* (不保留旧分组,防止"未置顶但残留分组名"的僵尸状态,见 RFC-158 §4)。
|
|
257
|
+
*/
|
|
258
|
+
setPinned(pinned: boolean, group?: string): void;
|
|
259
|
+
/**
|
|
260
|
+
* 持久化 config 字段存取器(取代 cli 侧 `as unknown as {...}` 裸 cast,给持久化契约真类型)。
|
|
261
|
+
* RFC-108 D3:todoList/editedFiles/subagents/drafts 四个 UI 面板字段从 PersistedSessionConfig
|
|
262
|
+
* 分离为独立存储——这些 get/set 方法签名(同步、同名)保持不变,但内部存储已从 metadata.config
|
|
263
|
+
* 迁至独立 panelState 字段。绝大多数消费点因此无需修改。
|
|
264
|
+
*/
|
|
265
|
+
getTodoList(): TodoItem[] | undefined;
|
|
266
|
+
setTodoList(todos: TodoItem[]): void;
|
|
267
|
+
getEditedFiles(): EditedFile[] | undefined;
|
|
268
|
+
setEditedFiles(files: EditedFile[]): void;
|
|
269
|
+
/**
|
|
270
|
+
* 持久化输入历史(@deprecated RFC-088 M4b:移入 cli InputHistoryStore。
|
|
271
|
+
* 仅保留读取以兼容旧快照一次性迁移)。
|
|
272
|
+
*/
|
|
273
|
+
getInputHistory(): NonNullable<PersistedSessionConfig['inputHistory']> | undefined;
|
|
274
|
+
/**
|
|
275
|
+
* @deprecated RFC-088 M4b:新代码应写 cli InputHistoryStore + persist(), 不再经 session 快照。
|
|
276
|
+
*/
|
|
277
|
+
setInputHistory(entries: NonNullable<PersistedSessionConfig['inputHistory']>): void;
|
|
278
|
+
/** 子代理面板持久化(stat-only)。RFC-108 D3:存储迁至 panelState 独立字段。 */
|
|
279
|
+
getSubagents(): SubagentEntry[] | undefined;
|
|
280
|
+
setSubagents(entries: SubagentEntry[]): void;
|
|
281
|
+
/** 绝对回合总数(resume 末尾对齐派生 turnBoundaries)。 */
|
|
282
|
+
getTurnCount(): number | undefined;
|
|
283
|
+
setTurnCount(n: number): void;
|
|
284
|
+
/** 草稿面板持久化(DraftPane 的 Ctrl+S 保存/加载)。RFC-108 D3:存储迁至 panelState 独立字段。 */
|
|
285
|
+
getDrafts(): DraftEntry[] | undefined;
|
|
286
|
+
setDrafts(entries: DraftEntry[]): void;
|
|
287
|
+
/**
|
|
288
|
+
* 回合完成摘要面板持久化(TUI 底部摘要行"✓ 完成 · N 个工具 · 耗时 · tokens · 模型")。
|
|
289
|
+
* 每回合累加写入(整表覆盖式 set,调用方 read-append-write),与 messages() 物理隔离,
|
|
290
|
+
* 不进 provider 请求——只供 resume 时按 turn 插回对话流重绘。
|
|
291
|
+
*/
|
|
292
|
+
getTurnSummaries(): TurnSummaryEntry[] | undefined;
|
|
293
|
+
setTurnSummaries(entries: TurnSummaryEntry[]): void;
|
|
294
|
+
}
|
|
295
|
+
interface SessionStore<T = unknown> {
|
|
296
|
+
createSession(id?: string): Promise<Session<T>>;
|
|
297
|
+
getSession(id: string): Session<T> | undefined;
|
|
298
|
+
listSessions(): ReadonlyArray<Session<T>>;
|
|
299
|
+
deleteSession(id: string): Promise<boolean>;
|
|
300
|
+
listSessionsPaginated(options: PaginationOptions): PaginatedResult<Session<T>>;
|
|
301
|
+
}
|
|
302
|
+
interface SessionSnapshot<T = unknown> {
|
|
303
|
+
version: number;
|
|
304
|
+
id: string;
|
|
305
|
+
entries: SessionEntry<T>[];
|
|
306
|
+
metadata: SessionMetadata;
|
|
307
|
+
leafId?: string;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* session 域持久化接口——`extends` @x-otto/persistence 的共享方法契约
|
|
311
|
+
* `PersistenceShape`,固化为域信封 `SessionSnapshot<T>`(entries[]/leafId/SessionMetadata)+ 域
|
|
312
|
+
* 分页 `PaginationOptions`(sortBy=lastActiveAt|createdAt)。**不**复用 `Persistence<T>` 本体:
|
|
313
|
+
* 其信封 `Snapshot<T>`(data:T) 与分页 sortBy 是通用语义,与 session 域类型真异(详见 PersistenceShape doc)。
|
|
314
|
+
*/
|
|
315
|
+
interface SessionPersistence<T = unknown> extends PersistenceShape<SessionSnapshot<T>, PaginationOptions> {
|
|
316
|
+
/**
|
|
317
|
+
* 只读排序 id 列表(RFC-154 D1)——不含 entries,供批量恢复类场景(如 restoreAll)在
|
|
318
|
+
* 重量级 `load()` 前先选出候选集,避免对不会被选中的会话做全量反序列化。
|
|
319
|
+
*
|
|
320
|
+
* 契约(重要事项规则):返回结果必须按 lastActiveAt **降序**排列(新→旧),且排除已归档
|
|
321
|
+
* 会话(D1×D3 接线——归档语义是"不再被批量恢复路径主动拉起")。调用方信任该顺序,不再做
|
|
322
|
+
* 二次排序兜底;三个实现(Relational/File/Remote)必须各自保证该契约,并各自有独立的排序
|
|
323
|
+
* 正确性单测覆盖(不能只测其中一个后端)。
|
|
324
|
+
*/
|
|
325
|
+
listRecentMeta(limit: number): Promise<string[]>;
|
|
326
|
+
/**
|
|
327
|
+
* RFC-305 M3:后端 turn-flush 能力声明——持久化后端决定 turn 级落库策略,而非用户配置。
|
|
328
|
+
* - 'incremental'(缺省):游标增量 append、低延迟(本地 relational SQLite)→ 每 turn 落库,
|
|
329
|
+
* 崩溃窗口毫秒级(RFC-305 M1)。
|
|
330
|
+
* - 'snapshot':整份快照上传/覆写(远端 PUT、旧 blob 逃生口整份文件覆写)→ turn 级节流
|
|
331
|
+
* 落库(避免每 turn 全量上传随会话线性膨胀),prompt.end 仍强制落库。
|
|
332
|
+
* 由实现类静态声明(能力事实),装配层零配置;未来远端若提供 append-tail API,改声明即可
|
|
333
|
+
* 自动升级为 every-turn,无需任何调用方改动。
|
|
334
|
+
*/
|
|
335
|
+
turnFlush?: 'incremental' | 'snapshot';
|
|
336
|
+
/**
|
|
337
|
+
* RFC-325 M4:DB 冷归档能力(optional,只有 relational SQLite 后端实现)。
|
|
338
|
+
* 将 entries 复制到外部 .jsonl.gz + 标 archived=1,**不删除 DB 行**(RFC-159 红线)。
|
|
339
|
+
*/
|
|
340
|
+
archiveSession?: (sessionId: string) => Promise<string>;
|
|
341
|
+
/** 从外部归档恢复(幂等 append,不删除既有条目)。 */
|
|
342
|
+
restoreArchivedSession?: (sessionId: string) => Promise<number>;
|
|
343
|
+
}
|
|
344
|
+
interface PaginationOptions {
|
|
345
|
+
page: number;
|
|
346
|
+
pageSize?: number;
|
|
347
|
+
order?: 'asc' | 'desc';
|
|
348
|
+
sortBy?: 'lastActiveAt' | 'createdAt';
|
|
349
|
+
}
|
|
350
|
+
//#endregion
|
|
351
|
+
//#region src/in-memory-session.d.ts
|
|
352
|
+
declare class InMemorySession<T = unknown> implements Session<T> {
|
|
353
|
+
readonly id: string;
|
|
354
|
+
private readonly sessionEntries;
|
|
355
|
+
private readonly entryMap;
|
|
356
|
+
/**
|
|
357
|
+
* RFC-178 D2:会话消息内容字节估算(增量维护,避免每回合 O(n) 重算——lesson_54)。
|
|
358
|
+
* 覆盖全部变更路径:append 累加 / cap splice 递减 / clearContext purge 递减 /
|
|
359
|
+
* recordCompaction replacement 计入 / stripReplacementInPlace 剥离递减 /
|
|
360
|
+
* restoreEntries 全量重算基准。估算口径见 message-bytes.ts(UTF-16 下界,确定性)。
|
|
361
|
+
*/
|
|
362
|
+
private _estimatedContentBytes;
|
|
363
|
+
/** RFC-178:当前消息内容字节估算(含活跃 compaction replacement 的驻留)。 */
|
|
364
|
+
get estimatedContentBytes(): number;
|
|
365
|
+
/**
|
|
366
|
+
* 终局 review(2026-07-21):当前消息条目数——全路径增量维护(append +1 / purgeBeforeBoundary
|
|
367
|
+
* -=removed / capStoredHistory·capStoredHistoryByBytes 各自赋值 / restoreEntries 全量重算),
|
|
368
|
+
* 与 `verifyEstimatedContentBytes` 同源纪律:热路径读此 getter 而非重新 O(n) 扫描 sessionEntries
|
|
369
|
+
* 或调用 `metadata().messageCount`(后者对 tags/config 做防御性深拷贝,代价更高)。
|
|
370
|
+
*/
|
|
371
|
+
get messageCount(): number;
|
|
372
|
+
/**
|
|
373
|
+
* 计数器漂移校验(终局审查 2026-07-18 M2 守卫):从 sessionEntries 全量重算字节估算,
|
|
374
|
+
* 与增量计数器比对。`_estimatedContentBytes` 是"必须靠人力在所有 mutation 路径上手动
|
|
375
|
+
* 同步"的派生状态——新增 mutation 路径漏更新计数器不会崩溃,只会让字节驻留 cap 触发
|
|
376
|
+
* 时机静默错位。本方法给测试(每个 mutation 路径测试的收尾断言)与 debug 排查提供
|
|
377
|
+
* 唯一真源比对点。O(n) 全量遍历,**勿在热路径调用**(lesson_54)。
|
|
378
|
+
*
|
|
379
|
+
* @returns 漂移量(`recomputed - counter`;0 = 无漂移)
|
|
380
|
+
*/
|
|
381
|
+
verifyEstimatedContentBytes(): {
|
|
382
|
+
counter: number;
|
|
383
|
+
recomputed: number;
|
|
384
|
+
drift: number;
|
|
385
|
+
};
|
|
386
|
+
private readonly meta;
|
|
387
|
+
/**
|
|
388
|
+
* RFC-108 D3:五个 UI 面板字段(todoList/editedFiles/subagents/drafts/turnSummaries)的
|
|
389
|
+
* 独立内存存储,不再纳入 SessionMetadata.config。字段类型从原 PersistedSessionConfig
|
|
390
|
+
* 同名字段迁移。对外接口(Session 的 get/set 方法)签名完全保持不变,绝大多数消费点
|
|
391
|
+
* 无需修改。turnSummaries 是后续新增字段(回合完成摘要行持久化,见 TurnSummaryEntry)。
|
|
392
|
+
*/
|
|
393
|
+
private readonly panelState;
|
|
394
|
+
constructor(id: string, parentSessionId?: string, forkPoint?: number);
|
|
395
|
+
private _leafId;
|
|
396
|
+
get leafId(): string | undefined;
|
|
397
|
+
append(message: T): void;
|
|
398
|
+
/**
|
|
399
|
+
* RFC-283 D2:移除最后一条 message entry(内存态回滚)。
|
|
400
|
+
* 用于 prompt prep 阶段失败时回滚从未到达 LLM 的 user message。
|
|
401
|
+
* 只移除最后一条 type='message' entry,回退 _leafId 到其 parentId。
|
|
402
|
+
* 不触碰 compaction/clear 等结构 entry。只在 prompt 执行期间(save 之前)调用。
|
|
403
|
+
*/
|
|
404
|
+
removeLast(): void;
|
|
405
|
+
compact(summary: string, compactedCount: number): void;
|
|
406
|
+
/**
|
|
407
|
+
* 自动压缩(keep-recent)写回——记录一条携带 replacement 的压缩条目。
|
|
408
|
+
* replacement = 压缩时保留的近期消息快照(replacement-history 模型)。buildContext
|
|
409
|
+
* 命中即返回 {summary, [...replacement, ...其后新增消息]},重启原样恢复、不重算。
|
|
410
|
+
* 与 compact()(fold-all 位置式,手动 /compact 用)并存,由 buildContext 据 replacement 是否在场分派。
|
|
411
|
+
*/
|
|
412
|
+
recordCompaction(summary: string, replacement: T[]): void;
|
|
413
|
+
/** RFC-160 D2a:剥离资格谓词(组装根注入,如"entry_id 已在持久化游标之内")。 */
|
|
414
|
+
private replacementStripEligibility;
|
|
415
|
+
/** 注入运行时增量剥离的资格谓词(缺省无谓词 = 运行时剥离禁用,仅装载剥离生效)。 */
|
|
416
|
+
setReplacementStripEligibility(predicate: (entryId: string) => boolean): void;
|
|
417
|
+
/** 当前 leaf parent 链上最近的 compaction(即 buildContext 的活跃边界),无则 undefined。 */
|
|
418
|
+
private findActiveCompaction;
|
|
419
|
+
/**
|
|
420
|
+
* RFC-160:就地剥离某 compaction 的 replacement(内存投影;entryMap 与 sessionEntries
|
|
421
|
+
* 同步替换为剥离副本,不变更 DB)。幂等:已剥离/无 replacement 时为 no-op。
|
|
422
|
+
*/
|
|
423
|
+
private stripReplacementInPlace;
|
|
424
|
+
/**
|
|
425
|
+
* 清除边界。append 一个 type:'clear' 条目——其后 buildContext() 真空、visibleMessages() 空,
|
|
426
|
+
* 但 messages()(完整转录)保留全部(时间旅行可回溯 clear 前)。不增 compactionCount(clear 非压缩)。
|
|
427
|
+
*
|
|
428
|
+
* RFC-151 D2:可选 purge 物理释放——purge:true 时从 sessionEntries 头部 splice 移除
|
|
429
|
+
* 该边界之前的所有 message 类型条目,仅保留边界及之后的内容。
|
|
430
|
+
*/
|
|
431
|
+
clearContext(options?: {
|
|
432
|
+
purge?: boolean;
|
|
433
|
+
}): void;
|
|
434
|
+
/**
|
|
435
|
+
* RFC-194 D3:物理释放最后一个 clear 边界之前的内存驻留(DB append-forever 不动)。
|
|
436
|
+
* 供持久化层在「clear 边界确认落盘成功后」调用(顺序保证见 RFC-194 D2——先 save
|
|
437
|
+
* 后 purge,否则未落盘消息会永久丢失,违反 RFC-159 红线;本方法自身不感知持久化
|
|
438
|
+
* 状态,保持 session 包零持久化依赖)。
|
|
439
|
+
*
|
|
440
|
+
* 行为:
|
|
441
|
+
* - 无 clear 边界 → no-op(removedCount=0);
|
|
442
|
+
* - 边界前 `type:'message'` 条目:splice 移除 + entryMap 删除 + 字节/条数账目递减;
|
|
443
|
+
* - 边界前 `type:'compaction'` 条目:剥离 replacement(RFC-151 purge 分支的既有缺口
|
|
444
|
+
* ——clear 后边界前所有 compaction 必然非活跃(findActiveCompactionFrom 遇 clear
|
|
445
|
+
* 即断),其 replacement 是纯驻留,重工具会话的驻留大头恰在这里);
|
|
446
|
+
* - 幂等:重复调用第二次 removedCount=0、strippedReplacements=0。
|
|
447
|
+
*
|
|
448
|
+
* 已知限制(RFC-194 R2):purge 后进程内 branch() 到 clear 前条目会抛 Entry not
|
|
449
|
+
* found——DB 全量在,可经 loadEntriesByIds hydrate 恢复(RFC-160 既有机制);当前
|
|
450
|
+
* 无生产调用路径跨 clear 边界 branch。
|
|
451
|
+
*/
|
|
452
|
+
purgeClearedHistory(): {
|
|
453
|
+
removedCount: number;
|
|
454
|
+
strippedReplacements: number;
|
|
455
|
+
};
|
|
456
|
+
/** clearContext({purge:true}) 与 purgeClearedHistory() 的共享实现(RFC-194 D3 收编)。 */
|
|
457
|
+
private purgeBeforeBoundary;
|
|
458
|
+
branch(entryId: string): void;
|
|
459
|
+
/** 从任意起点沿 parent 链找最近的 compaction 边界(clear 截断),branch() 收编用。 */
|
|
460
|
+
private findActiveCompactionFrom;
|
|
461
|
+
entries(): ReadonlyArray<SessionEntry<T>>;
|
|
462
|
+
branchEntries(): ReadonlyArray<SessionEntry<T>>;
|
|
463
|
+
buildContext(): SessionContext<T>;
|
|
464
|
+
messages(): ReadonlyArray<T>;
|
|
465
|
+
/**
|
|
466
|
+
* 可见转录——最后一个 **clear** 边界之后的消息(compaction 边界不切可见)。
|
|
467
|
+
* 无 clear → 等于 messages() 全量。供 `--continue`/TUI 重绘。
|
|
468
|
+
*/
|
|
469
|
+
visibleMessages(): ReadonlyArray<T>;
|
|
470
|
+
metadata(): SessionMetadata;
|
|
471
|
+
/**
|
|
472
|
+
* RFC-078 R1/R2:单写路径 + 等级守卫。弱来源不得覆盖强来源
|
|
473
|
+
* (custom 永不被 ai/derived 盖;同级可覆盖,如 ai 重生覆盖旧 ai)。
|
|
474
|
+
*/
|
|
475
|
+
setTitle(title: string, source: TitleSource): boolean;
|
|
476
|
+
/** 设置创建此会话的 SDK 版本(仅在新会话创建时调用一次)。 */
|
|
477
|
+
setSdkVersion(version: string): void;
|
|
478
|
+
/** 归档状态持久化镜像写入(见 `Session.setArchived` 契约注释)。 */
|
|
479
|
+
setArchived(archived: boolean): void;
|
|
480
|
+
/** 置顶状态写入。`pinned=false` 时无条件清空 `pinGroup`(不保留旧分组,见 RFC-158 §4)。 */
|
|
481
|
+
setPinned(pinned: boolean, group?: string): void;
|
|
482
|
+
/** 获取持久化的任务面板状态。RFC-108 D3:存储迁至 panelState 独立字段。 */
|
|
483
|
+
getTodoList(): TodoItem[] | undefined;
|
|
484
|
+
/** 设置持久化的任务面板状态。RFC-108 D3:存储迁至 panelState 独立字段。 */
|
|
485
|
+
setTodoList(todos: TodoItem[]): void;
|
|
486
|
+
/** 获取持久化的编辑文件面板状态。RFC-108 D3:存储迁至 panelState 独立字段。 */
|
|
487
|
+
getEditedFiles(): EditedFile[] | undefined;
|
|
488
|
+
/** 设置持久化的编辑文件面板状态。max 50 条(最新在前)。RFC-108 D3:存储迁至 panelState。 */
|
|
489
|
+
setEditedFiles(files: EditedFile[]): void;
|
|
490
|
+
/** 获取持久化的输入历史。 */
|
|
491
|
+
getInputHistory(): string[] | undefined;
|
|
492
|
+
/**
|
|
493
|
+
* 设置持久化的输入历史。max 100 条(最近在末尾)。config 为惰性对象,首次写入时初始化。
|
|
494
|
+
* @deprecated RFC-088 M4b:历史已移至 cli InputHistoryStore。本方法仅迁移路径调用——
|
|
495
|
+
* 传空数组 = **删除字段**(快照不留 `inputHistory: []` 残迹,对齐 M4b 设计"下次快照字段消失")。
|
|
496
|
+
*/
|
|
497
|
+
setInputHistory(entries: string[]): void;
|
|
498
|
+
/** 获取持久化的子代理面板状态。RFC-108 D3:存储迁至 panelState 独立字段。 */
|
|
499
|
+
getSubagents(): SubagentEntry[] | undefined;
|
|
500
|
+
/** 设置持久化的子代理面板状态(stat-only)。max 100 条。RFC-108 D3:存储迁至 panelState。 */
|
|
501
|
+
setSubagents(entries: SubagentEntry[]): void;
|
|
502
|
+
/** 获取持久化的回合完成摘要列表(TUI 底部摘要行)。 */
|
|
503
|
+
getTurnSummaries(): TurnSummaryEntry[] | undefined;
|
|
504
|
+
/**
|
|
505
|
+
* 设置持久化的回合完成摘要列表。max 200 条(最新在末尾——调用方按 turn 递增顺序
|
|
506
|
+
* read-append-write)。上限对齐 todoList/subagents 同类"面板态防无界增长"纪律;
|
|
507
|
+
* 200 远超单会话正常回合数,仅作防御性上限,不做"只留最近 N 条"这类会丢历史的裁剪。
|
|
508
|
+
*/
|
|
509
|
+
setTurnSummaries(entries: TurnSummaryEntry[]): void;
|
|
510
|
+
/** 获取持久化的绝对回合总数。 */
|
|
511
|
+
getTurnCount(): number | undefined;
|
|
512
|
+
/** 设置持久化的绝对回合总数。config 惰性初始化。 */
|
|
513
|
+
setTurnCount(n: number): void;
|
|
514
|
+
/** 获取持久化的草稿列表。RFC-108 D3:存储迁至 panelState 独立字段。 */
|
|
515
|
+
getDrafts(): DraftEntry[] | undefined;
|
|
516
|
+
/** 设置持久化的草稿列表。max 50 条(对齐迁移前 DraftStore 上限)。RFC-108 D3:存储迁至 panelState。 */
|
|
517
|
+
setDrafts(entries: DraftEntry[]): void;
|
|
518
|
+
/**
|
|
519
|
+
* 把存储的会话历史封顶到最近 maxMessages 条(删最早的消息条目,compaction/clear 边界不参与
|
|
520
|
+
* 计数、不被裁)。调用后 messageCount 重新派生。
|
|
521
|
+
*
|
|
522
|
+
* 这是**纯存储/内存防御**(条数维度),**不塑造模型上下文**——与 @x-otto/memory 的
|
|
523
|
+
* `prune`(截短工具文本/token 维度)、`compaction`(摘要/token 维度)是完全不同的机制,勿混。
|
|
524
|
+
* canStartHistory 谓词保证封顶切点工具边界对齐(保留区第一条非孤儿 tool_result)。
|
|
525
|
+
*
|
|
526
|
+
* @returns 本次是否真的从内存里删除了消息条目 + 删除条数(用户可观测性——调用方据此判断
|
|
527
|
+
* 是否需要向用户提示"历史已从内存视图移除",见 lesson `resume 数据丢失`系列排查:此前该
|
|
528
|
+
* 操作零 UI 反馈,用户长会话(>2000 条)resume 后发现前段历史消失却毫无线索。**本方法只
|
|
529
|
+
* 操作内存**——持久层是否也删除取决于调用方使用的 persistence 实现:RFC-159 起默认的
|
|
530
|
+
* `RelationalSessionPersistence`(SQLite)是 append-forever,被本方法裁掉的条目仍完整
|
|
531
|
+
* 保留在 DB;仅已废弃的 `OTTO_SESSION_BLOB=1` 逃生口后端会因整份快照覆写而真实丢失。
|
|
532
|
+
* 返回 `{capped:false}` 时未发生任何裁剪(正常场景,消息数未超上限)。
|
|
533
|
+
*
|
|
534
|
+
* **RFC-321 R5——返回语义是"尽力裁"而非"裁到 maxMessages"**:裁剪永不跨越活跃
|
|
535
|
+
* compaction 边界(R4,见 `removeOldestMessages`),故 `removedCount` 可能小于所需,
|
|
536
|
+
* 裁后条数可能仍 > maxMessages。调用方**必须容忍未达标,且不得据此重试或重复发事件**
|
|
537
|
+
* (否则复现 2026-07-21 的 `history-capped` 刷屏病理)。
|
|
538
|
+
*
|
|
539
|
+
* @returns `capped`/`removedCount` 同前;`boundaryLimited` = 因边界保护提前停止;
|
|
540
|
+
* `activeOversized` = 边界前死历史已裁尽、**活跃区自身仍超限**(RFC-321 D1-a′ 逃生阀
|
|
541
|
+
* 信号,供上层请求强制压缩;M1 只产出信号,消费方接线属 M2/T320-08b)。
|
|
542
|
+
*/
|
|
543
|
+
capStoredHistory(maxMessages: number, canStartHistory?: (msg: T) => boolean): {
|
|
544
|
+
capped: boolean;
|
|
545
|
+
removedCount: number;
|
|
546
|
+
boundaryLimited: boolean;
|
|
547
|
+
activeOversized: boolean;
|
|
548
|
+
};
|
|
549
|
+
/**
|
|
550
|
+
* RFC-321 R12(M5 压缩前置):**只读预判**——把历史裁到 `targetMessages` 条是否会
|
|
551
|
+
* 触及活跃上下文(即边界前的死历史不够裁)。不做任何修改。
|
|
552
|
+
*
|
|
553
|
+
* 用途:`applyResidencyCaps` 在裁剪**之前**调用,若返回 true 则先请求 force 压缩建立
|
|
554
|
+
* 新边界、本轮跳过裁剪;下一轮边界已在,裁剪只碰死历史(无损)。这修复了"无压缩边界
|
|
555
|
+
* 时 cap 直接裁掉模型仍在用的活跃上下文且无摘要替代"的纯信息丢失(实证:100 条裁到
|
|
556
|
+
* 20 条,模型上下文直接少 80 条,无任何摘要覆盖)。
|
|
557
|
+
*
|
|
558
|
+
* 判据:活跃边界之前的 message 条目数 < 需要移除的条数。无活跃边界时,边界前死历史
|
|
559
|
+
* 视为 0——即任何裁剪都会触及活跃上下文,恒返回 true(这正是最该先压缩的场景)。
|
|
560
|
+
*/
|
|
561
|
+
wouldTrimTouchActiveContext(targetMessages: number): boolean;
|
|
562
|
+
/** 同上,字节口径(供 `capStoredHistoryByBytes` 的前置判定)。 */
|
|
563
|
+
wouldByteTrimTouchActiveContext(targetBytes: number): boolean;
|
|
564
|
+
/**
|
|
565
|
+
* RFC-178 D3 硬段:把存储的会话历史按**字节估算**收紧到 maxBytes 以内(从最早的消息条目
|
|
566
|
+
* 开始裁,compaction/clear 边界不裁)。与 `capStoredHistory`(条数模式)共享同一裁剪
|
|
567
|
+
* 循环与工具边界对齐谓词——两模式并存时语义为"取更紧者"(各自独立调用,各自只在超限时
|
|
568
|
+
* 生效)。同为**纯内存防御**,DB append-forever 不受影响(RFC-159)。
|
|
569
|
+
*
|
|
570
|
+
* 保底护栏:无论多超限,保留区至少留 1 条消息(避免字节预算配置过小时清空整个会话视图)。
|
|
571
|
+
*
|
|
572
|
+
* **RFC-321 R4/R5**:同样受活跃 compaction 边界保护、同样是"尽力裁"语义,返回字段与
|
|
573
|
+
* `capStoredHistory` 同构(`activeOversized` 此处按字节口径判定:裁后估算字节仍超预算)。
|
|
574
|
+
*/
|
|
575
|
+
capStoredHistoryByBytes(maxBytes: number, canStartHistory?: (msg: T) => boolean): {
|
|
576
|
+
capped: boolean;
|
|
577
|
+
removedCount: number;
|
|
578
|
+
boundaryLimited: boolean;
|
|
579
|
+
activeOversized: boolean;
|
|
580
|
+
};
|
|
581
|
+
/**
|
|
582
|
+
* 共享裁剪循环(条数/字节两模式复用):从 sessionEntries 头部移除 removeCount 条 message
|
|
583
|
+
* 条目,随后按 canStartHistory 谓词继续移除至工具边界对齐(保留区第一条非孤儿
|
|
584
|
+
* tool_result)。每次移除同步递减字节估算(RFC-178 D2 路径 2)。
|
|
585
|
+
*
|
|
586
|
+
* **RFC-321 R4(边界即止,不可削弱)**:裁剪**永不跨越活跃 compaction 边界**——遇到
|
|
587
|
+
* 该边界即停止,宁可少裁(`removed < removeCount`)也不破坏 branch 链完整性。
|
|
588
|
+
*
|
|
589
|
+
* 修复前的缺陷(RFC-321 P1-a,实证):本循环只按 `type === 'message'` 从头部 splice,
|
|
590
|
+
* 既不感知 compaction 边界、也不修复被删条目后继的 `parentId`。一旦裁剪深度超过边界前的
|
|
591
|
+
* 死历史条数,边界条目的 `parentId` 就会指向已删 id → `walkBranch` 回溯撞空提前截断 →
|
|
592
|
+
* 边界不在 branch 上 → `buildContext` 走 `lastBoundaryIndex === -1` 分支 → **summary 静默
|
|
593
|
+
* 丢失**(模型上下文降级,用户与日志均无信号)。主要可达面是 `ResidencyGovernor` 的
|
|
594
|
+
* critical 档(cap 除以 4,`residency-governor.ts`)——内存压力下恰恰最可能触发。
|
|
595
|
+
*
|
|
596
|
+
* 边界之前的条目全是"已被摘要替换的死历史",边界之后是模型仍在用的活跃上下文;以边界
|
|
597
|
+
* 为界即把"裁剪只能裁死历史"从时序假设变成**结构不变式**(不再依赖上游触发器配合)。
|
|
598
|
+
*
|
|
599
|
+
* @returns `removed` 实际移除条数(可能 < removeCount);`boundaryLimited` 是否因边界
|
|
600
|
+
* 保护提前停止(诊断用,非错误)。
|
|
601
|
+
*/
|
|
602
|
+
private removeOldestMessages;
|
|
603
|
+
setTags(tags: string[]): void;
|
|
604
|
+
addTag(tag: string): void;
|
|
605
|
+
restoreEntries(entries: SessionEntry<T>[], meta: SessionMetadata, restoredLeafId?: string): void;
|
|
606
|
+
/**
|
|
607
|
+
* 契约:`entries` 为**浅拷贝**——数组是新的,但 entry 对象按引用共享。持久化 / 时间旅行
|
|
608
|
+
* 调用方即刻序列化或 restore-verbatim,不变更 entry;深拷全量消息内容代价高且无收益。
|
|
609
|
+
* **调用方不得变更返回的 entry 对象**。`metadata` 已深拷(含 config),与活树独立。
|
|
610
|
+
*/
|
|
611
|
+
toSnapshot(): {
|
|
612
|
+
entries: SessionEntry<T>[];
|
|
613
|
+
metadata: SessionMetadata;
|
|
614
|
+
leafId?: string;
|
|
615
|
+
};
|
|
616
|
+
private walkBranch;
|
|
617
|
+
private getBranchMessageEntries;
|
|
618
|
+
/** 最后一个边界条目(compaction|clear)的 branch 下标,无则 -1。 */
|
|
619
|
+
private lastBoundaryIndex;
|
|
620
|
+
}
|
|
621
|
+
//#endregion
|
|
622
|
+
//#region src/message-bytes.d.ts
|
|
623
|
+
/**
|
|
624
|
+
* message-bytes.ts — RFC-178 D2:消息内容字节估算(会话驻留治理的字节维度口径)。
|
|
625
|
+
*
|
|
626
|
+
* 估算口径:文本 content 取 UTF-16 code unit 长度(`String.length`,纯 ASCII 下真实
|
|
627
|
+
* JS 驻留 ≈ 估算 × 2,恒为下界,见 RFC-178 §3 D2 UTF-16 效应量化);image 块取 base64
|
|
628
|
+
* data 长度(base64 字符串本身驻留内存,1 char ≈ 真实 1 byte 图片数据 × 4/3 的字符串);
|
|
629
|
+
* tool_call/tool_use 块取 arguments/input 的 JSON 序列化长度(`JSON.stringify` 产生字符串,
|
|
630
|
+
* 取 `.length`);顶层 `tool_calls` 数组取 `function.arguments` 字段长度(OpenAI 风格,
|
|
631
|
+
* arguments 已是 JSON 字符串)。
|
|
632
|
+
* 不追求 sizeof 精度(JS 无消息级真实内存测量),只求确定性 + 与内容体量单调相关。
|
|
633
|
+
*
|
|
634
|
+
* 兼容多种消息形态(AgentMessage 的 Anthropic content-block 与 OpenAI 风格均覆盖):
|
|
635
|
+
* - `content: string`
|
|
636
|
+
* - `content: Array<{ text?; source?: { data? }; data?; image_url?: { url? }; tool_call 块
|
|
637
|
+
* (arguments: Record<string,unknown>); tool_use 块(input: unknown) }>`
|
|
638
|
+
* - 顶层 `tool_calls` 数组(OpenAI/TUI 风格,`{ function: { arguments?: string } }`)
|
|
639
|
+
*/
|
|
640
|
+
declare function estimateMessageContentBytes(message: unknown): number;
|
|
641
|
+
//#endregion
|
|
642
|
+
//#region src/store.d.ts
|
|
643
|
+
declare class InMemorySessionStore<T = unknown> implements SessionStore<T> {
|
|
644
|
+
private readonly sessions;
|
|
645
|
+
createSession(id?: string): Promise<Session<T>>;
|
|
646
|
+
getSession(id: string): Session<T> | undefined;
|
|
647
|
+
listSessions(): ReadonlyArray<Session<T>>;
|
|
648
|
+
listSessionsPaginated(options: PaginationOptions): PaginatedResult<Session<T>>;
|
|
649
|
+
deleteSession(id: string): Promise<boolean>;
|
|
650
|
+
forkSession(sourceId: string, newId?: string): Promise<Session<T> | undefined>;
|
|
651
|
+
}
|
|
652
|
+
declare function createInMemorySessionStore<T = unknown>(): SessionStore<T>;
|
|
653
|
+
//#endregion
|
|
654
|
+
//#region src/memory-persistence.d.ts
|
|
655
|
+
/**
|
|
656
|
+
* 内存态会话持久化。复用内核抽象基类 {@link MemoryPersistenceBase},仅声明 `extractId` +
|
|
657
|
+
* 按 `SessionMetadata`(lastActiveAt/createdAt)取排序值——与 File/Remote 同款薄子类模式。
|
|
658
|
+
* 内核已统一 save/load/delete/list/listPaginated 实现(M2-02,路径 A)。
|
|
659
|
+
*/
|
|
660
|
+
declare class InMemorySessionPersistence<T = unknown> extends MemoryPersistenceBase<SessionSnapshot<T>> implements SessionPersistence<T> {
|
|
661
|
+
constructor();
|
|
662
|
+
protected extractId(snapshot: SessionSnapshot<T>): string;
|
|
663
|
+
protected getSortValue(snapshot: SessionSnapshot<T>, sortBy: string): number;
|
|
664
|
+
/** RFC-154 D1:内存态实现——过滤归档 + 按 lastActiveAt 降序 + 截断,零 IO 开销。 */
|
|
665
|
+
listRecentMeta(limit: number): Promise<string[]>;
|
|
666
|
+
}
|
|
667
|
+
//#endregion
|
|
668
|
+
//#region src/file-persistence.d.ts
|
|
669
|
+
interface FileSessionPersistenceOptions {
|
|
670
|
+
baseDir: string;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* @deprecated blob session persistence replaced by RelationalSessionPersistence。
|
|
674
|
+
* 仅保留为 OTTO_SESSION_BLOB=1 逃生口;关系路径线上跑稳后删除。
|
|
675
|
+
*/
|
|
676
|
+
declare class FileSessionPersistence<T = unknown> extends DiskPersistence<SessionSnapshot<T>> implements SessionPersistence<T> {
|
|
677
|
+
/**
|
|
678
|
+
* RFC-305 M3:文件后端整份快照覆写(非游标增量)→ 'snapshot'。该后端仅为
|
|
679
|
+
* OTTO_SESSION_BLOB=1 逃生口(M63 已废弃,跑稳后删除),节流语义与 remote 一致。
|
|
680
|
+
*/
|
|
681
|
+
readonly turnFlush: "snapshot";
|
|
682
|
+
private readonly sessionBaseDir;
|
|
683
|
+
private readonly sessionExtension;
|
|
684
|
+
constructor(options: FileSessionPersistenceOptions);
|
|
685
|
+
protected extractId(snapshot: SessionSnapshot<T>): string;
|
|
686
|
+
/**
|
|
687
|
+
* RFC-154 D1(blob 逃生口实现):单文件即整个 snapshot(entries+metadata 打包),无法像
|
|
688
|
+
* SQLite 那样只读元数据列——只能先按 mtime(lastActiveAt 代理值)排序,再逐个 load() 直到
|
|
689
|
+
* 凑够 limit 个非归档会话。比"全量 load 后排序"仍更省:mtime 排序在前,命中 limit 后即
|
|
690
|
+
* 停止读取,不会读到排序靠后但从不会被选中的文件。该路径 M63 起标记弃用,不追求极致优化
|
|
691
|
+
* (RFC-154 §3 D1)。
|
|
692
|
+
*/
|
|
693
|
+
listRecentMeta(limit: number): Promise<string[]>;
|
|
694
|
+
}
|
|
695
|
+
//#endregion
|
|
696
|
+
//#region src/session-repository.d.ts
|
|
697
|
+
/**
|
|
698
|
+
* SessionRepository — 会话关系化仓储(row 级,结构类型)。
|
|
699
|
+
*
|
|
700
|
+
* RFC-074 M5-02b:从 @x-otto/persistence 迁回本包(@x-otto/session)。会话 schema 与领域同归属,
|
|
701
|
+
* 通用存储内核(@x-otto/persistence)不再硬编码 session 领域表(根治 R-LAYER 依赖倒置)。
|
|
702
|
+
*
|
|
703
|
+
* 本层仍只认结构化行(workspaces / sessions / session_entries),**不依赖 SessionSnapshot 领域形状**
|
|
704
|
+
* (snapshot↔rows 桥在 relational-bridge)。change_log(entry 级)属后续阶段C(gated),本层不建。
|
|
705
|
+
*/
|
|
706
|
+
interface WorkspaceRow {
|
|
707
|
+
readonly id: string;
|
|
708
|
+
readonly name: string;
|
|
709
|
+
readonly ownerId?: string;
|
|
710
|
+
readonly orgId?: string;
|
|
711
|
+
readonly createdAt: number;
|
|
712
|
+
}
|
|
713
|
+
interface SessionRow {
|
|
714
|
+
readonly id: string;
|
|
715
|
+
/** 分区第一维(= ws_<id>);默认 'default'。 */
|
|
716
|
+
readonly workspaceKey: string;
|
|
717
|
+
readonly title?: string;
|
|
718
|
+
/** 标题来源等级(RFC-078 D1)——'custom' | 'ai' | 'derived',回血时据此护 custom。 */
|
|
719
|
+
readonly titleSource?: 'custom' | 'ai' | 'derived';
|
|
720
|
+
readonly parentSessionId?: string;
|
|
721
|
+
readonly forkPoint?: number;
|
|
722
|
+
/** 当前活跃分支叶(entry_id 指针)。 */
|
|
723
|
+
readonly leafId?: string;
|
|
724
|
+
readonly ownerId?: string;
|
|
725
|
+
readonly tags: string[];
|
|
726
|
+
/** PersistedSessionConfig,本层不透明(JSON 持久化)。 */
|
|
727
|
+
readonly config?: unknown;
|
|
728
|
+
readonly createdAt: number;
|
|
729
|
+
readonly lastActiveAt: number;
|
|
730
|
+
/**
|
|
731
|
+
* 会话条目总数(= sessions.next_seq,与 getEntryCount() 同源)。用于批量枚举场景
|
|
732
|
+
* (listSessions)零额外查询代价地区分"真空会话"(从未 prompt 过)与"有内容但
|
|
733
|
+
* 未加载进内存的冷会话"——不要与 AgentSessionInfo.messageCount 混淆,后者在冷会话
|
|
734
|
+
* 展示路径里可能因性能考虑而恒为占位值。可选:只有读取路径(decodeSession)填充,
|
|
735
|
+
* 写入路径(sessionRowFromSnapshot,供 createSession INSERT 消费)不提供——INSERT
|
|
736
|
+
* 语句本就不写 next_seq 列(固定为 0,见 SQL 字面量),提供也不会被使用。
|
|
737
|
+
*/
|
|
738
|
+
readonly entryCount?: number;
|
|
739
|
+
/** 归档持久化镜像(见 SessionMetadata.archived 契约注释)。 */
|
|
740
|
+
readonly archived?: boolean;
|
|
741
|
+
/** 置顶状态。 */
|
|
742
|
+
readonly pinned?: boolean;
|
|
743
|
+
/** 置顶分组名(RFC-158)。仅 pinned=true 时生效;缺省落入默认组。 */
|
|
744
|
+
readonly pinGroup?: string;
|
|
745
|
+
}
|
|
746
|
+
type SessionEntryType = 'message' | 'compaction' | 'clear';
|
|
747
|
+
/** 追加输入:seq 由仓储发号,调用方只给 entryId(幂等键)+ 内容。 */
|
|
748
|
+
interface EntryInput {
|
|
749
|
+
/** 客户端生成(规则0/1 幂等键);(sessionId, entryId) 唯一。 */
|
|
750
|
+
readonly entryId: string;
|
|
751
|
+
readonly parentId?: string;
|
|
752
|
+
readonly type: SessionEntryType;
|
|
753
|
+
/** 该 entry 的 payload(message / {summary,replacement} / clear),本层不透明。 */
|
|
754
|
+
readonly data: unknown;
|
|
755
|
+
readonly createdAt: number;
|
|
756
|
+
}
|
|
757
|
+
interface EntryRow extends EntryInput {
|
|
758
|
+
readonly sessionId: string;
|
|
759
|
+
/** 会话内插入序(规则0:≠ change_log 游标)。 */
|
|
760
|
+
readonly seq: number;
|
|
761
|
+
}
|
|
762
|
+
interface SessionRepository {
|
|
763
|
+
/**
|
|
764
|
+
* ⚠️ **DORMANT(resume/workspace 关联审计,2026-07-11 核实)**:`workspaces` 表 + 本三方法
|
|
765
|
+
* 当前**零生产调用方**(仅 `sqlite-session-repository.test.ts` 覆盖)——对应 RFC-034 M57
|
|
766
|
+
* 里程碑设计的"web-ui workspaces CRUD"(`POST/GET/DELETE /workspaces`),但该 HTTP 路由
|
|
767
|
+
* 从未实现,只落地了本层 SQLite 原语。当前 local workspace 身份完全靠 `.otto/workspace.json`
|
|
768
|
+
* marker(`@x-otto/workspace` 包)+ `sessions.workspace_key` 列分区,不依赖本表。
|
|
769
|
+
* 参考 RFC-067 M115-04(workspace 相关代码"现实修正:不强删活包"的既有判定)与
|
|
770
|
+
* M115-02(remote-persistence-server 删除前需产品确认)先例——**接它前先把 web-ui
|
|
771
|
+
* workspaces CRUD 路由一并接上**,删它前先问是否已确认 web-ui 落地路线放弃该设计。
|
|
772
|
+
* 勿孤立增删本三方法。
|
|
773
|
+
*/
|
|
774
|
+
upsertWorkspace(ws: WorkspaceRow): Promise<void>;
|
|
775
|
+
getWorkspace(id: string): Promise<WorkspaceRow | null>;
|
|
776
|
+
listWorkspaces(): Promise<WorkspaceRow[]>;
|
|
777
|
+
createSession(row: SessionRow): Promise<void>;
|
|
778
|
+
/**
|
|
779
|
+
* 轻量排序 id 列表(RFC-154 D1)——只读 sessions 表的 `id`/`last_active_at`/`archived` 列,
|
|
780
|
+
* 不碰 `session_entries`(不做 entries 反序列化)。供 `restoreAll` 等批量恢复场景在重量级
|
|
781
|
+
* `load()` 前先选出候选集:按 `last_active_at` 降序、排除 `archived=1`(D1×D3 接线——归档
|
|
782
|
+
* 会话不应被批量恢复路径主动拉起),取前 `limit` 个。
|
|
783
|
+
*
|
|
784
|
+
* 与 `listSessions` 的区别:`listSessions` 返回全量 `SessionRow[]`(含归档,供 /resume 冷会话
|
|
785
|
+
* 列表等需要完整元数据展示的场景),本方法只返回排序后的 id、且过滤掉归档,两者服务不同消费方,
|
|
786
|
+
* 不应合并(合并会破坏 listColdSessionsOverride 需要展示 archived 状态的现有行为)。
|
|
787
|
+
*/
|
|
788
|
+
listRecentSessionIds(workspaceKey: string, limit: number): Promise<string[]>;
|
|
789
|
+
/**
|
|
790
|
+
* 更新活跃叶 + 活跃时间(append 后;leaf_id 是少数可变字段,规则5)。
|
|
791
|
+
* RFC-078 M132-02:title/titleSource 同属可变字段——每次 save 透传以持久化标题变更;
|
|
792
|
+
* 缺省(undefined)时不改动既有标题(COALESCE 语义,避免 null 抹掉已存标题)。
|
|
793
|
+
*
|
|
794
|
+
* config 同属可变字段(修复:createSession 是 ON CONFLICT DO NOTHING 幂等写,只在会话
|
|
795
|
+
* 首次创建时落 config 一次;此前每次 save 都只经 touchSession 更新 leaf/title,config 列
|
|
796
|
+
* 此后永不再更新——write_todos/editedFiles/subagents 等回合内更新的状态在会话创建之后
|
|
797
|
+
* 全部无法持久化,resume 读到的是首次创建时的旧快照)。缺省(undefined)时不改动既有
|
|
798
|
+
* config(COALESCE 语义,与 title 一致);传入时整体覆盖(调用方已做好合并,见
|
|
799
|
+
* PersistenceSync.mergeSnapshotConfig)。
|
|
800
|
+
*
|
|
801
|
+
* archived/pinned 同属可变字段(/archive /pin 命令改后需持久化):缺省不改动既有值。
|
|
802
|
+
*
|
|
803
|
+
* **leaf_id 悬空防护(P0 事故修复,2026-07-12)**:实现必须在写入前校验 `leafId` 真实存在
|
|
804
|
+
* 于本会话的 entries 集合——不存在时不得覆盖既有 leaf_id(等价 COALESCE 语义),并通过
|
|
805
|
+
* 返回值 `leafPersisted:false` 告知调用方"本次 leaf 未采纳",供其记录告警。悬空 leaf_id
|
|
806
|
+
* 会导致 resume 时 fallback 到错误分支(可能是 clear 边界),使 visibleMessages() 返回空、
|
|
807
|
+
* 整段转录对用户不可见(根因:多进程/多实例并发写同一 session 时的竞态窗口,见
|
|
808
|
+
* sqlite-session-repository.ts 实现注释的完整事故分析)。
|
|
809
|
+
* @returns `leafPersisted` — 本次传入的 `leafId` 是否真的被采纳写入(false = 悬空,已拒绝覆盖)。
|
|
810
|
+
*/
|
|
811
|
+
touchSession(id: string, leafId: string | undefined, lastActiveAt: number, title?: string, titleSource?: 'custom' | 'ai' | 'derived', config?: unknown, archived?: boolean, pinned?: boolean, pinGroup?: string): Promise<{
|
|
812
|
+
leafPersisted: boolean;
|
|
813
|
+
}>;
|
|
814
|
+
getSession(id: string): Promise<SessionRow | null>;
|
|
815
|
+
/** 按 workspace_key 分区列出(规则9 隔离);NULL workspace_key 视为 'default'。 */
|
|
816
|
+
listSessions(workspaceKey: string): Promise<SessionRow[]>;
|
|
817
|
+
/**
|
|
818
|
+
* 全量 session id(**跨 workspace**)。孤儿 trace 扫除(RFC-102 D3)用:trace 目录
|
|
819
|
+
* 是全局共享的,孤儿判定必须对照全部 workspace 的会话集合,按分区过滤会误删。
|
|
820
|
+
*/
|
|
821
|
+
listAllSessionIds(): Promise<string[]>;
|
|
822
|
+
deleteSession(id: string): Promise<boolean>;
|
|
823
|
+
/**
|
|
824
|
+
* 追加一条 entry,**仓储发号 seq**(规则1:next_seq 行锁取号,非 MAX)。
|
|
825
|
+
* (sessionId, entryId) 幂等:已存在则返回其既有 seq、不重复落行(规则0/1)。
|
|
826
|
+
* @returns 该 entry 的 seq(会话内插入序)。
|
|
827
|
+
*/
|
|
828
|
+
appendEntry(sessionId: string, entry: EntryInput): Promise<number>;
|
|
829
|
+
/** 读会话 entries(可选 sinceSeq);按 seq 升序。分支重建走 parentId(规则2,调用方在内存做)。 */
|
|
830
|
+
loadEntries(sessionId: string, sinceSeq?: number): Promise<EntryRow[]>;
|
|
831
|
+
/**
|
|
832
|
+
* 读会话尾部最多 `count` 条 entries,按 seq 升序返回(RFC-159 D4 尾窗口加载)。
|
|
833
|
+
* 与 `loadEntries` 并存——不改既有全量语义,窗口意图显式表达。
|
|
834
|
+
*/
|
|
835
|
+
loadTailEntries(sessionId: string, count: number): Promise<EntryRow[]>;
|
|
836
|
+
/**
|
|
837
|
+
* RFC-300 M2:读会话 `seq < beforeSeq` 的最多 `limit` 条 entries,按 seq 升序返回
|
|
838
|
+
* (历史回看补页的反向分页原语——内存顶部向上翻页拉更早)。走 (session_id, seq)
|
|
839
|
+
* 主键索引,O(limit)。`beforeSeq <= 0` 返回空(无更早)。
|
|
840
|
+
*/
|
|
841
|
+
loadEntriesBefore(sessionId: string, beforeSeq: number, limit: number): Promise<EntryRow[]>;
|
|
842
|
+
/**
|
|
843
|
+
* 按 entry_id 集合点查(RFC-160 D3 hydrate 原语,走 (session_id, entry_id) 唯一索引)。
|
|
844
|
+
* 供剥离态 compaction 取回 replacement 真身;返回按 seq 升序,不存在的 id 静默缺席
|
|
845
|
+
* (调用方按需 fail-fast)。
|
|
846
|
+
*/
|
|
847
|
+
loadEntriesByIds(sessionId: string, entryIds: readonly string[]): Promise<EntryRow[]>;
|
|
848
|
+
/**
|
|
849
|
+
* RFC-160 D7:内容寻址 blob 存储(compaction replacement 消息去重)。
|
|
850
|
+
* putBlobs 幂等(INSERT OR IGNORE,hash 主键);getBlobs 返回 hash→content,缺席静默
|
|
851
|
+
* (调用方按需 fail-fast)。content 为序列化后的单条消息 JSON。
|
|
852
|
+
*/
|
|
853
|
+
putBlobs(blobs: ReadonlyArray<{
|
|
854
|
+
hash: string;
|
|
855
|
+
content: string;
|
|
856
|
+
}>): Promise<void>;
|
|
857
|
+
getBlobs(hashes: readonly string[]): Promise<Map<string, string>>;
|
|
858
|
+
/**
|
|
859
|
+
* 按 type 统计会话 entries 行数(RFC-159 D4:窗口加载后 messageCount/compactionCount
|
|
860
|
+
* 由 DB 权威计数,不再依赖"加载集 filter"——保证 UI 计数反映全量而非窗口)。
|
|
861
|
+
*/
|
|
862
|
+
countEntriesByType(sessionId: string, type: string): Promise<number>;
|
|
863
|
+
/**
|
|
864
|
+
* O(1) 读 `next_seq`(= entry 行数,seq 0-based 单调)——增量游标 + 跨进程并发写检测用,
|
|
865
|
+
* 避免 `loadEntries().length` 的 O(n) 全量加载。会话不存在返回 0。
|
|
866
|
+
*/
|
|
867
|
+
getEntryCount(sessionId: string): Promise<number>;
|
|
868
|
+
}
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region src/session-write-lease.d.ts
|
|
871
|
+
/** 拿不到写租约时 save 抛出的类型化错误(供上层识别并发事件与普通 IO 失败)。 */
|
|
872
|
+
declare class SessionWriteLeaseDeniedError extends Error {
|
|
873
|
+
readonly sessionId: string;
|
|
874
|
+
constructor(sessionId: string);
|
|
875
|
+
}
|
|
876
|
+
interface SessionWriteLeaseManager {
|
|
877
|
+
/** 获取(或确认持有)某会话的写租约。false = 另一存活进程持有。幂等。 */
|
|
878
|
+
acquire(sessionId: string): boolean;
|
|
879
|
+
/** 释放某会话的租约(仅当仍归本进程)。幂等。 */
|
|
880
|
+
release(sessionId: string): void;
|
|
881
|
+
/** 释放本进程持有的全部租约(app 停机/进程退出用;同步,exit 钩子安全)。 */
|
|
882
|
+
releaseAll(): void;
|
|
883
|
+
/** 本进程当前是否持有某会话租约(token 实核,非本地缓存)。 */
|
|
884
|
+
isOwner(sessionId: string): boolean;
|
|
885
|
+
/**
|
|
886
|
+
* 只读探测(无副作用,不 acquire/不 touch 锁文件):某会话当前是否被**另一存活进程**
|
|
887
|
+
* 持有写租约。用于 `/resume` 列表等展示场景——提前告知用户"切进去会是只读",
|
|
888
|
+
* 而非等到真正 save 被拒才事后感知(见 handlers/session.ts /resume)。
|
|
889
|
+
*
|
|
890
|
+
* 语义与 acquire() 的接管判定对齐但不产生任何接管/写入:锁文件不存在 → false;
|
|
891
|
+
* 锁文件存在且是本进程持有(isOwner) → false(自己不算"另一进程");
|
|
892
|
+
* 锁文件存在且未失活(未超 staleMs) → true;已失活(视为崩溃残留) → false
|
|
893
|
+
* (不阻止未来 acquire 接管,避免误报"只读")。
|
|
894
|
+
*/
|
|
895
|
+
peekLockedByOther(sessionId: string): boolean;
|
|
896
|
+
/**
|
|
897
|
+
* 只读诊断(无副作用):返回某会话锁的当前状态详情,供用户面向的诊断/强制解锁命令
|
|
898
|
+
* 消费(RFC-171 D4,`/session unlock` 只读模式)。内部复用与 `peekLockedByOther` 相同的
|
|
899
|
+
* 读锁文件逻辑,避免重复实现。
|
|
900
|
+
*/
|
|
901
|
+
inspect(sessionId: string): LeaseInspection;
|
|
902
|
+
/**
|
|
903
|
+
* 强制释放某会话的锁,跳过 token 实核(不要求本进程是持有者)。仅供用户主动触发的
|
|
904
|
+
* 强制解锁命令使用(RFC-171 D4),用于 `acquire()` 存活性探测无法自动恢复的场景
|
|
905
|
+
* (如 hung 进程:心跳停摆但 pid 未退出)。调用方负责在调用前完成风险确认。
|
|
906
|
+
*/
|
|
907
|
+
forceRelease(sessionId: string): void;
|
|
908
|
+
/**
|
|
909
|
+
* 终态关闭(停机专用,幂等):releaseAll() + 此后 acquire() 恒返回 false。
|
|
910
|
+
*
|
|
911
|
+
* 背景(2026-07-14 修复):`/exit -r` 陪跑重启下父进程 spawn 子进程后**不退出**
|
|
912
|
+
* (持续陪跑到子进程退出,见 cli.ts),`process.on('exit')` 释放钩子因此永不触发——
|
|
913
|
+
* stopApp() 的 saveAll() 又会在 spawn 之前 lazy-acquire 租约,导致新会话进程对同一
|
|
914
|
+
* session 永久只读(心跳持续续约,60s stale takeover 永不满足)。
|
|
915
|
+
*
|
|
916
|
+
* `close()` 必须在 stopApp() 返回、子进程 spawn 之前被调用(当前接线在
|
|
917
|
+
* session-pool-factory.ts 的 `onDispose`,位于 agentRuntime.dispose() 内,正确处于
|
|
918
|
+
* saveAll() 之后、app.stop() 返回之前)。`closed` 标志防止 dispose 链之后迟到的
|
|
919
|
+
* fire-and-forget save 重新 acquire、复活已死的锁文件+心跳。
|
|
920
|
+
*/
|
|
921
|
+
close(): void;
|
|
922
|
+
}
|
|
923
|
+
/** `inspect()` 返回的锁状态详情(RFC-171 D4)。 */
|
|
924
|
+
interface LeaseInspection {
|
|
925
|
+
/** 锁文件是否存在。 */
|
|
926
|
+
readonly locked: boolean;
|
|
927
|
+
/** 持有者 pid(旧格式锁文件或畸形内容时为 undefined)。 */
|
|
928
|
+
readonly pid?: number;
|
|
929
|
+
/** 持有者 hostname。 */
|
|
930
|
+
readonly hostname?: string;
|
|
931
|
+
/**
|
|
932
|
+
* 存活性探测结果:`true`=确认存活,`false`=确认死亡,`undefined`=无法判断
|
|
933
|
+
* (跨 hostname、或无 pid 信息)。
|
|
934
|
+
*/
|
|
935
|
+
readonly alive?: boolean;
|
|
936
|
+
/** 锁文件 mtime 距今毫秒数(用于诊断"是否疑似 hung 进程"或"是否刚释放")。 */
|
|
937
|
+
readonly mtimeAgoMs?: number;
|
|
938
|
+
}
|
|
939
|
+
interface SessionWriteLeaseOptions {
|
|
940
|
+
/** 锁文件目录(如 `<sessionsDir>/locks`),不存在时自动创建。 */
|
|
941
|
+
lockDir: string;
|
|
942
|
+
/**
|
|
943
|
+
* 失活阈值:锁文件 mtime 距今超过此值视为持有者已崩溃,可被接管。
|
|
944
|
+
* 必须显著大于 renewEveryMs(默认 60s vs 10s = 6x)。
|
|
945
|
+
*/
|
|
946
|
+
staleMs?: number;
|
|
947
|
+
/** 心跳间隔:后台 unref 定时器 touch 锁文件 mtime,保活 idle 会话的租约。 */
|
|
948
|
+
renewEveryMs?: number;
|
|
949
|
+
/** 是否注册 process exit 钩子同步释放(默认 true;测试注入 false 避免跨用例泄漏)。 */
|
|
950
|
+
installExitHook?: boolean;
|
|
951
|
+
}
|
|
952
|
+
declare function createSessionWriteLeaseManager(options: SessionWriteLeaseOptions): SessionWriteLeaseManager;
|
|
953
|
+
/**
|
|
954
|
+
* RFC-345 §R5:影子模式无操作写租约 stub。
|
|
955
|
+
*
|
|
956
|
+
* 写租约的目的是**跨进程互斥**保护持久化 auth/session 数据(RFC-159 D3,防 2026-07-13
|
|
957
|
+
* 数据丢失事故)。影子会话零落盘、无外部可见引用、无跨进程并发写同一磁盘会话——该风险
|
|
958
|
+
* 从根不存在,故写租约在影子态可安全退化为 no-op(本函数是该豁免的唯一实现处):
|
|
959
|
+
* - acquire 恒 true(本进程"独占"内存会话,无竞争者);
|
|
960
|
+
* - peekLockedByOther / inspect.locked 恒 false(无锁文件);
|
|
961
|
+
* - 全部方法不碰文件系统(不建 lockDir、不写 .lock、无心跳定时器)。
|
|
962
|
+
*
|
|
963
|
+
* 注意:这不削弱 RFC-159 红线——red line 保护的是"持久化数据被并发写破坏",无持久化则
|
|
964
|
+
* 无保护对象。next_seq 单调等**数据完整性**不变量仍由 :memory: SqliteSessionRepository
|
|
965
|
+
* 等价保持(不在本 stub 豁免范围)。
|
|
966
|
+
*/
|
|
967
|
+
declare function createNoopSessionWriteLeaseManager(): SessionWriteLeaseManager;
|
|
968
|
+
//#endregion
|
|
969
|
+
//#region src/relational-session-persistence.d.ts
|
|
970
|
+
/** 极简日志口子(避免 @x-otto/session 依赖 @x-otto/shared);缺省 no-op,App 接线可注真 logger。 */
|
|
971
|
+
interface MinimalLogger {
|
|
972
|
+
warn(obj: Record<string, unknown>, msg: string): void;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* RFC-037 阶段B:会话持久化走 `SessionRepository`(关系化 + append-only),取代 blob 整写。
|
|
976
|
+
* RFC-159:append-forever——save 路径**永不删除** DB entries 行(内存被 capStoredHistory
|
|
977
|
+
* 裁剪后短于 DB 是合法常态),游标 entry_id 制防裁剪位移漏存。
|
|
978
|
+
*
|
|
979
|
+
* - `save`:**只追加新 tail entries**(游标 entry_id 之后的条目)——写放大从 O(n)/save
|
|
980
|
+
* 降到 O(新增条数)。createSession/appendEntry 幂等(规则1),重复 save 安全。
|
|
981
|
+
* - `load`:`reassembleSnapshot` 还原 snapshot → 上层 InMemorySession 照常在内存走 parent 链
|
|
982
|
+
* 重建上下文(规则2:读不优化即天然满足,buildContext 语义不变)。
|
|
983
|
+
*
|
|
984
|
+
* 实例按 `workspaceKey` 绑定(RFC-034 分区);list/listPaginated 仅返回本 workspace 会话。
|
|
985
|
+
*/
|
|
986
|
+
declare class RelationalSessionPersistence<T = unknown> implements SessionPersistence<T> {
|
|
987
|
+
private readonly repo;
|
|
988
|
+
/**
|
|
989
|
+
* 惰性解析 workspaceKey:RFC-034/M60-02 切换中,持久化在 App 构造期建好,而 `workspaceRef`
|
|
990
|
+
* 在 `start()` 才解析(异步 walk-up)。传 getter 即可在首次 save/load(start() 恢复阶段、
|
|
991
|
+
* resolveLocal 之后)读到真 `ws_<id>`,无需重构生命周期。传字符串则为静态键(组件测试用)。
|
|
992
|
+
*/
|
|
993
|
+
private readonly resolveWorkspaceKey;
|
|
994
|
+
private readonly defaultPageSize;
|
|
995
|
+
private readonly logger;
|
|
996
|
+
/**
|
|
997
|
+
* 每会话最后已持久化的 entry_id(增量 append 游标)。
|
|
998
|
+
*
|
|
999
|
+
* RFC-159 D2:游标从"条数制"改"entry_id 制"。条数制在 capStoredHistory 裁剪内存头部后
|
|
1000
|
+
* 会产生数组位移——`entries[已持久化数:]` 切片起点跳过真正的新消息,静默漏存(生产事故
|
|
1001
|
+
* dd6d6e73 的 R4 缺陷)。entry_id 制按身份定位追加起点,与数组位移无关;定位失败
|
|
1002
|
+
* (游标条目已被内存裁掉、或 clear/fork 重建了 entries)退化为全量幂等 append
|
|
1003
|
+
* (appendEntry 按 entry_id 幂等,安全无重复)。
|
|
1004
|
+
*/
|
|
1005
|
+
private readonly persisted;
|
|
1006
|
+
/** RFC-159 D4:load 尾窗口配置(缺省不开窗,全量加载,行为不变)。 */
|
|
1007
|
+
private readonly maxLoadEntries;
|
|
1008
|
+
private readonly canStartHistory;
|
|
1009
|
+
/** RFC-159 D3:per-session 单写者租约(缺省无防护,由组装根注入)。 */
|
|
1010
|
+
private readonly writeLease;
|
|
1011
|
+
/** RFC-160 D2:load 剥离开关(组装根注入)。 */
|
|
1012
|
+
private readonly stripInactiveReplacements;
|
|
1013
|
+
constructor(options: {
|
|
1014
|
+
repo: SessionRepository;
|
|
1015
|
+
workspaceKey?: string | (() => string | undefined);
|
|
1016
|
+
defaultPageSize?: number;
|
|
1017
|
+
logger?: MinimalLogger;
|
|
1018
|
+
/**
|
|
1019
|
+
* RFC-159 D4:超过此条数的会话 load 只取尾部窗口(DB 全量保留,append-forever)。
|
|
1020
|
+
* 调用方传 SESSION_MAX_HISTORY_MESSAGES(@x-otto/session-contract)——本包不依赖
|
|
1021
|
+
* contract 包,经参数注入保持依赖方向。
|
|
1022
|
+
*/
|
|
1023
|
+
maxLoadEntries?: number; /** 窗口切点对齐谓词(防孤儿 tool_result,与 capStoredHistory 同语义)。 */
|
|
1024
|
+
canStartHistory?: (msg: T) => boolean;
|
|
1025
|
+
/**
|
|
1026
|
+
* RFC-159 D3:per-session 单写者租约。注入后每次 save 先 acquire——拿不到
|
|
1027
|
+
* (另一存活进程持有)抛 SessionWriteLeaseDeniedError,本进程对该会话只读。
|
|
1028
|
+
* 缺省不注入 = 无租约防护(单进程测试/嵌入场景),行为不变。
|
|
1029
|
+
*/
|
|
1030
|
+
writeLease?: SessionWriteLeaseManager; /** RFC-160 D2:load 时剥离非活跃 compaction 的 replacement(缺省 false,行为不变)。 */
|
|
1031
|
+
stripInactiveReplacements?: boolean;
|
|
1032
|
+
});
|
|
1033
|
+
/**
|
|
1034
|
+
* RFC-159 D3:archive/restore 同样是**写路径**,必须过写租约闸门。
|
|
1035
|
+
*
|
|
1036
|
+
* RFC-330 M2 审查发现:archive 写 `sessions.archived=1`、restore 追加 entries,
|
|
1037
|
+
* 二者都改 DB,但此前直接透传 repo、绕过了 `save()` 上的租约检查——
|
|
1038
|
+
* 另一存活进程持锁时仍会被本进程改写,与 D3"绝不静默覆盖"的红线冲突。
|
|
1039
|
+
* 此处复用与 save() 完全相同的 acquire 语义(幂等 + stale takeover)。
|
|
1040
|
+
*/
|
|
1041
|
+
private assertWritable;
|
|
1042
|
+
/** RFC-325 M4:透传 repository 的 archive 能力(只有 SQLite repo 实现)。 */
|
|
1043
|
+
archiveSession(sessionId: string): Promise<string>;
|
|
1044
|
+
/** RFC-325 M4:透传 repository 的 restore 能力。 */
|
|
1045
|
+
restoreArchivedSession(sessionId: string): Promise<number>;
|
|
1046
|
+
save(snapshot: SessionSnapshot<T>): Promise<void>;
|
|
1047
|
+
/**
|
|
1048
|
+
* 修复(RFC-cross-workspace-restore-guard):跨 workspace 越界读取防护。
|
|
1049
|
+
* 此前本方法只按 `id` 查询,不校验会话真实归属的 `workspace_key` 是否与本
|
|
1050
|
+
* persistence 实例当前解析的 workspaceKey 一致——任何知道其他 workspace 会话
|
|
1051
|
+
* id 的调用方(如 `--continue <id>` / `sessionManager.restore(id)` 兜底路径)都能
|
|
1052
|
+
* 绕过 `/resume` 列表层面的 workspace 分区隔离,跨项目拉起并非本 workspace 的会话。
|
|
1053
|
+
* `/resume` 正常路径本身安全(先经 `listAllSessions` 过滤,找不到不会走到这里),
|
|
1054
|
+
* 但 `load()` 作为持久层唯一入口理应自身闭环——不能把隔离职责完全甩给每个调用方。
|
|
1055
|
+
* 归属不一致时 fail-closed 返回 null(与"会话不存在"同语义,不泄漏其存在性),并记警告日志。
|
|
1056
|
+
*/
|
|
1057
|
+
load(id: string): Promise<SessionSnapshot<T> | null>;
|
|
1058
|
+
/**
|
|
1059
|
+
* RFC-160 D2a:暴露 entry_id 持久化游标(只读),供 runtime 构造运行时剥离资格谓词
|
|
1060
|
+
* ——"前条 compaction 已持久化"判定查此游标而非 DB(save 是异步 fire-and-forget,
|
|
1061
|
+
* 查 DB 有假阴性;游标只在 save 链成功后推进,是"已交给持久层"的可靠信号)。
|
|
1062
|
+
*/
|
|
1063
|
+
getPersistedCursor(sessionId: string): string | undefined;
|
|
1064
|
+
/**
|
|
1065
|
+
* RFC-160 D3:剥离态 hydrate 能力(ReplacementHydrationCapable)。runtime 三出口
|
|
1066
|
+
* (checkpoint 捕获前/fork 前/restore 后)经能力探测调用,点查回填 replacement 真身。
|
|
1067
|
+
*/
|
|
1068
|
+
hydrateReplacements(sessionId: string, entries: readonly SessionEntry<T>[]): Promise<SessionEntry<T>[]>;
|
|
1069
|
+
delete(id: string): Promise<boolean>;
|
|
1070
|
+
list(): Promise<string[]>;
|
|
1071
|
+
/** RFC-154 D1:委托仓储层 SQL 排序+归档过滤+LIMIT,零额外内存开销(不拉 entries)。 */
|
|
1072
|
+
listRecentMeta(limit: number): Promise<string[]>;
|
|
1073
|
+
listPaginated(options: PaginationOptions): Promise<PaginatedResult<string>>;
|
|
1074
|
+
}
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region src/relational-bridge.d.ts
|
|
1077
|
+
/** snapshot → sessions 行(供 explode / RelationalSessionPersistence 复用)。 */
|
|
1078
|
+
declare function sessionRowFromSnapshot<T>(snapshot: SessionSnapshot<T>, workspaceKey: string): SessionRow;
|
|
1079
|
+
/** 一条 SessionEntry → 仓储输入(id/parentId/type/timestamp 进列,其余进 data)。 */
|
|
1080
|
+
declare function entryToInput<T>(e: SessionEntry<T>): EntryInput;
|
|
1081
|
+
/** 炸开 snapshot 到仓储行(createSession + 逐条 appendEntry,保序 + parentId/leaf/config)。 */
|
|
1082
|
+
declare function explodeSnapshot<T>(snapshot: SessionSnapshot<T>, workspaceKey: string, repo: SessionRepository): Promise<void>;
|
|
1083
|
+
/** RFC-159 D4:尾窗口加载选项。 */
|
|
1084
|
+
interface ReassembleOptions<T> {
|
|
1085
|
+
/** 加载条数上限:DB 条目数超过时只取尾部窗口(缺省 = 全量,行为不变)。 */
|
|
1086
|
+
maxEntries?: number;
|
|
1087
|
+
/**
|
|
1088
|
+
* 窗口切点对齐谓词(与 InMemorySession.capStoredHistory 同语义):窗口首条消息必须
|
|
1089
|
+
* 满足此谓词(如 role !== 'tool_result',防孤儿 tool_result 触发 provider 400)。
|
|
1090
|
+
* 不满足的前导消息被丢弃;非 message 条目(compaction/clear)天然是安全切点,保留。
|
|
1091
|
+
* 注:compaction 的 replacement 内嵌在单条 entry 的 data 内(非独立行),窗口切割
|
|
1092
|
+
* 不可能拆散 compaction↔replacement 结构。
|
|
1093
|
+
*/
|
|
1094
|
+
canStartHistory?: (msg: T) => boolean;
|
|
1095
|
+
/**
|
|
1096
|
+
* RFC-160 D2/D5:装载时剥离"非活跃"compaction 的 replacement(内存投影,DB 不动)。
|
|
1097
|
+
* 保守保留集 = 当前 leafId parent 链上的**全部** compaction + 加载集内 seq 最大的
|
|
1098
|
+
* compaction;其余置 replacementStripped 标记并丢弃 replacement 数组(summary 保留)。
|
|
1099
|
+
* 时序:尾窗口化**先于**剥离——剥离面对窗口化后的加载集。缺省 false(行为不变)。
|
|
1100
|
+
*/
|
|
1101
|
+
stripInactiveReplacements?: boolean;
|
|
1102
|
+
}
|
|
1103
|
+
/** 从仓储行重组 SessionSnapshot(metadata 计数派生,R4;窗口加载时计数走 DB 权威,RFC-159 D4)。 */
|
|
1104
|
+
declare function reassembleSnapshot<T>(id: string, repo: SessionRepository, options?: ReassembleOptions<T>): Promise<SessionSnapshot<T> | null>;
|
|
1105
|
+
//#endregion
|
|
1106
|
+
//#region src/replacement-blob-codec.d.ts
|
|
1107
|
+
/** 单条消息的内容寻址哈希(sha256 hex,前缀标记算法便于未来演进)。 */
|
|
1108
|
+
declare function hashMessageContent(serialized: string): string;
|
|
1109
|
+
/**
|
|
1110
|
+
* 写侧:把 data blob 里的内嵌 replacement 转为 hashes(新格式),消息本体落 blob 表。
|
|
1111
|
+
* 非 compaction / 无 replacement / 已是新格式 → 原样返回(幂等)。
|
|
1112
|
+
* 返回值是**新对象**(不变更入参)。
|
|
1113
|
+
*/
|
|
1114
|
+
declare function encodeReplacementToBlobs(entryType: string, data: Record<string, unknown>, repo: SessionRepository): Promise<Record<string, unknown>>;
|
|
1115
|
+
/**
|
|
1116
|
+
* 读侧:把新格式行的 replacementHashes 组装回内嵌 replacement(顺序保持)。
|
|
1117
|
+
* 旧格式(内嵌 replacement)/ 无 hashes → 原样返回。
|
|
1118
|
+
* @throws blob 缺席(理论上仅残缺导入)——fail-fast,不允许静默丢消息。
|
|
1119
|
+
*/
|
|
1120
|
+
declare function decodeReplacementFromBlobs(entryType: string, data: Record<string, unknown>, repo: SessionRepository): Promise<Record<string, unknown>>;
|
|
1121
|
+
//#endregion
|
|
1122
|
+
//#region src/replacement-hydrator.d.ts
|
|
1123
|
+
/**
|
|
1124
|
+
* 持久层可选能力接口(RFC-160 D3):支持剥离态 hydrate 的 persistence 实现它。
|
|
1125
|
+
* runtime 的三出口(checkpoint/fork/restore)按能力探测调用——非 Relational 后端
|
|
1126
|
+
* (file/memory/remote)不实现,出口断言 hasStrippedReplacements 恒 false(它们不剥离),
|
|
1127
|
+
* 天然跳过。
|
|
1128
|
+
*/
|
|
1129
|
+
interface ReplacementHydrationCapable<T = unknown> {
|
|
1130
|
+
hydrateReplacements(sessionId: string, entries: readonly SessionEntry<T>[]): Promise<SessionEntry<T>[]>;
|
|
1131
|
+
}
|
|
1132
|
+
/** 能力探测(类型守卫)。 */
|
|
1133
|
+
declare function supportsReplacementHydration<T>(persistence: unknown): persistence is ReplacementHydrationCapable<T>;
|
|
1134
|
+
/** entries 中是否存在剥离态 compaction(D4 出口断言的判定原语)。 */
|
|
1135
|
+
declare function hasStrippedReplacements<T>(entries: readonly SessionEntry<T>[]): boolean;
|
|
1136
|
+
/**
|
|
1137
|
+
* 回填 entries 中全部剥离态 compaction 的 replacement 真身。
|
|
1138
|
+
* 返回新数组(原数组不变更);无剥离态时原样返回(零拷贝快路径)。
|
|
1139
|
+
* @throws DB 行不可达时(理论上仅跨机导入残缺数据)——fail-fast,不允许剥离态继续。
|
|
1140
|
+
*/
|
|
1141
|
+
declare function hydrateStrippedReplacements<T>(sessionId: string, entries: readonly SessionEntry<T>[], repo: SessionRepository): Promise<SessionEntry<T>[]>;
|
|
1142
|
+
//#endregion
|
|
1143
|
+
//#region src/constants.d.ts
|
|
1144
|
+
/**
|
|
1145
|
+
* 会话域容量/超时/快照版本调优常量(RFC-074 M8-06c / R-ENVCONST)。
|
|
1146
|
+
*
|
|
1147
|
+
* 从 @x-otto/env 的 god-constants 桶迁回 session——会话域调优常量归会话包。消费方:session 包内
|
|
1148
|
+
* (各 persistence)+ runtime(SessionPool/time-travel/sync,已依赖 session)+ coding(SessionPool 容量)。
|
|
1149
|
+
* 仍用 env 的 `normalizeEnv` 解析环境变量(真跨切面 helper 留 env)。
|
|
1150
|
+
*
|
|
1151
|
+
* **例外**:会话历史存储条数上限(`SESSION_MAX_HISTORY_MESSAGES`)**不在本文件**——它的真实
|
|
1152
|
+
* 消费方分处 `@x-otto/runtime`(`persistence-sync.ts`)与 `@x-otto/tui`(`context-store.ts`)两个
|
|
1153
|
+
* 不同包,而非 `@x-otto/session` 包内部,故归口在两者共同依赖的 `@x-otto/session-contract`
|
|
1154
|
+
* (见 `packages/session-contract/src/constants.ts`),供跨包共享单一真源。RFC-155 M155-1。
|
|
1155
|
+
*/
|
|
1156
|
+
declare const SESSION_IDLE_TIMEOUT_MS: number;
|
|
1157
|
+
declare const SESSION_MAX: number;
|
|
1158
|
+
declare const SESSION_PAGE_SIZE: number;
|
|
1159
|
+
declare const SESSION_SNAPSHOT_VERSION: number;
|
|
1160
|
+
//#endregion
|
|
1161
|
+
//#region src/sqlite-session-repository.d.ts
|
|
1162
|
+
/**
|
|
1163
|
+
* 关系化会话 schema 段(workspaces + sessions + session_entries)。
|
|
1164
|
+
* RFC-074 M5-02b:随 SessionRepository 从 @x-otto/persistence 迁回本包——会话 schema 与领域同归属。
|
|
1165
|
+
* 经 `acquireDb(dbPath, wal, SESSION_MIGRATIONS)` 注入给通用 sqlite 连接基建执行。会话表间无外键,
|
|
1166
|
+
* 且会话 db(sessions.db)独立于 blob db(storage.db),故只需本段、无需与 blob 段 compose。
|
|
1167
|
+
*/
|
|
1168
|
+
declare const SESSION_MIGRATIONS: Migration[];
|
|
1169
|
+
interface SqliteSessionRepositoryOptions {
|
|
1170
|
+
dbPath: string;
|
|
1171
|
+
wal?: boolean;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* SQLite 实现(RFC-037 阶段A)。schema = workspaces / sessions / session_entries(SESSION_MIGRATIONS)。
|
|
1175
|
+
* 通过 @x-otto/persistence 的通用连接基建 `acquireDb`/`releaseDb` 打开 sessions.db,DDL 由注入的
|
|
1176
|
+
* SESSION_MIGRATIONS 经 applyMigrations 统一管理;本类不自建表。
|
|
1177
|
+
*/
|
|
1178
|
+
declare class SqliteSessionRepository implements SessionRepository {
|
|
1179
|
+
private readonly db;
|
|
1180
|
+
private readonly archiveDir;
|
|
1181
|
+
/** RFC-345:内存会话(dbPath=':memory:')——归档不落盘(磁盘零写;:memory: 无跨重启需求)。 */
|
|
1182
|
+
private readonly ephemeral;
|
|
1183
|
+
constructor(options: SqliteSessionRepositoryOptions);
|
|
1184
|
+
close(): void;
|
|
1185
|
+
/** RFC-324 D4 M4:归档——把会话的全部 entries 读出,逐行写入 .jsonl.gz(gzip 压缩)。
|
|
1186
|
+
* 不删除 entries 行(RFC-159 R4 红线),只标记 archived=1 + 复制到外部文件。
|
|
1187
|
+
* 返回值:归档文件路径。 */
|
|
1188
|
+
archiveSession(sessionId: string): Promise<string>;
|
|
1189
|
+
/** RFC-324 D4 M4:恢复——从 .jsonl.gz 读回并重建 entries(RFC-159 append-forever)
|
|
1190
|
+
* 已有的 entries 不删(幂等 append 可能产生重复,appendEntry 幂等去重可容忍),
|
|
1191
|
+
* 仅当 DB 中的条目数 < 归档条目数时追加缺失部分。 */
|
|
1192
|
+
restoreArchivedSession(sessionId: string): Promise<number>;
|
|
1193
|
+
upsertWorkspace(ws: WorkspaceRow): Promise<void>;
|
|
1194
|
+
getWorkspace(id: string): Promise<WorkspaceRow | null>;
|
|
1195
|
+
listWorkspaces(): Promise<WorkspaceRow[]>;
|
|
1196
|
+
createSession(row: SessionRow): Promise<void>;
|
|
1197
|
+
touchSession(id: string, leafId: string | undefined, lastActiveAt: number, title?: string, titleSource?: 'custom' | 'ai' | 'derived', config?: unknown, archived?: boolean, pinned?: boolean, pinGroup?: string): Promise<{
|
|
1198
|
+
leafPersisted: boolean;
|
|
1199
|
+
}>;
|
|
1200
|
+
getSession(id: string): Promise<SessionRow | null>;
|
|
1201
|
+
listSessions(workspaceKey: string): Promise<SessionRow[]>;
|
|
1202
|
+
listAllSessionIds(): Promise<string[]>;
|
|
1203
|
+
/**
|
|
1204
|
+
* 批量恢复候选(`restoreAll` 专用)。三重过滤:workspace 分区 + 未归档 + **非真空**。
|
|
1205
|
+
*
|
|
1206
|
+
* RFC-336 D1-a:`next_seq > 0` 是 D1(会话行预建)的**前置条件**,不是可选优化。
|
|
1207
|
+
* D1 让"会话创建即落 sessions 行"成为常态,于是从未 prompt 过的真空会话也会留行,
|
|
1208
|
+
* 且其 `last_active_at` 恰是创建时间(很新)→ 在 `ORDER BY last_active_at DESC` 下
|
|
1209
|
+
* 排在最前,会按 `room` 名额优先占位,把真正有内容的会话挤出恢复窗口。
|
|
1210
|
+
* 那不是"用户看到垃圾"级的 UX 问题,而是**池容量被空壳耗尽导致真实会话无法恢复**的
|
|
1211
|
+
* 功能性回归。故 D1 与本过滤必须同一里程碑落地(RFC-336 重要事项规则 8)。
|
|
1212
|
+
*
|
|
1213
|
+
* `next_seq` 与 `getEntryCount()`/`SessionRow.entryCount` 同源(RFC-159 D5 单调不回退),
|
|
1214
|
+
* 已是 sessions 表列,过滤零额外查询代价、不触 session_entries。
|
|
1215
|
+
*
|
|
1216
|
+
* **不变量**:本过滤只作用于**批量**恢复候选。`restore(id)` 按需单条恢复不得加此过滤——
|
|
1217
|
+
* 用户显式给出 id 时应能恢复空会话(同 `/resume` 对 arg 场景的既有豁免语义)。
|
|
1218
|
+
*/
|
|
1219
|
+
listRecentSessionIds(workspaceKey: string, limit: number): Promise<string[]>;
|
|
1220
|
+
deleteSession(id: string): Promise<boolean>;
|
|
1221
|
+
appendEntry(sessionId: string, entry: EntryInput): Promise<number>;
|
|
1222
|
+
loadEntries(sessionId: string, sinceSeq?: number): Promise<EntryRow[]>;
|
|
1223
|
+
getEntryCount(sessionId: string): Promise<number>;
|
|
1224
|
+
loadTailEntries(sessionId: string, count: number): Promise<EntryRow[]>;
|
|
1225
|
+
loadEntriesBefore(sessionId: string, beforeSeq: number, limit: number): Promise<EntryRow[]>;
|
|
1226
|
+
countEntriesByType(sessionId: string, type: string): Promise<number>;
|
|
1227
|
+
putBlobs(blobs: ReadonlyArray<{
|
|
1228
|
+
hash: string;
|
|
1229
|
+
content: string;
|
|
1230
|
+
}>): Promise<void>;
|
|
1231
|
+
getBlobs(hashes: readonly string[]): Promise<Map<string, string>>;
|
|
1232
|
+
loadEntriesByIds(sessionId: string, entryIds: readonly string[]): Promise<EntryRow[]>;
|
|
1233
|
+
/**
|
|
1234
|
+
* IN 子句分批查询(终局审查 2026-07-18 S14:getBlobs/loadEntriesByIds 的同构分批循环
|
|
1235
|
+
* 收束为单一实现)。按 500/批切分防 SQLite 绑定变量上限(999);`prefixParams` 是每批
|
|
1236
|
+
* 固定前置绑定参数(如 session_id),批内 IN 列表参数追加其后。
|
|
1237
|
+
*/
|
|
1238
|
+
private batchInQuery;
|
|
1239
|
+
}
|
|
1240
|
+
//#endregion
|
|
1241
|
+
//#region src/remote-persistence.d.ts
|
|
1242
|
+
interface RemoteSessionPersistenceOptions {
|
|
1243
|
+
baseUrl: string;
|
|
1244
|
+
getAuth: () => Promise<{
|
|
1245
|
+
token: string;
|
|
1246
|
+
}>;
|
|
1247
|
+
getHeaders?: () => Promise<Record<string, string>>;
|
|
1248
|
+
fetch: typeof globalThis.fetch;
|
|
1249
|
+
timeoutMs: number;
|
|
1250
|
+
/** M71b: user identity id,非空时所有请求带 `X-Otto-User-Id` header。 */
|
|
1251
|
+
userId?: string;
|
|
1252
|
+
/**
|
|
1253
|
+
* workspace 分区键(`ws_<id>`,缺省 'default')——修复(N3,resume/workspace 关联审计):
|
|
1254
|
+
* 此前 remote 后端从不透传 workspaceRef.key,所有远端会话请求恒落 'default' 分区,
|
|
1255
|
+
* 与本地 SQLite 后端(按 workspace_key 分区)行为不一致——多个本地 workspace 共享同一
|
|
1256
|
+
* `--session-url` 时,服务端的 `/:ns/:wsKey/:id` 分区能力被架空,会话互相可见/冲突。
|
|
1257
|
+
* 客户端侧仅做 URL 路径拼接(见 RemotePersistence.wsPath),真正的隔离仍需服务端
|
|
1258
|
+
* `workspaceAuth` 中间件 + token↔workspace scope 校验(M57,已就位)。
|
|
1259
|
+
* 支持惰性 getter(消费方在 App 构造期创建实例,workspaceRef 要到 start() 才解析完成,
|
|
1260
|
+
* 与 RelationalSessionPersistence.workspaceKey 同因,见 Bug1 修复记录)。
|
|
1261
|
+
*/
|
|
1262
|
+
wsKey?: string | (() => string | undefined);
|
|
1263
|
+
}
|
|
1264
|
+
declare class RemoteSessionPersistence<T = unknown> extends RemotePersistence<SessionSnapshot<T>> implements SessionPersistence<T> {
|
|
1265
|
+
/**
|
|
1266
|
+
* RFC-305 M3:远端 save 是整份快照 PUT(无游标增量),每 turn 上传随会话线性变贵 →
|
|
1267
|
+
* 声明 'snapshot',PersistenceSync 据此对 turn 级落库节流(prompt.end 仍强制落库)。
|
|
1268
|
+
* 未来服务端提供 append-tail endpoint 后改此声明即可自动升级 every-turn。
|
|
1269
|
+
*/
|
|
1270
|
+
readonly turnFlush: "snapshot";
|
|
1271
|
+
constructor(options: RemoteSessionPersistenceOptions);
|
|
1272
|
+
protected get serviceLabel(): string;
|
|
1273
|
+
protected get endpointFlag(): string;
|
|
1274
|
+
protected extractId(snapshot: SessionSnapshot<T>): string;
|
|
1275
|
+
/**
|
|
1276
|
+
* RFC-154 D1:走服务端新增的专用端点 `GET /sessions/:wsKey/recent?limit=N`(不是修复既有
|
|
1277
|
+
* `listPaginated`——该方法在服务端返回 shape 与客户端类型契约不匹配,是独立 backlog,见
|
|
1278
|
+
* `packages/remote-persistence-server/src/routes/storage.ts` 头注)。服务端已保证排序
|
|
1279
|
+
* (lastActiveAt 降序)+ 归档过滤,客户端直接透传结果。
|
|
1280
|
+
*/
|
|
1281
|
+
listRecentMeta(limit: number): Promise<string[]>;
|
|
1282
|
+
}
|
|
1283
|
+
//#endregion
|
|
1284
|
+
export { type DraftEntry, type EditedFile, type EntryInput, type EntryRow, FileSessionPersistence, type FileSessionPersistenceOptions, InMemorySession, InMemorySessionPersistence, InMemorySessionStore, type LeaseInspection, type PaginationOptions, type PersistedSessionConfig, type ReassembleOptions, RelationalSessionPersistence, RemotePersistenceError, RemoteSessionPersistence, type RemoteSessionPersistenceOptions, RemoteSnapshotConflictError, type ReplacementHydrationCapable, SESSION_IDLE_TIMEOUT_MS, SESSION_MAX, SESSION_MIGRATIONS, SESSION_PAGE_SIZE, SESSION_SNAPSHOT_VERSION, type Session, type SessionContext, type SessionEntry, type SessionEntryType, type SessionMetadata, type SessionPersistence, type SessionRepository, type SessionRow, type SessionSnapshot, type SessionStore, SessionWriteLeaseDeniedError, type SessionWriteLeaseManager, type SessionWriteLeaseOptions, SqliteSessionRepository, type SqliteSessionRepositoryOptions, type SubagentEntry, type TodoItem, type TurnSummaryEntry, type WorkspaceRow, createInMemorySessionStore, createNoopSessionWriteLeaseManager, createSessionWriteLeaseManager, decodeReplacementFromBlobs, encodeReplacementToBlobs, entryToInput, estimateMessageContentBytes, explodeSnapshot, hasStrippedReplacements, hashMessageContent, hydrateStrippedReplacements, reassembleSnapshot, sessionRowFromSnapshot, supportsReplacementHydration };
|
|
1285
|
+
//# sourceMappingURL=index.d.ts.map
|